clearctx 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Continuity Hooks — Handler functions for Claude Code lifecycle hooks.
6
+ *
7
+ * This module exports 4 handler functions, one for each hook event:
8
+ * - handleSessionStart — Injects a diff-aware briefing at session start
9
+ * - handleStop — Lightweight background checkpoint after each response
10
+ * - handlePreCompact — Critical save before context compaction
11
+ * - handleSessionEnd — Final snapshot on exit (Ctrl+C, /exit, error)
12
+ *
13
+ * Each handler receives the parsed hook input JSON from Claude Code.
14
+ * Handlers for SessionStart and PreCompact can return { additionalContext }
15
+ * which gets injected into Claude's conversation.
16
+ *
17
+ * Part of the Session Continuity Engine (Layer 0).
18
+ */
19
+
20
+ const SessionSnapshot = require('./session-snapshot');
21
+ const BriefingGenerator = require('./briefing-generator');
22
+ const DecisionJournal = require('./decision-journal');
23
+ const PatternRegistry = require('./pattern-registry');
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ /**
28
+ * handleSessionStart — Fires when a Claude Code session begins.
29
+ *
30
+ * This is the most important handler. It checks if continuity data exists
31
+ * for the project, and if so, generates a diff-aware briefing that gets
32
+ * injected into the conversation automatically.
33
+ *
34
+ * @param {Object} input - The parsed hook JSON from Claude Code
35
+ * @param {string} input.working_directory - Absolute path to the project root
36
+ * @param {string} input.session_id - Unique session identifier
37
+ * @returns {Object} { additionalContext: string } or {} if no briefing needed
38
+ */
39
+ function handleSessionStart(input) {
40
+ // Extract the project root directory from the hook input
41
+ const workDir = input.working_directory;
42
+ if (!workDir) return {};
43
+
44
+ // Check if we have any previous snapshot data for this project
45
+ const snap = new SessionSnapshot(workDir);
46
+ const latest = snap.getLatest();
47
+
48
+ // If no previous snapshot exists, this is a new project — nothing to brief
49
+ if (!latest) return {};
50
+
51
+ // Generate a diff-aware briefing comparing last snapshot to current state
52
+ // Budget: 500 tokens (characters) to keep session start fast
53
+ const gen = new BriefingGenerator(workDir);
54
+ const result = gen.generate({
55
+ maxTokens: 500,
56
+ includeDecisions: true,
57
+ includePatterns: true
58
+ });
59
+
60
+ // If we got a briefing, inject it into the conversation
61
+ if (result && result.markdown) {
62
+ return { additionalContext: result.markdown };
63
+ }
64
+
65
+ return {};
66
+ }
67
+
68
+ /**
69
+ * handleStop — Fires after every Claude response (async, runs in background).
70
+ *
71
+ * This is a lightweight handler. It only captures a new snapshot if the
72
+ * last one is more than 30 minutes old, to avoid excessive I/O.
73
+ *
74
+ * Since this hook is async, it cannot return additionalContext.
75
+ *
76
+ * @param {Object} input - The parsed hook JSON from Claude Code
77
+ * @param {string} input.working_directory - Absolute path to the project root
78
+ * @param {string} input.session_id - Unique session identifier
79
+ * @returns {Object} Always returns {} (async hook, no context injection)
80
+ */
81
+ function handleStop(input) {
82
+ const workDir = input.working_directory;
83
+ const sessionId = input.session_id || 'auto-stop';
84
+ if (!workDir) return {};
85
+
86
+ // Check if continuity data exists — don't auto-create on Stop events
87
+ const snap = new SessionSnapshot(workDir);
88
+ const latest = snap.getLatest();
89
+ if (!latest) return {};
90
+
91
+ // Only snapshot if the last one is more than 30 minutes old
92
+ // This keeps the Stop hook fast and non-disruptive
93
+ const THIRTY_MINUTES = 30 * 60 * 1000; // 30 minutes in milliseconds
94
+ const lastCapturedAt = new Date(latest.capturedAt).getTime();
95
+ const timeSinceLast = Date.now() - lastCapturedAt;
96
+
97
+ if (timeSinceLast > THIRTY_MINUTES) {
98
+ // Capture a safety-net snapshot
99
+ snap.capture(sessionId, {
100
+ taskSummary: 'Auto-checkpoint (30min interval)',
101
+ activeFiles: []
102
+ });
103
+ }
104
+
105
+ return {};
106
+ }
107
+
108
+ /**
109
+ * handlePreCompact — Fires right before context compaction.
110
+ *
111
+ * This is the CRITICAL save moment. Context is about to be destroyed by
112
+ * compaction, so we capture a full snapshot and generate a compact briefing
113
+ * that gets re-injected after compaction. This preserves essential context
114
+ * that would otherwise be lost.
115
+ *
116
+ * @param {Object} input - The parsed hook JSON from Claude Code
117
+ * @param {string} input.working_directory - Absolute path to the project root
118
+ * @param {string} input.session_id - Unique session identifier
119
+ * @returns {Object} { additionalContext: string } with preserved context
120
+ */
121
+ function handlePreCompact(input) {
122
+ const workDir = input.working_directory;
123
+ const sessionId = input.session_id || 'pre-compact';
124
+ if (!workDir) return {};
125
+
126
+ // Step 1: Generate a compact briefing BEFORE capturing a new snapshot.
127
+ // This way the diff compares the PREVIOUS snapshot to current state,
128
+ // giving a meaningful summary of what changed during this session.
129
+ // (If we captured first, the diff would always be empty.)
130
+ const gen = new BriefingGenerator(workDir);
131
+ const briefing = gen.generate({
132
+ maxTokens: 300,
133
+ includeDecisions: true,
134
+ includePatterns: true
135
+ });
136
+
137
+ // Step 2: NOW capture a full snapshot so future sessions can diff against it
138
+ const snap = new SessionSnapshot(workDir);
139
+ snap.capture(sessionId, {
140
+ taskSummary: 'Auto-saved before context compaction',
141
+ activeFiles: []
142
+ });
143
+
144
+ // Step 3: Get recent decisions to reinforce after compaction
145
+ const journal = new DecisionJournal(workDir);
146
+ const recentDecisions = journal.list({ limit: 5 });
147
+
148
+ // Step 4: Get patterns to reinforce after compaction
149
+ const registry = new PatternRegistry(workDir);
150
+ const patterns = registry.list({});
151
+
152
+ // Step 5: Build the additionalContext string
153
+ let context = '=== Context Preserved by Session Continuity Engine ===\n';
154
+ context += 'Your session context was just compacted. Here\'s what you need to remember:\n\n';
155
+
156
+ // Add the briefing markdown
157
+ if (briefing && briefing.markdown) {
158
+ context += briefing.markdown + '\n\n';
159
+ }
160
+
161
+ // Add recent decisions
162
+ if (recentDecisions.length > 0) {
163
+ context += 'Key decisions this session:\n';
164
+ for (const d of recentDecisions) {
165
+ context += `- ${d.decision}`;
166
+ if (d.reason) context += ` (${d.reason})`;
167
+ context += '\n';
168
+ }
169
+ context += '\n';
170
+ }
171
+
172
+ // Add patterns
173
+ if (patterns.length > 0) {
174
+ context += 'Code patterns for this project:\n';
175
+ for (const p of patterns) {
176
+ context += `- ${p.rule}`;
177
+ if (p.context) context += ` [${p.context}]`;
178
+ context += '\n';
179
+ }
180
+ context += '\n';
181
+ }
182
+
183
+ context += '=== End Preserved Context ===';
184
+
185
+ return { additionalContext: context };
186
+ }
187
+
188
+ /**
189
+ * handleSessionEnd — Fires on Ctrl+C, /exit, error, or terminal close.
190
+ *
191
+ * Captures a final snapshot and prunes old snapshots (keeps last 20).
192
+ * This hook is async — it runs in the background and the session may
193
+ * terminate before it completes. The snapshot write is the priority;
194
+ * pruning is best-effort.
195
+ *
196
+ * @param {Object} input - The parsed hook JSON from Claude Code
197
+ * @param {string} input.working_directory - Absolute path to the project root
198
+ * @param {string} input.session_id - Unique session identifier
199
+ * @returns {Object} Always returns {} (async hook, no context injection)
200
+ */
201
+ function handleSessionEnd(input) {
202
+ const workDir = input.working_directory;
203
+ const sessionId = input.session_id || 'session-end';
204
+ if (!workDir) return {};
205
+
206
+ // Step 1: Capture a full snapshot on exit
207
+ const snap = new SessionSnapshot(workDir);
208
+ snap.capture(sessionId, {
209
+ taskSummary: 'Auto-saved on session exit',
210
+ activeFiles: []
211
+ });
212
+
213
+ // Step 2: Prune old snapshots — keep the 20 most recent
214
+ // This is best-effort; if the process terminates before we finish, that's OK
215
+ try {
216
+ const MAX_SNAPSHOTS = 20;
217
+ const snapshotsDir = path.join(snap.getProjectDir(), 'snapshots');
218
+
219
+ if (fs.existsSync(snapshotsDir)) {
220
+ // Read all snapshot files
221
+ const files = fs.readdirSync(snapshotsDir)
222
+ .filter(f => f.startsWith('snap_') && f.endsWith('.json'));
223
+
224
+ // If we have more than MAX_SNAPSHOTS, delete the oldest ones
225
+ if (files.length > MAX_SNAPSHOTS) {
226
+ // Sort by filename (contains timestamp) ascending — oldest first
227
+ files.sort();
228
+
229
+ // Delete oldest files, keeping the most recent MAX_SNAPSHOTS
230
+ const toDelete = files.slice(0, files.length - MAX_SNAPSHOTS);
231
+ for (const file of toDelete) {
232
+ try {
233
+ fs.unlinkSync(path.join(snapshotsDir, file));
234
+ } catch (e) {
235
+ // Ignore delete errors — best-effort pruning
236
+ }
237
+ }
238
+ }
239
+ }
240
+ } catch (e) {
241
+ // Pruning failed — that's OK, the snapshot was already saved
242
+ }
243
+
244
+ return {};
245
+ }
246
+
247
+ // Export all handler functions
248
+ module.exports = {
249
+ handleSessionStart,
250
+ handleStop,
251
+ handlePreCompact,
252
+ handleSessionEnd
253
+ };