claude-mneme 2.9.1
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/.claude-plugin/plugin.json +17 -0
- package/CLAUDE.md +98 -0
- package/CONFIG_REFERENCE.md +495 -0
- package/README.md +40 -0
- package/commands/entity.md +64 -0
- package/commands/forget.md +69 -0
- package/commands/remember.md +60 -0
- package/commands/status.md +90 -0
- package/commands/summarize.md +69 -0
- package/hooks/hooks.json +123 -0
- package/package.json +12 -0
- package/scripts/mem-add.mjs +59 -0
- package/scripts/mem-entity.mjs +143 -0
- package/scripts/mem-forget.mjs +245 -0
- package/scripts/mem-status.mjs +319 -0
- package/scripts/mem-summarize.mjs +338 -0
- package/scripts/post-compact.mjs +132 -0
- package/scripts/post-tool-use.mjs +353 -0
- package/scripts/pre-compact.mjs +491 -0
- package/scripts/session-start.mjs +283 -0
- package/scripts/session-stop.mjs +31 -0
- package/scripts/stop-capture.mjs +294 -0
- package/scripts/subagent-stop.mjs +203 -0
- package/scripts/summarize.mjs +428 -0
- package/scripts/sync.mjs +609 -0
- package/scripts/user-prompt-submit.mjs +77 -0
- package/scripts/utils.mjs +2142 -0
- package/scripts/utils.test.mjs +1465 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreCompact Hook - Context Preservation
|
|
4
|
+
*
|
|
5
|
+
* Fires before Claude Code compacts the conversation. This is an opportunity to:
|
|
6
|
+
* 1. Flush pending log entries
|
|
7
|
+
* 2. Extract important context from the transcript before it's compressed
|
|
8
|
+
* 3. Force immediate summarization
|
|
9
|
+
* 4. Optionally save a transcript snapshot
|
|
10
|
+
*
|
|
11
|
+
* All behavior is configurable via ~/.claude-mneme/config.json under "preCompact"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
15
|
+
import { join, dirname } from 'path';
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { ensureDeps, ensureMemoryDirs, loadConfig, getProjectName, flushPendingLog, appendLogEntry, logError } from './utils.mjs';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
|
|
23
|
+
// Read hook input from stdin
|
|
24
|
+
let input = '';
|
|
25
|
+
process.stdin.setEncoding('utf8');
|
|
26
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
27
|
+
process.stdin.on('end', async () => {
|
|
28
|
+
try {
|
|
29
|
+
const hookData = JSON.parse(input);
|
|
30
|
+
await processPreCompact(hookData);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(`[claude-mneme] PreCompact error: ${e.message}`);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read and parse transcript from file path
|
|
39
|
+
*/
|
|
40
|
+
function readTranscript(transcriptPath) {
|
|
41
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
|
47
|
+
if (!content) return [];
|
|
48
|
+
|
|
49
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
50
|
+
const transcript = [];
|
|
51
|
+
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
try {
|
|
54
|
+
const entry = JSON.parse(line);
|
|
55
|
+
transcript.push(entry);
|
|
56
|
+
} catch {
|
|
57
|
+
// Skip malformed lines
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return transcript;
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract text content from a message
|
|
69
|
+
*/
|
|
70
|
+
function extractText(content) {
|
|
71
|
+
if (typeof content === 'string') return content;
|
|
72
|
+
if (Array.isArray(content)) {
|
|
73
|
+
return content
|
|
74
|
+
.filter(block => block.type === 'text')
|
|
75
|
+
.map(block => block.text)
|
|
76
|
+
.join('\n');
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract decisions/choices from transcript
|
|
83
|
+
*/
|
|
84
|
+
function extractDecisions(transcript, maxItems) {
|
|
85
|
+
const decisions = [];
|
|
86
|
+
const decisionPatterns = [
|
|
87
|
+
/(?:decided|choosing|going with|selected|picked|using|will use|opted for)\s+(.{10,100})/gi,
|
|
88
|
+
/(?:the approach|the solution|the plan) (?:is|will be)\s+(.{10,100})/gi,
|
|
89
|
+
/(?:let's|we'll|I'll)\s+(?:go with|use|implement)\s+(.{10,100})/gi,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const entry of transcript) {
|
|
93
|
+
if (entry.type !== 'assistant') continue;
|
|
94
|
+
const text = extractText(entry.message?.content);
|
|
95
|
+
if (!text) continue;
|
|
96
|
+
|
|
97
|
+
for (const pattern of decisionPatterns) {
|
|
98
|
+
pattern.lastIndex = 0;
|
|
99
|
+
let match;
|
|
100
|
+
while ((match = pattern.exec(text)) !== null && decisions.length < maxItems) {
|
|
101
|
+
const decision = match[1].trim().replace(/[.,:;]$/, '');
|
|
102
|
+
if (decision.length > 10 && !decisions.includes(decision)) {
|
|
103
|
+
decisions.push(decision);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return decisions.slice(0, maxItems);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract file paths mentioned in transcript
|
|
114
|
+
*/
|
|
115
|
+
function extractFiles(transcript, maxItems) {
|
|
116
|
+
const files = new Set();
|
|
117
|
+
const filePatterns = [
|
|
118
|
+
/(?:^|[\s"'`])([a-zA-Z0-9_\-./]+\.[a-zA-Z]{1,10})(?:[\s"'`:]|$)/g,
|
|
119
|
+
/(?:file|path|in|from|to|edit|read|write|create)\s+[`"']?([a-zA-Z0-9_\-./]+\.[a-zA-Z]{1,10})[`"']?/gi,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const skipExtensions = ['com', 'org', 'net', 'io', 'dev', 'app'];
|
|
123
|
+
|
|
124
|
+
for (const entry of transcript) {
|
|
125
|
+
const text = extractText(entry.message?.content || entry.content);
|
|
126
|
+
if (!text) continue;
|
|
127
|
+
|
|
128
|
+
for (const pattern of filePatterns) {
|
|
129
|
+
pattern.lastIndex = 0;
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
132
|
+
const file = match[1];
|
|
133
|
+
const ext = file.split('.').pop()?.toLowerCase();
|
|
134
|
+
if (ext && !skipExtensions.includes(ext) && file.length < 100) {
|
|
135
|
+
files.add(file);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (files.size >= maxItems * 2) break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return Array.from(files).slice(0, maxItems);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract errors encountered in transcript
|
|
148
|
+
*/
|
|
149
|
+
function extractErrors(transcript, maxItems) {
|
|
150
|
+
const errors = [];
|
|
151
|
+
const errorPatterns = [
|
|
152
|
+
/(?:error|exception|failed|failure):\s*(.{10,150})/gi,
|
|
153
|
+
/(?:cannot|can't|couldn't|unable to)\s+(.{10,100})/gi,
|
|
154
|
+
/(?:TypeError|ReferenceError|SyntaxError|Error):\s*(.{10,150})/gi,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const entry of transcript) {
|
|
158
|
+
const text = extractText(entry.message?.content || entry.content);
|
|
159
|
+
if (!text) continue;
|
|
160
|
+
|
|
161
|
+
for (const pattern of errorPatterns) {
|
|
162
|
+
pattern.lastIndex = 0;
|
|
163
|
+
let match;
|
|
164
|
+
while ((match = pattern.exec(text)) !== null && errors.length < maxItems) {
|
|
165
|
+
const error = match[1].trim().replace(/[.,:;]$/, '');
|
|
166
|
+
if (error.length > 10 && !errors.some(e => e.includes(error) || error.includes(e))) {
|
|
167
|
+
errors.push(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return errors.slice(0, maxItems);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract TODOs and action items
|
|
178
|
+
*/
|
|
179
|
+
function extractTodos(transcript, maxItems) {
|
|
180
|
+
const todos = [];
|
|
181
|
+
const todoPatterns = [
|
|
182
|
+
/(?:TODO|FIXME|HACK|XXX):\s*(.{10,100})/gi,
|
|
183
|
+
/(?:we |you |I )?(?:need to|should|must|have to)\s+((?:add|fix|update|create|remove|implement|refactor|change|move|rename|delete|migrate|test|check|verify|ensure|resolve|handle|configure|set up|clean up)\b.{5,100})/gi,
|
|
184
|
+
/(?:next step|action item|follow.?up):\s*(.{10,100})/gi,
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const entry of transcript) {
|
|
188
|
+
if (entry.type !== 'assistant') continue;
|
|
189
|
+
const text = extractText(entry.message?.content);
|
|
190
|
+
if (!text) continue;
|
|
191
|
+
|
|
192
|
+
for (const pattern of todoPatterns) {
|
|
193
|
+
pattern.lastIndex = 0;
|
|
194
|
+
let match;
|
|
195
|
+
while ((match = pattern.exec(text)) !== null && todos.length < maxItems) {
|
|
196
|
+
const todo = match[1].trim().replace(/[.,:;]$/, '');
|
|
197
|
+
if (todo.length > 10 && !todos.includes(todo)) {
|
|
198
|
+
todos.push(todo);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return todos.slice(0, maxItems);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract key discussion points using AI
|
|
209
|
+
*/
|
|
210
|
+
async function extractKeyPoints(transcript, config, maxItems) {
|
|
211
|
+
// Get the last N messages for context
|
|
212
|
+
const recentMessages = transcript.slice(-20);
|
|
213
|
+
const conversationText = recentMessages
|
|
214
|
+
.filter(e => e.type === 'user' || e.type === 'assistant')
|
|
215
|
+
.map(e => {
|
|
216
|
+
const role = e.type === 'user' ? 'User' : 'Assistant';
|
|
217
|
+
const text = extractText(e.message?.content || e.content);
|
|
218
|
+
return `${role}: ${text.substring(0, 500)}`;
|
|
219
|
+
})
|
|
220
|
+
.join('\n\n');
|
|
221
|
+
|
|
222
|
+
if (!conversationText || conversationText.length < 100) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const prompt = `Extract the ${maxItems} most important key points from this conversation that should be remembered. Focus on:
|
|
227
|
+
- Decisions made
|
|
228
|
+
- Problems solved
|
|
229
|
+
- Important discoveries
|
|
230
|
+
- User preferences expressed
|
|
231
|
+
|
|
232
|
+
<conversation>
|
|
233
|
+
${conversationText.substring(0, 4000)}
|
|
234
|
+
</conversation>
|
|
235
|
+
|
|
236
|
+
Output ONLY a JSON array of strings, each being a concise key point (max 100 chars each).
|
|
237
|
+
Example: ["Decided to use TypeScript for type safety", "Fixed auth bug by adding token refresh"]`;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
ensureDeps();
|
|
241
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
242
|
+
|
|
243
|
+
async function* messageGenerator() {
|
|
244
|
+
yield {
|
|
245
|
+
type: 'user',
|
|
246
|
+
message: { role: 'user', content: prompt },
|
|
247
|
+
session_id: `pre-compact-${Date.now()}`,
|
|
248
|
+
parent_tool_use_id: null,
|
|
249
|
+
isSynthetic: true
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const queryResult = query({
|
|
254
|
+
prompt: messageGenerator(),
|
|
255
|
+
options: {
|
|
256
|
+
model: config.model,
|
|
257
|
+
disallowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Task', 'TodoWrite'],
|
|
258
|
+
pathToClaudeCodeExecutable: config.claudePath
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
let response = '';
|
|
263
|
+
try {
|
|
264
|
+
for await (const message of queryResult) {
|
|
265
|
+
if (message.type === 'assistant') {
|
|
266
|
+
const content = message.message.content;
|
|
267
|
+
response = Array.isArray(content)
|
|
268
|
+
? content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
269
|
+
: typeof content === 'string' ? content : '';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (iterError) {
|
|
273
|
+
if (!response) throw iterError;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (response) {
|
|
277
|
+
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
278
|
+
if (jsonMatch) {
|
|
279
|
+
const points = JSON.parse(jsonMatch[0]);
|
|
280
|
+
return points.slice(0, maxItems);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error(`[claude-mneme] Key points extraction error: ${error.message}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Save transcript snapshot, rotating old snapshots to keep at most maxCount.
|
|
292
|
+
*/
|
|
293
|
+
function saveSnapshot(transcript, paths, trigger, maxCount = 10) {
|
|
294
|
+
const snapshotDir = join(paths.project, 'snapshots');
|
|
295
|
+
if (!existsSync(snapshotDir)) {
|
|
296
|
+
mkdirSync(snapshotDir, { recursive: true });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
300
|
+
const snapshotPath = join(snapshotDir, `pre-compact-${trigger}-${timestamp}.jsonl`);
|
|
301
|
+
|
|
302
|
+
const content = transcript.map(e => JSON.stringify(e)).join('\n');
|
|
303
|
+
writeFileSync(snapshotPath, content + '\n');
|
|
304
|
+
|
|
305
|
+
// Rotate: keep only the most recent maxCount snapshots
|
|
306
|
+
try {
|
|
307
|
+
const files = readdirSync(snapshotDir)
|
|
308
|
+
.filter(f => f.startsWith('pre-compact-') && f.endsWith('.jsonl'))
|
|
309
|
+
.sort(); // Lexicographic sort works because timestamps are ISO-formatted
|
|
310
|
+
if (files.length > maxCount) {
|
|
311
|
+
for (const old of files.slice(0, files.length - maxCount)) {
|
|
312
|
+
unlinkSync(join(snapshotDir, old));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (e) {
|
|
316
|
+
logError(e, 'pre-compact:snapshotRotation');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.error(`[claude-mneme] Saved transcript snapshot: ${snapshotPath}`);
|
|
320
|
+
return snapshotPath;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Force run summarization script
|
|
325
|
+
*/
|
|
326
|
+
function forceSummarize(cwd) {
|
|
327
|
+
const summarizeScript = join(__dirname, 'summarize.mjs');
|
|
328
|
+
|
|
329
|
+
return new Promise((resolve) => {
|
|
330
|
+
const child = spawn('node', [summarizeScript, cwd], {
|
|
331
|
+
stdio: 'inherit',
|
|
332
|
+
cwd
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
child.on('close', () => resolve());
|
|
336
|
+
child.on('error', () => resolve());
|
|
337
|
+
|
|
338
|
+
// Timeout after 60 seconds
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
try { child.kill(); } catch {}
|
|
341
|
+
resolve();
|
|
342
|
+
}, 60000);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Main processing function
|
|
348
|
+
*/
|
|
349
|
+
async function processPreCompact(hookData) {
|
|
350
|
+
const { trigger, transcript_path, cwd, custom_instructions } = hookData;
|
|
351
|
+
const workingDir = cwd || process.cwd();
|
|
352
|
+
const config = loadConfig();
|
|
353
|
+
const pcConfig = config.preCompact || {};
|
|
354
|
+
|
|
355
|
+
// Check if hook is enabled
|
|
356
|
+
if (pcConfig.enabled === false) {
|
|
357
|
+
process.exit(0);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check if we should respond to this trigger
|
|
362
|
+
const triggers = pcConfig.triggers || ['auto', 'manual'];
|
|
363
|
+
if (!triggers.includes(trigger)) {
|
|
364
|
+
process.exit(0);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const paths = ensureMemoryDirs(workingDir);
|
|
369
|
+
const projectName = getProjectName(workingDir);
|
|
370
|
+
|
|
371
|
+
console.error(`[claude-mneme] PreCompact triggered (${trigger}) for "${projectName}"`);
|
|
372
|
+
|
|
373
|
+
// 1. Flush pending log entries
|
|
374
|
+
if (pcConfig.flushPending !== false) {
|
|
375
|
+
flushPendingLog(workingDir, 0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 2. Read transcript
|
|
379
|
+
const transcript = readTranscript(transcript_path);
|
|
380
|
+
|
|
381
|
+
// 3. Save snapshot if enabled
|
|
382
|
+
if (pcConfig.saveSnapshot && transcript.length > 0) {
|
|
383
|
+
saveSnapshot(transcript, paths, trigger);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 4. Extract context if enabled
|
|
387
|
+
if (pcConfig.extractContext !== false && transcript.length > 0) {
|
|
388
|
+
const extraction = pcConfig.extraction || {};
|
|
389
|
+
const categories = extraction.categories || {};
|
|
390
|
+
const maxItems = extraction.maxItems || 10;
|
|
391
|
+
|
|
392
|
+
const extracted = {
|
|
393
|
+
ts: new Date().toISOString(),
|
|
394
|
+
trigger,
|
|
395
|
+
customInstructions: custom_instructions || null
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
let hasContent = false;
|
|
399
|
+
|
|
400
|
+
if (categories.decisions !== false) {
|
|
401
|
+
const decisions = extractDecisions(transcript, maxItems);
|
|
402
|
+
if (decisions.length > 0) {
|
|
403
|
+
extracted.decisions = decisions;
|
|
404
|
+
hasContent = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (categories.files !== false) {
|
|
409
|
+
const files = extractFiles(transcript, maxItems);
|
|
410
|
+
if (files.length > 0) {
|
|
411
|
+
extracted.files = files;
|
|
412
|
+
hasContent = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (categories.errors !== false) {
|
|
417
|
+
const errors = extractErrors(transcript, maxItems);
|
|
418
|
+
if (errors.length > 0) {
|
|
419
|
+
extracted.errors = errors;
|
|
420
|
+
hasContent = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (categories.todos !== false) {
|
|
425
|
+
const todos = extractTodos(transcript, maxItems);
|
|
426
|
+
if (todos.length > 0) {
|
|
427
|
+
extracted.todos = todos;
|
|
428
|
+
hasContent = true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (categories.keyPoints !== false) {
|
|
433
|
+
const keyPoints = await extractKeyPoints(transcript, config, maxItems);
|
|
434
|
+
if (keyPoints.length > 0) {
|
|
435
|
+
extracted.keyPoints = keyPoints;
|
|
436
|
+
hasContent = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Save extracted context
|
|
441
|
+
if (hasContent) {
|
|
442
|
+
const extractedPath = join(paths.project, 'extracted-context.json');
|
|
443
|
+
|
|
444
|
+
// Append to existing extractions (keep last 5)
|
|
445
|
+
let extractions = [];
|
|
446
|
+
if (existsSync(extractedPath)) {
|
|
447
|
+
try {
|
|
448
|
+
extractions = JSON.parse(readFileSync(extractedPath, 'utf-8'));
|
|
449
|
+
} catch (e) {
|
|
450
|
+
logError(e, 'pre-compact:extracted-context.json');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
extractions.push(extracted);
|
|
455
|
+
if (extractions.length > 5) {
|
|
456
|
+
extractions = extractions.slice(-5);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
writeFileSync(extractedPath, JSON.stringify(extractions, null, 2) + '\n');
|
|
460
|
+
console.error(`[claude-mneme] Extracted context saved (${Object.keys(extracted).length - 3} categories)`);
|
|
461
|
+
|
|
462
|
+
// Log a summary entry
|
|
463
|
+
const summaryParts = [];
|
|
464
|
+
if (extracted.decisions?.length) summaryParts.push(`${extracted.decisions.length} decisions`);
|
|
465
|
+
if (extracted.files?.length) summaryParts.push(`${extracted.files.length} files`);
|
|
466
|
+
if (extracted.errors?.length) summaryParts.push(`${extracted.errors.length} errors`);
|
|
467
|
+
if (extracted.keyPoints?.length) summaryParts.push(`${extracted.keyPoints.length} key points`);
|
|
468
|
+
|
|
469
|
+
if (summaryParts.length > 0) {
|
|
470
|
+
appendLogEntry({
|
|
471
|
+
ts: new Date().toISOString(),
|
|
472
|
+
type: 'compact',
|
|
473
|
+
trigger,
|
|
474
|
+
content: `Pre-compact extraction: ${summaryParts.join(', ')}`
|
|
475
|
+
}, workingDir);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 5. Force summarization if enabled
|
|
481
|
+
if (pcConfig.forceSummarize !== false) {
|
|
482
|
+
console.error(`[claude-mneme] Forcing summarization before compact...`);
|
|
483
|
+
await forceSummarize(workingDir);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
console.error(`[claude-mneme] PreCompact processing complete`);
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Timeout fallback
|
|
491
|
+
setTimeout(() => process.exit(0), 120000);
|