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.
- package/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +1006 -0
- package/STRATEGY.md +485 -0
- package/bin/cli.js +1756 -0
- package/bin/continuity-hook.js +118 -0
- package/bin/mcp.js +27 -0
- package/bin/setup.js +929 -0
- package/package.json +56 -0
- package/src/artifact-store.js +710 -0
- package/src/atomic-io.js +99 -0
- package/src/briefing-generator.js +451 -0
- package/src/continuity-hooks.js +253 -0
- package/src/contract-store.js +525 -0
- package/src/decision-journal.js +229 -0
- package/src/delegate.js +348 -0
- package/src/dependency-resolver.js +453 -0
- package/src/diff-engine.js +473 -0
- package/src/file-lock.js +161 -0
- package/src/index.js +61 -0
- package/src/lineage-graph.js +402 -0
- package/src/manager.js +510 -0
- package/src/mcp-server.js +3501 -0
- package/src/pattern-registry.js +221 -0
- package/src/pipeline-engine.js +618 -0
- package/src/prompts.js +1217 -0
- package/src/safety-net.js +170 -0
- package/src/session-snapshot.js +508 -0
- package/src/snapshot-engine.js +490 -0
- package/src/stale-detector.js +169 -0
- package/src/store.js +131 -0
- package/src/stream-session.js +463 -0
- package/src/team-hub.js +615 -0
|
@@ -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
|
+
};
|