#!/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