Files
gh-kylesnowschwartz-simplec…/hooks/handlers/pre_compact_handler.rb
2025-11-30 08:36:27 +08:00

358 lines
9.6 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'claude_hooks'
# PreCompact Handler
#
# PURPOSE: Control and customize transcript compaction before it occurs
# TRIGGERS: Before Claude Code compacts (summarizes) the conversation transcript
#
# COMMON USE CASES:
# - Preserve important conversation segments from compaction
# - Extract and save key decisions or insights before compaction
# - Backup full transcript before summarization
# - Add metadata or tags to preserve context
# - Control which parts of the conversation should be preserved
# - Generate custom summaries alongside system compaction
#
# SETTINGS.JSON CONFIGURATION:
# {
# "hooks": {
# "PreCompact": [{
# "matcher": "",
# "hooks": [{
# "type": "command",
# "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/entrypoints/pre_compact.rb"
# }]
# }]
# }
# }
class PreCompactHandler < ClaudeHooks::PreCompact
def call
log "Processing pre-compaction for session: #{session_id}"
# Example: Backup full transcript
# backup_full_transcript
# Example: Extract key insights before compaction
# extract_key_insights
# Example: Preserve important segments
# mark_segments_for_preservation
# Example: Generate custom summary
# generate_custom_summary
# Allow compaction to proceed
allow_continue!
output_data
end
private
def backup_full_transcript
log 'Backing up full transcript before compaction'
if transcript_path && File.exist?(transcript_path)
transcript_content = read_transcript
# Example: Save to backup location
backup_path = generate_backup_path
# File.write(backup_path, transcript_content)
log "Full transcript backed up to: #{backup_path}"
log "Transcript size: #{transcript_content.length} characters"
else
log 'No transcript found to backup', level: :warn
end
end
def extract_key_insights
log 'Extracting key insights before compaction'
return unless transcript_path && File.exist?(transcript_path)
transcript = read_transcript
insights = analyze_transcript_for_insights(transcript)
if insights.any?
log "Extracted #{insights.length} key insights"
# save_insights_to_file(insights)
insights.each_with_index do |insight, index|
log "Insight #{index + 1}: #{insight[:type]} - #{insight[:summary][0..100]}..."
end
else
log 'No key insights found in transcript'
end
end
def mark_segments_for_preservation
log 'Identifying segments to preserve from compaction'
return unless transcript_path && File.exist?(transcript_path)
transcript = read_transcript
important_segments = identify_important_segments(transcript)
if important_segments.any?
log "Identified #{important_segments.length} important segments for preservation"
# Example: Mark segments with special markers that compaction should preserve
important_segments.each do |segment|
log "Preserving: #{segment[:type]} at #{segment[:location]}"
end
# save_preservation_markers(important_segments)
else
log 'No critical segments identified for preservation'
end
end
def generate_custom_summary
log 'Generating custom summary before system compaction'
return unless transcript_path && File.exist?(transcript_path)
transcript = read_transcript
custom_summary = create_structured_summary(transcript)
log "Generated custom summary with #{custom_summary.keys.length} sections"
# save_custom_summary(custom_summary)
custom_summary.each do |section, content|
log "#{section}: #{content[:count]} items" if content.is_a?(Hash) && content[:count]
end
end
def analyze_transcript_for_insights(transcript)
insights = []
lines = transcript.split("\n")
# Look for decision points
decision_patterns = [
/decided to/i,
/chose to/i,
/selected/i,
/will use/i,
/approach will be/i
]
decision_patterns.each do |pattern|
lines.each_with_index do |line, index|
next unless line.match?(pattern)
insights << {
type: 'decision',
line_number: index + 1,
content: line.strip,
summary: extract_decision_summary(line)
}
end
end
# Look for important findings
finding_patterns = [
/found that/i,
/discovered/i,
/identified/i,
/analysis shows/i,
/results indicate/i
]
finding_patterns.each do |pattern|
lines.each_with_index do |line, index|
next unless line.match?(pattern)
insights << {
type: 'finding',
line_number: index + 1,
content: line.strip,
summary: extract_finding_summary(line)
}
end
end
# Look for code changes
code_patterns = [
/implemented/i,
/added function/i,
/created class/i,
/modified/i,
/refactored/i
]
code_patterns.each do |pattern|
lines.each_with_index do |line, index|
next unless line.match?(pattern)
insights << {
type: 'code_change',
line_number: index + 1,
content: line.strip,
summary: extract_code_change_summary(line)
}
end
end
insights
end
def identify_important_segments(transcript)
segments = []
lines = transcript.split("\n")
# Identify error resolution segments
error_context = []
in_error_resolution = false
lines.each_with_index do |line, index|
if line.match?(/error|exception|failed/i)
in_error_resolution = true
error_context = [index]
elsif in_error_resolution && line.match?(/fixed|resolved|solved|working/i)
error_context << index
segments << {
type: 'error_resolution',
location: "lines #{error_context.first}-#{error_context.last}",
start_line: error_context.first,
end_line: error_context.last,
summary: 'Error resolution sequence'
}
in_error_resolution = false
error_context = []
end
# Identify architectural decisions
next unless line.match?(/architecture|design pattern|structure|approach/i)
segments << {
type: 'architectural_decision',
location: "line #{index + 1}",
line_number: index + 1,
content: line.strip,
summary: 'Architectural decision or discussion'
}
# Identify code review segments
next unless line.match?(/review|feedback|suggestion|improvement/i)
segments << {
type: 'code_review',
location: "line #{index + 1}",
line_number: index + 1,
content: line.strip,
summary: 'Code review or improvement suggestion'
}
end
segments
end
def create_structured_summary(transcript)
lines = transcript.split("\n")
{
session_info: {
session_id: session_id,
line_count: lines.length,
character_count: transcript.length,
compaction_time: Time.now
},
tools_used: extract_tools_used(lines),
files_modified: extract_files_modified(lines),
key_decisions: extract_decisions(lines),
errors_and_resolutions: extract_error_resolutions(lines),
user_goals: extract_user_goals(lines)
}
end
def extract_tools_used(lines)
tools = []
lines.each do |line|
if (match = line.match(/using (\w+) tool/i) || line.match(/tool: (\w+)/i))
tools << match[1]
end
end
{ tools: tools.uniq, count: tools.uniq.length }
end
def extract_files_modified(lines)
files = []
lines.each do |line|
if (match = line.match(/modified file: (.+)/i) || line.match(/editing (.+\.\w+)/i))
files << match[1].strip
end
end
{ files: files.uniq, count: files.uniq.length }
end
def extract_decisions(lines)
decisions = []
lines.each_with_index do |line, index|
next unless line.match?(/decided|chose|selected|will use/i)
decisions << {
line: index + 1,
summary: line.strip[0..100] + (line.length > 100 ? '...' : '')
}
end
{ decisions: decisions, count: decisions.length }
end
def extract_error_resolutions(lines)
resolutions = []
lines.each_with_index do |line, index|
next unless line.match?(/fixed|resolved|solved/i) &&
lines[(index - 1)..(index + 1)].any? { |l| l.match?(/error|exception/i) }
resolutions << {
line: index + 1,
summary: line.strip[0..100] + (line.length > 100 ? '...' : '')
}
end
{ resolutions: resolutions, count: resolutions.length }
end
def extract_user_goals(lines)
goals = []
lines.each_with_index do |line, index|
next unless line.match?(/^user:|^User:/i) && line.match?(/help me|I need|can you|please/i)
goals << {
line: index + 1,
summary: line.strip[0..150] + (line.length > 150 ? '...' : '')
}
end
{ goals: goals, count: goals.length }
end
def generate_backup_path
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
project_path_for("backups/transcripts/#{session_id}_#{timestamp}.txt")
end
def extract_decision_summary(line)
line.strip[0..100] + (line.length > 100 ? '...' : '')
end
def extract_finding_summary(line)
line.strip[0..100] + (line.length > 100 ? '...' : '')
end
def extract_code_change_summary(line)
line.strip[0..100] + (line.length > 100 ? '...' : '')
end
end
# Testing support - run this file directly to test with sample data
if __FILE__ == $PROGRAM_NAME
ClaudeHooks::CLI.test_runner(PreCompactHandler) do |input_data|
input_data['session_id'] = 'test-session-01'
input_data['reason'] = 'transcript_size_limit'
end
end