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,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Force Manual Summarization
|
|
4
|
+
*
|
|
5
|
+
* Triggers summarization regardless of entry count.
|
|
6
|
+
* Used by the /summarize slash command.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node mem-summarize.mjs [--dry-run]
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --dry-run Show what would be summarized without actually doing it
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
15
|
+
import {
|
|
16
|
+
ensureDeps,
|
|
17
|
+
ensureMemoryDirs,
|
|
18
|
+
loadConfig,
|
|
19
|
+
getProjectName,
|
|
20
|
+
formatEntriesForSummary,
|
|
21
|
+
emptyStructuredSummary,
|
|
22
|
+
deduplicateEntries,
|
|
23
|
+
flushPendingLog,
|
|
24
|
+
logError
|
|
25
|
+
} from './utils.mjs';
|
|
26
|
+
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
29
|
+
const paths = ensureMemoryDirs(cwd);
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
const projectName = getProjectName(cwd);
|
|
32
|
+
|
|
33
|
+
// Flush any pending entries first
|
|
34
|
+
flushPendingLog(cwd, 0);
|
|
35
|
+
|
|
36
|
+
// Check if log exists
|
|
37
|
+
if (!existsSync(paths.log)) {
|
|
38
|
+
console.log(JSON.stringify({
|
|
39
|
+
project: projectName,
|
|
40
|
+
status: 'empty',
|
|
41
|
+
message: 'No log entries to summarize.'
|
|
42
|
+
}));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const logContent = readFileSync(paths.log, 'utf-8').trim();
|
|
47
|
+
if (!logContent) {
|
|
48
|
+
console.log(JSON.stringify({
|
|
49
|
+
project: projectName,
|
|
50
|
+
status: 'empty',
|
|
51
|
+
message: 'No log entries to summarize.'
|
|
52
|
+
}));
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lines = logContent.split('\n').filter(l => l);
|
|
57
|
+
const entryCount = lines.length;
|
|
58
|
+
|
|
59
|
+
// Check for lock
|
|
60
|
+
const lockFile = paths.log + '.lock';
|
|
61
|
+
if (existsSync(lockFile)) {
|
|
62
|
+
const lockContent = readFileSync(lockFile, 'utf-8').trim();
|
|
63
|
+
const lockTime = parseInt(lockContent, 10);
|
|
64
|
+
if (lockTime && Date.now() - lockTime < 5 * 60 * 1000) {
|
|
65
|
+
console.log(JSON.stringify({
|
|
66
|
+
project: projectName,
|
|
67
|
+
status: 'locked',
|
|
68
|
+
message: 'Summarization already in progress.'
|
|
69
|
+
}));
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Dry run - just report what would happen
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
const keepCount = Math.min(config.keepRecentEntries, entryCount);
|
|
77
|
+
const summarizeCount = entryCount - keepCount;
|
|
78
|
+
|
|
79
|
+
// Read existing summary state
|
|
80
|
+
let hasSummary = false;
|
|
81
|
+
if (existsSync(paths.summaryJson)) {
|
|
82
|
+
try {
|
|
83
|
+
const summary = JSON.parse(readFileSync(paths.summaryJson, 'utf-8'));
|
|
84
|
+
hasSummary = !!summary.lastUpdated;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
logError(e, 'mem-summarize:summary.json');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(JSON.stringify({
|
|
91
|
+
project: projectName,
|
|
92
|
+
status: 'dry_run',
|
|
93
|
+
logEntries: entryCount,
|
|
94
|
+
wouldSummarize: summarizeCount,
|
|
95
|
+
wouldKeep: keepCount,
|
|
96
|
+
hasSummary,
|
|
97
|
+
summaryPath: paths.summaryJson
|
|
98
|
+
}, null, 2));
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Minimum entries to summarize (at least 3 to be meaningful)
|
|
103
|
+
const minEntriesToSummarize = 3;
|
|
104
|
+
if (entryCount < minEntriesToSummarize) {
|
|
105
|
+
console.log(JSON.stringify({
|
|
106
|
+
project: projectName,
|
|
107
|
+
status: 'skipped',
|
|
108
|
+
message: `Only ${entryCount} entries. Need at least ${minEntriesToSummarize} to summarize.`,
|
|
109
|
+
logEntries: entryCount
|
|
110
|
+
}));
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Acquire lock
|
|
115
|
+
writeFileSync(lockFile, Date.now().toString());
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Ensure SDK is installed, then import
|
|
119
|
+
ensureDeps();
|
|
120
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
121
|
+
|
|
122
|
+
// Read existing summary
|
|
123
|
+
let existingSummary = emptyStructuredSummary();
|
|
124
|
+
if (existsSync(paths.summaryJson)) {
|
|
125
|
+
try {
|
|
126
|
+
existingSummary = JSON.parse(readFileSync(paths.summaryJson, 'utf-8'));
|
|
127
|
+
} catch (e) {
|
|
128
|
+
logError(e, 'mem-summarize:existingSummary');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Calculate entries to summarize vs keep
|
|
133
|
+
const summarizeCount = Math.max(0, entryCount - config.keepRecentEntries);
|
|
134
|
+
if (summarizeCount === 0) {
|
|
135
|
+
console.log(JSON.stringify({
|
|
136
|
+
project: projectName,
|
|
137
|
+
status: 'skipped',
|
|
138
|
+
message: 'All entries are within the keep-recent window.',
|
|
139
|
+
logEntries: entryCount,
|
|
140
|
+
keepRecentEntries: config.keepRecentEntries
|
|
141
|
+
}));
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const entriesToSummarize = lines.slice(0, summarizeCount);
|
|
146
|
+
const entriesToKeep = lines.slice(summarizeCount);
|
|
147
|
+
|
|
148
|
+
// Parse and deduplicate entries
|
|
149
|
+
const parsedEntries = entriesToSummarize.map(line => {
|
|
150
|
+
try { return JSON.parse(line); }
|
|
151
|
+
catch { return null; }
|
|
152
|
+
}).filter(Boolean);
|
|
153
|
+
|
|
154
|
+
const deduped = deduplicateEntries(parsedEntries, config);
|
|
155
|
+
const dedupedLines = deduped.map(e => JSON.stringify(e));
|
|
156
|
+
const entriesText = formatEntriesForSummary(dedupedLines);
|
|
157
|
+
|
|
158
|
+
// Build context from existing summary
|
|
159
|
+
const existingContext = [];
|
|
160
|
+
if (existingSummary.projectContext) {
|
|
161
|
+
existingContext.push(`Project: ${existingSummary.projectContext}`);
|
|
162
|
+
}
|
|
163
|
+
if (existingSummary.keyDecisions?.length > 0) {
|
|
164
|
+
existingContext.push(`Key decisions: ${existingSummary.keyDecisions.map(d => d.decision).join('; ')}`);
|
|
165
|
+
}
|
|
166
|
+
if (existingSummary.currentState?.length > 0) {
|
|
167
|
+
existingContext.push(`Current state: ${existingSummary.currentState.map(s => `${s.topic}: ${s.status}`).join('; ')}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const prompt = `You are updating a structured memory for project "${projectName}".
|
|
171
|
+
|
|
172
|
+
<existing_context>
|
|
173
|
+
${existingContext.join('\n') || '(New project, no existing context)'}
|
|
174
|
+
Recent work items: ${existingSummary.recentWork?.length || 0}
|
|
175
|
+
</existing_context>
|
|
176
|
+
|
|
177
|
+
<new_entries>
|
|
178
|
+
${entriesText}
|
|
179
|
+
</new_entries>
|
|
180
|
+
|
|
181
|
+
Analyze the new entries and output a JSON object with updates:
|
|
182
|
+
|
|
183
|
+
{
|
|
184
|
+
"projectContext": "Updated project description if new info changes it, or null to keep existing",
|
|
185
|
+
"newKeyDecisions": [
|
|
186
|
+
{ "date": "YYYY-MM-DD", "decision": "Important architectural/design choice", "reason": "Why" }
|
|
187
|
+
],
|
|
188
|
+
"updateCurrentState": [
|
|
189
|
+
{ "topic": "Feature name", "status": "New or updated status" }
|
|
190
|
+
],
|
|
191
|
+
"newRecentWork": [
|
|
192
|
+
{ "date": "YYYY-MM-DD", "summary": "What was done" }
|
|
193
|
+
],
|
|
194
|
+
"promoteToCurrentState": [],
|
|
195
|
+
"removeFromRecentWork": []
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Rules:
|
|
199
|
+
- Only include fields that have updates (use empty arrays for no changes)
|
|
200
|
+
- Key decisions: major architectural choices, technology decisions, design patterns
|
|
201
|
+
- Current state: features implemented, work in progress, known issues
|
|
202
|
+
- Recent work: specific tasks completed in this batch of entries
|
|
203
|
+
- Merge similar entries, avoid duplicates
|
|
204
|
+
- Be concise — each item should be one clear sentence
|
|
205
|
+
- Output ONLY the JSON object`;
|
|
206
|
+
|
|
207
|
+
console.error(`[claude-mneme] Summarizing ${entriesToSummarize.length} entries for "${projectName}"...`);
|
|
208
|
+
|
|
209
|
+
async function* messageGenerator() {
|
|
210
|
+
yield {
|
|
211
|
+
type: 'user',
|
|
212
|
+
message: { role: 'user', content: prompt },
|
|
213
|
+
session_id: `memory-summarize-manual-${Date.now()}`,
|
|
214
|
+
parent_tool_use_id: null,
|
|
215
|
+
isSynthetic: true
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const queryResult = query({
|
|
220
|
+
prompt: messageGenerator(),
|
|
221
|
+
options: {
|
|
222
|
+
model: config.model,
|
|
223
|
+
disallowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Task', 'TodoWrite'],
|
|
224
|
+
pathToClaudeCodeExecutable: config.claudePath
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
let response = '';
|
|
229
|
+
try {
|
|
230
|
+
for await (const message of queryResult) {
|
|
231
|
+
if (message.type === 'assistant') {
|
|
232
|
+
const content = message.message.content;
|
|
233
|
+
response = Array.isArray(content)
|
|
234
|
+
? content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
235
|
+
: typeof content === 'string' ? content : '';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (iterError) {
|
|
239
|
+
if (!response) throw iterError;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!response) {
|
|
243
|
+
throw new Error('No response from summarization model');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
247
|
+
if (!jsonMatch) {
|
|
248
|
+
throw new Error('Could not parse JSON from response');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const updates = JSON.parse(jsonMatch[0]);
|
|
252
|
+
|
|
253
|
+
// Apply updates to summary
|
|
254
|
+
const result = { ...existingSummary };
|
|
255
|
+
|
|
256
|
+
if (updates.projectContext) {
|
|
257
|
+
result.projectContext = updates.projectContext;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (updates.newKeyDecisions?.length > 0) {
|
|
261
|
+
result.keyDecisions = [...(result.keyDecisions || []), ...updates.newKeyDecisions];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (updates.updateCurrentState?.length > 0) {
|
|
265
|
+
const stateMap = new Map((result.currentState || []).map(s => [s.topic, s]));
|
|
266
|
+
for (const update of updates.updateCurrentState) {
|
|
267
|
+
stateMap.set(update.topic, update);
|
|
268
|
+
}
|
|
269
|
+
result.currentState = Array.from(stateMap.values());
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let recentWork = [...(result.recentWork || [])];
|
|
273
|
+
|
|
274
|
+
// Remove stale items (process in reverse to preserve indices)
|
|
275
|
+
if (updates.removeFromRecentWork?.length > 0) {
|
|
276
|
+
const toRemove = new Set(updates.removeFromRecentWork.map(Number));
|
|
277
|
+
recentWork = recentWork.filter((_, i) => !toRemove.has(i));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Promote items to current state
|
|
281
|
+
if (updates.promoteToCurrentState?.length > 0) {
|
|
282
|
+
const toPromote = new Set(updates.promoteToCurrentState.map(Number));
|
|
283
|
+
for (const idx of toPromote) {
|
|
284
|
+
if (result.recentWork?.[idx]) {
|
|
285
|
+
const item = result.recentWork[idx];
|
|
286
|
+
result.currentState = result.currentState || [];
|
|
287
|
+
result.currentState.push({
|
|
288
|
+
topic: 'Completed',
|
|
289
|
+
status: item.summary
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
recentWork = recentWork.filter((_, i) => !toPromote.has(i));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (updates.newRecentWork?.length > 0) {
|
|
297
|
+
recentWork = [...recentWork, ...updates.newRecentWork];
|
|
298
|
+
}
|
|
299
|
+
result.recentWork = recentWork.slice(-10);
|
|
300
|
+
|
|
301
|
+
if (result.currentState?.length > 15) {
|
|
302
|
+
result.currentState = result.currentState.slice(-15);
|
|
303
|
+
}
|
|
304
|
+
if (result.keyDecisions?.length > 10) {
|
|
305
|
+
result.keyDecisions = result.keyDecisions.slice(-10);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
result.lastUpdated = new Date().toISOString();
|
|
309
|
+
|
|
310
|
+
// Write updated summary
|
|
311
|
+
writeFileSync(paths.summaryJson, JSON.stringify(result, null, 2) + '\n');
|
|
312
|
+
|
|
313
|
+
// Re-read the log to preserve any entries appended during summarization
|
|
314
|
+
const currentLogContent = readFileSync(paths.log, 'utf-8').trim();
|
|
315
|
+
const currentLines = currentLogContent ? currentLogContent.split('\n').filter(l => l) : [];
|
|
316
|
+
const remainingLines = currentLines.slice(summarizeCount);
|
|
317
|
+
writeFileSync(paths.log, remainingLines.join('\n') + (remainingLines.length ? '\n' : ''));
|
|
318
|
+
|
|
319
|
+
console.log(JSON.stringify({
|
|
320
|
+
project: projectName,
|
|
321
|
+
status: 'success',
|
|
322
|
+
summarized: entriesToSummarize.length,
|
|
323
|
+
kept: remainingLines.length,
|
|
324
|
+
summaryUpdated: result.lastUpdated
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
} catch (error) {
|
|
328
|
+
logError(error, 'summarize');
|
|
329
|
+
console.log(JSON.stringify({
|
|
330
|
+
project: projectName,
|
|
331
|
+
status: 'error',
|
|
332
|
+
message: error.message
|
|
333
|
+
}));
|
|
334
|
+
process.exit(1);
|
|
335
|
+
|
|
336
|
+
} finally {
|
|
337
|
+
try { unlinkSync(lockFile); } catch {}
|
|
338
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Post-Compact Hook (via SessionStart with "compact" matcher)
|
|
4
|
+
*
|
|
5
|
+
* Fires after Claude Code compacts the conversation.
|
|
6
|
+
* Injects extracted context back into the conversation to restore important information.
|
|
7
|
+
*
|
|
8
|
+
* Configurable via ~/.claude-mneme/config.json under "postCompact"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { ensureMemoryDirs, loadConfig, getProjectName, escapeAttr } from './utils.mjs';
|
|
14
|
+
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const paths = ensureMemoryDirs(cwd);
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
const projectName = getProjectName(cwd);
|
|
19
|
+
const pcConfig = config.postCompact || {};
|
|
20
|
+
|
|
21
|
+
// Check if hook is enabled
|
|
22
|
+
if (pcConfig.enabled === false) {
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Read extracted context from PreCompact
|
|
27
|
+
const extractedPath = join(paths.project, 'extracted-context.json');
|
|
28
|
+
let extractions = [];
|
|
29
|
+
|
|
30
|
+
if (existsSync(extractedPath)) {
|
|
31
|
+
try {
|
|
32
|
+
extractions = JSON.parse(readFileSync(extractedPath, 'utf-8'));
|
|
33
|
+
} catch {
|
|
34
|
+
extractions = [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get the most recent extraction
|
|
39
|
+
const latest = extractions[extractions.length - 1];
|
|
40
|
+
|
|
41
|
+
if (!latest) {
|
|
42
|
+
// No extracted context to inject
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check how recent the extraction is (within last 5 minutes)
|
|
47
|
+
const extractionAge = Date.now() - new Date(latest.ts).getTime();
|
|
48
|
+
const maxAge = (pcConfig.maxAgeMinutes || 5) * 60 * 1000;
|
|
49
|
+
|
|
50
|
+
if (extractionAge > maxAge) {
|
|
51
|
+
// Extraction is too old, skip injection
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build injection output
|
|
56
|
+
const sections = [];
|
|
57
|
+
|
|
58
|
+
sections.push(`<claude-mneme-restored project="${escapeAttr(projectName)}">`);
|
|
59
|
+
sections.push('## Context Restored After Compaction\n');
|
|
60
|
+
sections.push('The following context was extracted before compaction and may be relevant:\n');
|
|
61
|
+
|
|
62
|
+
// Inject based on configuration
|
|
63
|
+
const categories = pcConfig.categories || {
|
|
64
|
+
keyPoints: true,
|
|
65
|
+
decisions: true,
|
|
66
|
+
files: true,
|
|
67
|
+
errors: true,
|
|
68
|
+
todos: true
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (categories.keyPoints !== false && latest.keyPoints?.length > 0) {
|
|
72
|
+
sections.push('### Key Points');
|
|
73
|
+
for (const point of latest.keyPoints) {
|
|
74
|
+
sections.push(`- ${point}`);
|
|
75
|
+
}
|
|
76
|
+
sections.push('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (categories.decisions !== false && latest.decisions?.length > 0) {
|
|
80
|
+
sections.push('### Decisions Made');
|
|
81
|
+
for (const decision of latest.decisions) {
|
|
82
|
+
sections.push(`- ${decision}`);
|
|
83
|
+
}
|
|
84
|
+
sections.push('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (categories.files !== false && latest.files?.length > 0) {
|
|
88
|
+
const maxFiles = pcConfig.maxFiles || 10;
|
|
89
|
+
const files = latest.files.slice(0, maxFiles);
|
|
90
|
+
sections.push('### Files Referenced');
|
|
91
|
+
sections.push(`\`${files.join('`, `')}\``);
|
|
92
|
+
sections.push('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (categories.errors !== false && latest.errors?.length > 0) {
|
|
96
|
+
sections.push('### Errors Encountered');
|
|
97
|
+
for (const error of latest.errors) {
|
|
98
|
+
sections.push(`- ${error}`);
|
|
99
|
+
}
|
|
100
|
+
sections.push('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (categories.todos !== false && latest.todos?.length > 0) {
|
|
104
|
+
sections.push('### Pending Items');
|
|
105
|
+
for (const todo of latest.todos) {
|
|
106
|
+
sections.push(`- ${todo}`);
|
|
107
|
+
}
|
|
108
|
+
sections.push('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add custom instructions if any were provided during compact
|
|
112
|
+
if (latest.customInstructions) {
|
|
113
|
+
sections.push('### User Instructions for Compaction');
|
|
114
|
+
sections.push(latest.customInstructions);
|
|
115
|
+
sections.push('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
sections.push('</claude-mneme-restored>');
|
|
119
|
+
|
|
120
|
+
// Only output if we have meaningful content
|
|
121
|
+
const hasContent = latest.keyPoints?.length > 0 ||
|
|
122
|
+
latest.decisions?.length > 0 ||
|
|
123
|
+
latest.files?.length > 0 ||
|
|
124
|
+
latest.errors?.length > 0 ||
|
|
125
|
+
latest.todos?.length > 0 ||
|
|
126
|
+
latest.customInstructions;
|
|
127
|
+
|
|
128
|
+
if (hasContent) {
|
|
129
|
+
console.log(sections.join('\n'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.exit(0);
|