Skip to content

Commit 076acab

Browse files
committed
add PreCompact and SessionStart
1 parent 37011e7 commit 076acab

File tree

4 files changed

+395
-2
lines changed

4 files changed

+395
-2
lines changed

.claude/hooks/pre_compact.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.11"
4+
# dependencies = [
5+
# "python-dotenv",
6+
# ]
7+
# ///
8+
9+
import argparse
10+
import json
11+
import os
12+
import sys
13+
from pathlib import Path
14+
from datetime import datetime
15+
16+
try:
17+
from dotenv import load_dotenv
18+
load_dotenv()
19+
except ImportError:
20+
pass # dotenv is optional
21+
22+
23+
def log_pre_compact(input_data):
24+
"""Log pre-compact event to logs directory."""
25+
# Ensure logs directory exists
26+
log_dir = Path("logs")
27+
log_dir.mkdir(parents=True, exist_ok=True)
28+
log_file = log_dir / 'pre_compact.json'
29+
30+
# Read existing log data or initialize empty list
31+
if log_file.exists():
32+
with open(log_file, 'r') as f:
33+
try:
34+
log_data = json.load(f)
35+
except (json.JSONDecodeError, ValueError):
36+
log_data = []
37+
else:
38+
log_data = []
39+
40+
# Append the entire input data
41+
log_data.append(input_data)
42+
43+
# Write back to file with formatting
44+
with open(log_file, 'w') as f:
45+
json.dump(log_data, f, indent=2)
46+
47+
48+
def backup_transcript(transcript_path, trigger):
49+
"""Create a backup of the transcript before compaction."""
50+
try:
51+
if not os.path.exists(transcript_path):
52+
return
53+
54+
# Create backup directory
55+
backup_dir = Path("logs") / "transcript_backups"
56+
backup_dir.mkdir(parents=True, exist_ok=True)
57+
58+
# Generate backup filename with timestamp and trigger type
59+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
60+
session_name = Path(transcript_path).stem
61+
backup_name = f"{session_name}_pre_compact_{trigger}_{timestamp}.jsonl"
62+
backup_path = backup_dir / backup_name
63+
64+
# Copy transcript to backup
65+
import shutil
66+
shutil.copy2(transcript_path, backup_path)
67+
68+
return str(backup_path)
69+
except Exception:
70+
return None
71+
72+
73+
def main():
74+
try:
75+
# Parse command line arguments
76+
parser = argparse.ArgumentParser()
77+
parser.add_argument('--backup', action='store_true',
78+
help='Create backup of transcript before compaction')
79+
parser.add_argument('--verbose', action='store_true',
80+
help='Print verbose output')
81+
args = parser.parse_args()
82+
83+
# Read JSON input from stdin
84+
input_data = json.loads(sys.stdin.read())
85+
86+
# Extract fields
87+
session_id = input_data.get('session_id', 'unknown')
88+
transcript_path = input_data.get('transcript_path', '')
89+
trigger = input_data.get('trigger', 'unknown') # "manual" or "auto"
90+
custom_instructions = input_data.get('custom_instructions', '')
91+
92+
# Log the pre-compact event
93+
log_pre_compact(input_data)
94+
95+
# Create backup if requested
96+
backup_path = None
97+
if args.backup and transcript_path:
98+
backup_path = backup_transcript(transcript_path, trigger)
99+
100+
# Provide feedback based on trigger type
101+
if args.verbose:
102+
if trigger == "manual":
103+
message = f"Preparing for manual compaction (session: {session_id[:8]}...)"
104+
if custom_instructions:
105+
message += f"\nCustom instructions: {custom_instructions[:100]}..."
106+
else: # auto
107+
message = f"Auto-compaction triggered due to full context window (session: {session_id[:8]}...)"
108+
109+
if backup_path:
110+
message += f"\nTranscript backed up to: {backup_path}"
111+
112+
print(message)
113+
114+
# Success - compaction will proceed
115+
sys.exit(0)
116+
117+
except json.JSONDecodeError:
118+
# Handle JSON decode errors gracefully
119+
sys.exit(0)
120+
except Exception:
121+
# Handle any other errors gracefully
122+
sys.exit(0)
123+
124+
125+
if __name__ == '__main__':
126+
main()

.claude/hooks/session_start.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.11"
4+
# dependencies = [
5+
# "python-dotenv",
6+
# ]
7+
# ///
8+
9+
import argparse
10+
import json
11+
import os
12+
import sys
13+
import subprocess
14+
from pathlib import Path
15+
from datetime import datetime
16+
17+
try:
18+
from dotenv import load_dotenv
19+
load_dotenv()
20+
except ImportError:
21+
pass # dotenv is optional
22+
23+
24+
def log_session_start(input_data):
25+
"""Log session start event to logs directory."""
26+
# Ensure logs directory exists
27+
log_dir = Path("logs")
28+
log_dir.mkdir(parents=True, exist_ok=True)
29+
log_file = log_dir / 'session_start.json'
30+
31+
# Read existing log data or initialize empty list
32+
if log_file.exists():
33+
with open(log_file, 'r') as f:
34+
try:
35+
log_data = json.load(f)
36+
except (json.JSONDecodeError, ValueError):
37+
log_data = []
38+
else:
39+
log_data = []
40+
41+
# Append the entire input data
42+
log_data.append(input_data)
43+
44+
# Write back to file with formatting
45+
with open(log_file, 'w') as f:
46+
json.dump(log_data, f, indent=2)
47+
48+
49+
def get_git_status():
50+
"""Get current git status information."""
51+
try:
52+
# Get current branch
53+
branch_result = subprocess.run(
54+
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
55+
capture_output=True,
56+
text=True,
57+
timeout=5
58+
)
59+
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown"
60+
61+
# Get uncommitted changes count
62+
status_result = subprocess.run(
63+
['git', 'status', '--porcelain'],
64+
capture_output=True,
65+
text=True,
66+
timeout=5
67+
)
68+
if status_result.returncode == 0:
69+
changes = status_result.stdout.strip().split('\n') if status_result.stdout.strip() else []
70+
uncommitted_count = len(changes)
71+
else:
72+
uncommitted_count = 0
73+
74+
return current_branch, uncommitted_count
75+
except Exception:
76+
return None, None
77+
78+
79+
def get_recent_issues():
80+
"""Get recent GitHub issues if gh CLI is available."""
81+
try:
82+
# Check if gh is available
83+
gh_check = subprocess.run(['which', 'gh'], capture_output=True)
84+
if gh_check.returncode != 0:
85+
return None
86+
87+
# Get recent open issues
88+
result = subprocess.run(
89+
['gh', 'issue', 'list', '--limit', '5', '--state', 'open'],
90+
capture_output=True,
91+
text=True,
92+
timeout=10
93+
)
94+
if result.returncode == 0 and result.stdout.strip():
95+
return result.stdout.strip()
96+
except Exception:
97+
pass
98+
return None
99+
100+
101+
def load_development_context(source):
102+
"""Load relevant development context based on session source."""
103+
context_parts = []
104+
105+
# Add timestamp
106+
context_parts.append(f"Session started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
107+
context_parts.append(f"Session source: {source}")
108+
109+
# Add git information
110+
branch, changes = get_git_status()
111+
if branch:
112+
context_parts.append(f"Git branch: {branch}")
113+
if changes > 0:
114+
context_parts.append(f"Uncommitted changes: {changes} files")
115+
116+
# Load project-specific context files if they exist
117+
context_files = [
118+
".claude/CONTEXT.md",
119+
".claude/TODO.md",
120+
"TODO.md",
121+
".github/ISSUE_TEMPLATE.md"
122+
]
123+
124+
for file_path in context_files:
125+
if Path(file_path).exists():
126+
try:
127+
with open(file_path, 'r') as f:
128+
content = f.read().strip()
129+
if content:
130+
context_parts.append(f"\n--- Content from {file_path} ---")
131+
context_parts.append(content[:1000]) # Limit to first 1000 chars
132+
except Exception:
133+
pass
134+
135+
# Add recent issues if available
136+
issues = get_recent_issues()
137+
if issues:
138+
context_parts.append("\n--- Recent GitHub Issues ---")
139+
context_parts.append(issues)
140+
141+
return "\n".join(context_parts)
142+
143+
144+
def main():
145+
try:
146+
# Parse command line arguments
147+
parser = argparse.ArgumentParser()
148+
parser.add_argument('--load-context', action='store_true',
149+
help='Load development context at session start')
150+
parser.add_argument('--announce', action='store_true',
151+
help='Announce session start via TTS')
152+
args = parser.parse_args()
153+
154+
# Read JSON input from stdin
155+
input_data = json.loads(sys.stdin.read())
156+
157+
# Extract fields
158+
session_id = input_data.get('session_id', 'unknown')
159+
source = input_data.get('source', 'unknown') # "startup", "resume", or "clear"
160+
161+
# Log the session start event
162+
log_session_start(input_data)
163+
164+
# Load development context if requested
165+
if args.load_context:
166+
context = load_development_context(source)
167+
if context:
168+
# Using JSON output to add context
169+
output = {
170+
"hookSpecificOutput": {
171+
"hookEventName": "SessionStart",
172+
"additionalContext": context
173+
}
174+
}
175+
print(json.dumps(output))
176+
sys.exit(0)
177+
178+
# Announce session start if requested
179+
if args.announce:
180+
try:
181+
# Try to use TTS to announce session start
182+
script_dir = Path(__file__).parent
183+
tts_script = script_dir / "utils" / "tts" / "pyttsx3_tts.py"
184+
185+
if tts_script.exists():
186+
messages = {
187+
"startup": "Claude Code session started",
188+
"resume": "Resuming previous session",
189+
"clear": "Starting fresh session"
190+
}
191+
message = messages.get(source, "Session started")
192+
193+
subprocess.run(
194+
["uv", "run", str(tts_script), message],
195+
capture_output=True,
196+
timeout=5
197+
)
198+
except Exception:
199+
pass
200+
201+
# Success
202+
sys.exit(0)
203+
204+
except json.JSONDecodeError:
205+
# Handle JSON decode errors gracefully
206+
sys.exit(0)
207+
except Exception:
208+
# Handle any other errors gracefully
209+
sys.exit(0)
210+
211+
212+
if __name__ == '__main__':
213+
main()

.claude/settings.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,28 @@
8181
}
8282
]
8383
}
84+
],
85+
"PreCompact": [
86+
{
87+
"matcher": "",
88+
"hooks": [
89+
{
90+
"type": "command",
91+
"command": "uv run .claude/hooks/pre_compact.py"
92+
}
93+
]
94+
}
95+
],
96+
"SessionStart": [
97+
{
98+
"matcher": "",
99+
"hooks": [
100+
{
101+
"type": "command",
102+
"command": "uv run .claude/hooks/session_start.py"
103+
}
104+
]
105+
}
84106
]
85107
}
86108
}

0 commit comments

Comments
 (0)