claude-mem 2.0.2
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/LICENSE +31 -0
- package/README.md +79 -0
- package/claude-mem +0 -0
- package/hooks/pre-compact.js +139 -0
- package/hooks/session-end.js +157 -0
- package/hooks/session-start.js +195 -0
- package/package.json +37 -0
- package/src/claude-mem.js +859 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { query } from '@anthropic-ai/claude-code';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path, { join } from 'path';
|
|
6
|
+
import os, { homedir } from 'os';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { createAnalysisPrompt, DEBUG_MESSAGES } from '../dist/constants.js';
|
|
9
|
+
|
|
10
|
+
const DEBUG_MODE = true;
|
|
11
|
+
const CLAUDE_MEM_HOME = path.join(homedir(), '.claude-mem');
|
|
12
|
+
const INDEX_PATH = path.join(
|
|
13
|
+
CLAUDE_MEM_HOME,
|
|
14
|
+
'index',
|
|
15
|
+
'claude_mem_index.jsonl'
|
|
16
|
+
);
|
|
17
|
+
const ARCHIVES_DIR = path.join(CLAUDE_MEM_HOME, 'archives');
|
|
18
|
+
const LOGS_DIR = path.join(CLAUDE_MEM_HOME, 'logs');
|
|
19
|
+
|
|
20
|
+
let debugLogFile = null;
|
|
21
|
+
let debugLogStream = null;
|
|
22
|
+
|
|
23
|
+
function initDebugLog() {
|
|
24
|
+
if (!DEBUG_MODE) return;
|
|
25
|
+
|
|
26
|
+
ensureClaudeMemStructure();
|
|
27
|
+
|
|
28
|
+
const logsDir = LOGS_DIR;
|
|
29
|
+
if (!fs.existsSync(logsDir)) {
|
|
30
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
34
|
+
debugLogFile = path.join(logsDir, `claude-mem-${timestamp}.log`);
|
|
35
|
+
debugLogStream = fs.createWriteStream(debugLogFile, { flags: 'a' });
|
|
36
|
+
|
|
37
|
+
debugLog('🚀 DEBUG LOG STARTED');
|
|
38
|
+
debugLog(`📁 Log file: ${debugLogFile}`);
|
|
39
|
+
debugLog('═'.repeat(60));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function debugLog(message) {
|
|
43
|
+
if (!DEBUG_MODE || !debugLogStream) return;
|
|
44
|
+
|
|
45
|
+
const timestamp = new Date().toISOString();
|
|
46
|
+
const logLine = `[${timestamp}] ${message}\n`;
|
|
47
|
+
debugLogStream.write(logLine);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function closeDebugLog() {
|
|
51
|
+
if (debugLogStream) {
|
|
52
|
+
debugLog('✅ DEBUG LOG ENDED');
|
|
53
|
+
debugLogStream.end();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureClaudeMemStructure() {
|
|
58
|
+
if (!fs.existsSync(CLAUDE_MEM_HOME)) {
|
|
59
|
+
fs.mkdirSync(CLAUDE_MEM_HOME, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
if (!fs.existsSync(path.join(CLAUDE_MEM_HOME, 'index'))) {
|
|
62
|
+
fs.mkdirSync(path.join(CLAUDE_MEM_HOME, 'index'), { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
if (!fs.existsSync(ARCHIVES_DIR)) {
|
|
65
|
+
fs.mkdirSync(ARCHIVES_DIR, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
68
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class TranscriptCompressor {
|
|
73
|
+
constructor() {
|
|
74
|
+
ensureClaudeMemStructure();
|
|
75
|
+
debugLog('🤖 TranscriptCompressor initialized');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
extractToolUseChains(messages) {
|
|
79
|
+
const chains = [];
|
|
80
|
+
const toolUseMap = new Map();
|
|
81
|
+
|
|
82
|
+
messages.forEach((msg) => {
|
|
83
|
+
if (msg.parent_tool_use_id) {
|
|
84
|
+
const parentId = msg.parent_tool_use_id;
|
|
85
|
+
if (!toolUseMap.has(parentId)) {
|
|
86
|
+
toolUseMap.set(parentId, {
|
|
87
|
+
id: parentId,
|
|
88
|
+
tools: [],
|
|
89
|
+
messages: [],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
toolUseMap.get(parentId).messages.push(msg);
|
|
93
|
+
|
|
94
|
+
if (msg.type === 'assistant' && msg.message?.content) {
|
|
95
|
+
const content = Array.isArray(msg.message.content)
|
|
96
|
+
? msg.message.content[0]?.text || ''
|
|
97
|
+
: msg.message.content;
|
|
98
|
+
const toolMatch = content.match(/Using (\w+) tool/i);
|
|
99
|
+
if (toolMatch) {
|
|
100
|
+
toolUseMap.get(parentId).tools.push(toolMatch[1]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
toolUseMap.forEach((chain, id) => {
|
|
107
|
+
if (chain.tools.length > 0) {
|
|
108
|
+
chains.push(chain);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return chains;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
extractProjectName(transcriptPath) {
|
|
116
|
+
const dir = path.dirname(transcriptPath);
|
|
117
|
+
const dirName = path.basename(dir);
|
|
118
|
+
|
|
119
|
+
let projectName = dirName;
|
|
120
|
+
if (dirName.includes('-Scripts-')) {
|
|
121
|
+
const parts = dirName.split('-Scripts-');
|
|
122
|
+
if (parts.length > 1) {
|
|
123
|
+
projectName = parts[1];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return projectName.replace(/[^a-zA-Z0-9]/g, '_');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async compress(transcriptPath, sessionId) {
|
|
131
|
+
debugLog(DEBUG_MESSAGES.COMPRESSION_STARTED);
|
|
132
|
+
debugLog(DEBUG_MESSAGES.TRANSCRIPT_PATH(transcriptPath));
|
|
133
|
+
debugLog(DEBUG_MESSAGES.SESSION_ID(sessionId || 'auto-detected'));
|
|
134
|
+
debugLog('═'.repeat(60));
|
|
135
|
+
|
|
136
|
+
debugLog(DEBUG_MESSAGES.CLAUDE_SDK_CALL);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const projectName = this.extractProjectName(transcriptPath);
|
|
140
|
+
debugLog(DEBUG_MESSAGES.PROJECT_NAME(projectName));
|
|
141
|
+
debugLog('─'.repeat(40));
|
|
142
|
+
|
|
143
|
+
debugLog('📖 READING ENTIRE TRANSCRIPT (per CLAUDE.md spec)...');
|
|
144
|
+
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
145
|
+
debugLog(` Size: ${content.length} bytes`);
|
|
146
|
+
|
|
147
|
+
const lines = content
|
|
148
|
+
.trim()
|
|
149
|
+
.split('\n')
|
|
150
|
+
.filter((line) => line.trim());
|
|
151
|
+
debugLog(` Lines: ${lines.length}`);
|
|
152
|
+
|
|
153
|
+
const messages = [];
|
|
154
|
+
let parseErrors = 0;
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(lines[i]);
|
|
159
|
+
messages.push(parsed);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
parseErrors++;
|
|
162
|
+
debugLog(` ⚠️ Parse error on line ${i + 1}: ${e.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
debugLog(` Messages parsed: ${messages.length}`);
|
|
167
|
+
if (parseErrors > 0) {
|
|
168
|
+
debugLog(` Parse errors: ${parseErrors}`);
|
|
169
|
+
}
|
|
170
|
+
debugLog('─'.repeat(40));
|
|
171
|
+
|
|
172
|
+
debugLog(
|
|
173
|
+
DEBUG_MESSAGES.TRANSCRIPT_STATS(content.length, messages.length)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
debugLog('\n🎯 MAKING ONE CLAUDE SDK CALL (per CLAUDE.md spec)');
|
|
178
|
+
debugLog(` Analyzing ${messages.length} messages in a single call`);
|
|
179
|
+
debugLog('─'.repeat(40));
|
|
180
|
+
|
|
181
|
+
const prompt = this.createAnalysisPrompt(
|
|
182
|
+
messages,
|
|
183
|
+
projectName,
|
|
184
|
+
sessionId
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
debugLog('📤 FULL PROMPT BEING SENT TO CLAUDE:');
|
|
188
|
+
debugLog('═'.repeat(80));
|
|
189
|
+
debugLog(prompt);
|
|
190
|
+
debugLog('═'.repeat(80));
|
|
191
|
+
debugLog(`📊 PROMPT STATS: ${prompt.length} characters`);
|
|
192
|
+
|
|
193
|
+
debugLog('🤖 CALLING CLAUDE SDK...');
|
|
194
|
+
|
|
195
|
+
// Try to find Claude Code executable
|
|
196
|
+
const possibleClaudePaths = [
|
|
197
|
+
'/opt/homebrew/bin/claude',
|
|
198
|
+
'/usr/local/bin/claude',
|
|
199
|
+
process.env.CLAUDE_CODE_PATH,
|
|
200
|
+
].filter(Boolean);
|
|
201
|
+
|
|
202
|
+
let claudePath = null;
|
|
203
|
+
for (const path of possibleClaudePaths) {
|
|
204
|
+
if (path && fs.existsSync(path)) {
|
|
205
|
+
claudePath = path;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (claudePath) {
|
|
211
|
+
debugLog(DEBUG_MESSAGES.CLAUDE_PATH_FOUND(claudePath));
|
|
212
|
+
} else {
|
|
213
|
+
debugLog('⚠️ Claude Code executable not found, SDK will use default');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Determine which MCP config to use based on what exists
|
|
217
|
+
const possibleMcpConfigs = [
|
|
218
|
+
join(process.cwd(), '.mcp.json'), // Project-level config
|
|
219
|
+
join(os.homedir(), '.claude.json'), // User-level config (correct location)
|
|
220
|
+
join(os.homedir(), '.claude', '.mcp.json'), // Legacy location for backwards compatibility
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
let mcpConfigPath = possibleMcpConfigs.find(fs.existsSync);
|
|
224
|
+
if (!mcpConfigPath) {
|
|
225
|
+
debugLog('⚠️ No MCP config found, defaulting to ~/.claude.json');
|
|
226
|
+
mcpConfigPath = join(os.homedir(), '.claude.json');
|
|
227
|
+
} else {
|
|
228
|
+
debugLog(DEBUG_MESSAGES.MCP_CONFIG_USED(mcpConfigPath));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const response = await query({
|
|
232
|
+
prompt: prompt,
|
|
233
|
+
options: {
|
|
234
|
+
pathToClaudeCodeExecutable: claudePath,
|
|
235
|
+
mcpConfig: mcpConfigPath,
|
|
236
|
+
allowedTools: [
|
|
237
|
+
'mcp__claude-mem__create_entities',
|
|
238
|
+
'mcp__claude-mem__create_relations',
|
|
239
|
+
],
|
|
240
|
+
outputFormat: 'stream-json',
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
debugLog('📥 PROCESSING CLAUDE RESPONSE...');
|
|
245
|
+
|
|
246
|
+
const summaries = [];
|
|
247
|
+
let fullContent = '';
|
|
248
|
+
let messageCount = 0;
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
response &&
|
|
252
|
+
typeof response === 'object' &&
|
|
253
|
+
Symbol.asyncIterator in response
|
|
254
|
+
) {
|
|
255
|
+
debugLog(' Streaming response detected');
|
|
256
|
+
|
|
257
|
+
for await (const message of response) {
|
|
258
|
+
messageCount++;
|
|
259
|
+
|
|
260
|
+
if (message?.content) {
|
|
261
|
+
fullContent += message.content;
|
|
262
|
+
}
|
|
263
|
+
if (message?.text) {
|
|
264
|
+
fullContent += message.text;
|
|
265
|
+
}
|
|
266
|
+
if (message?.data) {
|
|
267
|
+
fullContent += message.data;
|
|
268
|
+
}
|
|
269
|
+
if (message?.message) {
|
|
270
|
+
if (
|
|
271
|
+
message.message.content &&
|
|
272
|
+
Array.isArray(message.message.content)
|
|
273
|
+
) {
|
|
274
|
+
message.message.content.forEach((item) => {
|
|
275
|
+
if (item.type === 'text' && item.text) {
|
|
276
|
+
fullContent += item.text;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (message?.type === 'result' && message?.result) {
|
|
283
|
+
debugLog(`🎯 FINAL RESULT RECEIVED:`);
|
|
284
|
+
debugLog(message.result);
|
|
285
|
+
fullContent = message.result;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
debugLog(
|
|
290
|
+
`📊 RESPONSE SUMMARY: ${messageCount} messages, ${fullContent.length} chars content`
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (fullContent) {
|
|
294
|
+
debugLog(` 📄 Parsing response as JSONL...`);
|
|
295
|
+
const lines = fullContent.split('\n');
|
|
296
|
+
let validLines = 0;
|
|
297
|
+
let invalidLines = 0;
|
|
298
|
+
|
|
299
|
+
lines.forEach((line, idx) => {
|
|
300
|
+
const trimmed = line.trim();
|
|
301
|
+
if (!trimmed) return;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const parsed = JSON.parse(trimmed);
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
parsed.session_id &&
|
|
308
|
+
parsed.summary &&
|
|
309
|
+
parsed.nodes &&
|
|
310
|
+
parsed.keywords
|
|
311
|
+
) {
|
|
312
|
+
summaries.push(parsed);
|
|
313
|
+
|
|
314
|
+
validLines++;
|
|
315
|
+
debugLog(` ✅ Line ${idx + 1}: Valid index entry`);
|
|
316
|
+
|
|
317
|
+
if (validLines === 1) {
|
|
318
|
+
debugLog(
|
|
319
|
+
` Sample: ${parsed.summary.substring(0, 100)}...`
|
|
320
|
+
);
|
|
321
|
+
debugLog(` Nodes: ${parsed.nodes.join(', ')}`);
|
|
322
|
+
debugLog(` Keywords: ${parsed.keywords.join(', ')}`);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
debugLog(
|
|
326
|
+
` ⚠️ Line ${idx + 1}: Missing required fields (session_id, summary, nodes, or keywords)`
|
|
327
|
+
);
|
|
328
|
+
invalidLines++;
|
|
329
|
+
}
|
|
330
|
+
} catch (e) {
|
|
331
|
+
debugLog(` ❌ Line ${idx + 1}: Invalid JSON - ${e.message}`);
|
|
332
|
+
invalidLines++;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
debugLog(
|
|
337
|
+
` 📊 Parsing complete: ${validLines} valid, ${invalidLines} invalid`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
debugLog(' ⚠️ NON-STREAMING RESPONSE DETECTED');
|
|
342
|
+
debugLog(' RAW RESPONSE:');
|
|
343
|
+
debugLog(JSON.stringify(response, null, 2));
|
|
344
|
+
|
|
345
|
+
if (response) {
|
|
346
|
+
debugLog(` Response type: ${typeof response}`);
|
|
347
|
+
|
|
348
|
+
if (typeof response === 'string') {
|
|
349
|
+
debugLog(` Response is string with ${response.length} chars`);
|
|
350
|
+
|
|
351
|
+
const lines = response.split('\n');
|
|
352
|
+
let validLines = 0;
|
|
353
|
+
|
|
354
|
+
lines.forEach((line, idx) => {
|
|
355
|
+
const trimmed = line.trim();
|
|
356
|
+
if (!trimmed) return;
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JSON.parse(trimmed);
|
|
360
|
+
if (
|
|
361
|
+
parsed.session_id &&
|
|
362
|
+
parsed.summary &&
|
|
363
|
+
parsed.nodes &&
|
|
364
|
+
parsed.keywords
|
|
365
|
+
) {
|
|
366
|
+
summaries.push(parsed);
|
|
367
|
+
|
|
368
|
+
validLines++;
|
|
369
|
+
debugLog(` ✅ Line ${idx + 1}: Valid index entry`);
|
|
370
|
+
}
|
|
371
|
+
} catch (e) {
|
|
372
|
+
debugLog(` ❌ Line ${idx + 1}: Invalid JSON - ${e.message}`);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
debugLog(` 📊 Parsed ${validLines} valid summaries`);
|
|
377
|
+
} else if (Array.isArray(response)) {
|
|
378
|
+
debugLog(` Response is array with ${response.length} items`);
|
|
379
|
+
response.forEach((item) => {
|
|
380
|
+
if (
|
|
381
|
+
item.session_id &&
|
|
382
|
+
item.summary &&
|
|
383
|
+
item.nodes &&
|
|
384
|
+
item.keywords
|
|
385
|
+
) {
|
|
386
|
+
summaries.push(item);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
} else if (typeof response === 'object') {
|
|
390
|
+
debugLog(' Response is object with keys:', Object.keys(response));
|
|
391
|
+
|
|
392
|
+
if (
|
|
393
|
+
response.session_id &&
|
|
394
|
+
response.summary &&
|
|
395
|
+
response.nodes &&
|
|
396
|
+
response.keywords
|
|
397
|
+
) {
|
|
398
|
+
summaries.push(response);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
debugLog(' ⚠️ Response is null/undefined');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
debugLog('─'.repeat(40));
|
|
407
|
+
debugLog(DEBUG_MESSAGES.COMPRESSION_COMPLETE(summaries.length));
|
|
408
|
+
|
|
409
|
+
if (summaries.length === 0) {
|
|
410
|
+
debugLog(' ⚠️ WARNING: No summaries were extracted!');
|
|
411
|
+
debugLog(
|
|
412
|
+
' This likely means Claude did not return the expected format.'
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const archivePath = this.createArchive(
|
|
418
|
+
transcriptPath,
|
|
419
|
+
projectName,
|
|
420
|
+
sessionId,
|
|
421
|
+
content
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
this.appendToIndex(
|
|
425
|
+
summaries,
|
|
426
|
+
projectName,
|
|
427
|
+
sessionId,
|
|
428
|
+
messages,
|
|
429
|
+
archivePath
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
debugLog(`\n✅ SUCCESS`);
|
|
433
|
+
debugLog(` Archive created: ${archivePath}`);
|
|
434
|
+
debugLog(` Index updated: ${INDEX_PATH}`);
|
|
435
|
+
debugLog(` Original size: ${content.length} bytes`);
|
|
436
|
+
debugLog(` Summaries created: ${summaries.length}`);
|
|
437
|
+
debugLog('═'.repeat(60));
|
|
438
|
+
|
|
439
|
+
return archivePath;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.error(`\n❌ COMPRESSION FAILED`);
|
|
442
|
+
console.error(` Error: ${error.message}`);
|
|
443
|
+
console.error(` Stack: ${error.stack}`);
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
createAnalysisPrompt(messages, projectName, sessionId) {
|
|
449
|
+
const conversationText = this.formatConversationForPrompt(
|
|
450
|
+
messages,
|
|
451
|
+
false
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const toolUseChains = this.extractToolUseChains(messages);
|
|
455
|
+
|
|
456
|
+
return `You are analyzing a Claude Code conversation transcript to create a sophisticated memory index system using the Model Context Protocol knowledge graph.
|
|
457
|
+
|
|
458
|
+
Your task:
|
|
459
|
+
1. Extract ALL key technical entities following MCP memory best practices
|
|
460
|
+
2. Create rich, searchable entities with detailed observations using MCP tools
|
|
461
|
+
3. Create specific, active-voice relationships between entities
|
|
462
|
+
4. Return compressed summaries in STRICT JSONL format with memory/keyword references
|
|
463
|
+
|
|
464
|
+
${
|
|
465
|
+
toolUseChains.length > 0
|
|
466
|
+
? `TOOL USE CHAINS DETECTED:
|
|
467
|
+
${toolUseChains.map((chain) => `- Tool chain ${chain.id}: ${chain.tools.join(' → ')}`).join('\n')}
|
|
468
|
+
Create relationships for these tool use sequences in the knowledge graph.
|
|
469
|
+
`
|
|
470
|
+
: ''
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
ENTITY EXTRACTION GUIDELINES:
|
|
474
|
+
Focus on these categories for creating a searchable index:
|
|
475
|
+
- **Technical Components**: Functions, classes, services, APIs, databases, modules
|
|
476
|
+
- **Architectural Patterns**: State management, authentication flows, data pipelines, design patterns
|
|
477
|
+
- **Development Decisions**: Design choices, trade-offs, problem solutions, optimizations
|
|
478
|
+
- **Workflows & Processes**: Build processes, deployment strategies, testing approaches
|
|
479
|
+
- **Integration Points**: External APIs, third-party services, data sources
|
|
480
|
+
- **Performance & Reliability**: Caching strategies, error handling, optimization techniques
|
|
481
|
+
- **Bugs & Fixes**: Issues encountered, debugging approaches, solutions applied
|
|
482
|
+
|
|
483
|
+
ENTITY FORMAT:
|
|
484
|
+
- name: "${projectName}_EntityName"
|
|
485
|
+
(Use clear, searchable names that describe WHAT it is, not session-specific IDs)
|
|
486
|
+
IMPORTANT: Include entity type in name for better categorization:
|
|
487
|
+
* "${projectName}_Component_Name" for UI/modules/services
|
|
488
|
+
* "${projectName}_Decision_Name" for architectural choices
|
|
489
|
+
* "${projectName}_Pattern_Name" for design patterns
|
|
490
|
+
* "${projectName}_Tool_Name" for libraries/tools
|
|
491
|
+
* "${projectName}_Fix_Name" for bug fixes
|
|
492
|
+
* "${projectName}_Workflow_Name" for processes
|
|
493
|
+
- entityType: Choose from:
|
|
494
|
+
* "component" - UI components, modules, services
|
|
495
|
+
* "pattern" - Architectural or design patterns
|
|
496
|
+
* "workflow" - Processes, pipelines, sequences
|
|
497
|
+
* "integration" - APIs, external services, data sources
|
|
498
|
+
* "concept" - Abstract ideas, methodologies, principles
|
|
499
|
+
* "decision" - Design choices, trade-offs, solutions
|
|
500
|
+
* "tool" - Utilities, libraries, development tools
|
|
501
|
+
* "fix" - Bug fixes, patches, workarounds
|
|
502
|
+
- observations: Rich, structured details for future recall:
|
|
503
|
+
* "Core purpose: [what it fundamentally does]"
|
|
504
|
+
* "Brief description: [one-line summary for session-start display]"
|
|
505
|
+
* "Implementation: [key technical details, code patterns]"
|
|
506
|
+
* "Dependencies: [what it requires or builds upon]"
|
|
507
|
+
* "Usage context: [when/why it's used]"
|
|
508
|
+
* "Performance characteristics: [speed, reliability, constraints]"
|
|
509
|
+
* "Integration points: [how it connects to other systems]"
|
|
510
|
+
* "Keywords: [searchable terms for this concept]"
|
|
511
|
+
* "Decision rationale: [why this approach was chosen]"
|
|
512
|
+
* "Next steps: [what needs to be done next with this component]"
|
|
513
|
+
* "Files modified: [list of files changed]"
|
|
514
|
+
* "Tools used: [development tools/commands used]"
|
|
515
|
+
* "UUID: ${sessionId}"
|
|
516
|
+
* "Session: ${sessionId}"
|
|
517
|
+
|
|
518
|
+
RELATIONSHIP FORMAT (Use specific, active voice):
|
|
519
|
+
Be precise with relationships to create a meaningful graph:
|
|
520
|
+
- "executes_via", "orchestrates_through", "validates_using"
|
|
521
|
+
- "provides_auth_to", "manages_state_for", "processes_events_from"
|
|
522
|
+
- "caches_data_from", "routes_requests_to", "transforms_data_for"
|
|
523
|
+
- "extends", "enhances_performance_of", "builds_upon"
|
|
524
|
+
- "fixes_issue_in", "replaces", "optimizes"
|
|
525
|
+
- "uses_tool_chain", "triggers_tool", "receives_result_from"
|
|
526
|
+
|
|
527
|
+
OUTPUT FORMAT REQUIREMENTS:
|
|
528
|
+
Return ONLY valid JSONL format (one JSON object per line, NOT an array) with this EXACT structure:
|
|
529
|
+
|
|
530
|
+
EXAMPLE OUTPUT:
|
|
531
|
+
{"timestamp":"${new Date().toISOString()}","session_id":"${sessionId}","project":"${projectName}","summary":"Implemented Redis caching system with TTL support and connection pooling","nodes":["${projectName}_Component_RedisCache","${projectName}_Pattern_ConnectionPool"],"keywords":["redis","caching","ttl","connection_pooling"],"message_count":42,"uuid":"${sessionId}","archive_path":"~/.claude-mem/archives/${sessionId}.jsonl.archive"}
|
|
532
|
+
{"timestamp":"${new Date().toISOString()}","session_id":"${sessionId}","project":"${projectName}","summary":"Built JWT authentication with refresh tokens and role-based access control","nodes":["${projectName}_Component_JWTAuth","${projectName}_Decision_RoleManager","${projectName}_Pattern_RefreshToken"],"keywords":["jwt","authentication","rbac","security","refresh_token"],"message_count":35,"uuid":"${sessionId}","archive_path":"~/.claude-mem/archives/${sessionId}.jsonl.archive"}
|
|
533
|
+
{"timestamp":"${new Date().toISOString()}","session_id":"${sessionId}","project":"${projectName}","summary":"Fixed WebSocket memory leak by implementing proper connection cleanup handlers","nodes":["${projectName}_Fix_WebSocketLeak","${projectName}_Component_ConnectionCleanup"],"keywords":["websocket","memory_leak","cleanup","debugging"],"message_count":28,"uuid":"${sessionId}","archive_path":"~/.claude-mem/archives/${sessionId}.jsonl.archive"}
|
|
534
|
+
|
|
535
|
+
SUMMARY REQUIREMENTS:
|
|
536
|
+
Each index entry must:
|
|
537
|
+
- Be a valid JSON object with ALL required fields: timestamp, session_id, project, summary, nodes, keywords, message_count, uuid, archive_path
|
|
538
|
+
- timestamp: Current ISO timestamp
|
|
539
|
+
- session_id: Use EXACT session ID "${sessionId}" (no "session-" prefix)
|
|
540
|
+
- project: "${projectName}"
|
|
541
|
+
- summary: Describe WHAT was accomplished and WHY it matters (active voice, specific, actionable)
|
|
542
|
+
* Start with action verb: "Implemented", "Fixed", "Refactored", "Designed", "Optimized"
|
|
543
|
+
* Include the main component/feature affected
|
|
544
|
+
* Mention the key outcome or improvement
|
|
545
|
+
- nodes: Array of 2-4 entity names created in knowledge graph (use type-prefixed names)
|
|
546
|
+
- keywords: Array of 3-5 searchable terms (include tool names, patterns, technologies)
|
|
547
|
+
- message_count: Total messages in conversation (use actual count)
|
|
548
|
+
- uuid: "${sessionId}"
|
|
549
|
+
- archive_path: "~/.claude-mem/archives/${sessionId}.jsonl.archive"
|
|
550
|
+
|
|
551
|
+
MCP TOOL USAGE:
|
|
552
|
+
1. FIRST: Call create_entities for each distinct concept/component/decision
|
|
553
|
+
- Create session entity: ${projectName}_Session_${sessionId}
|
|
554
|
+
- Create entities for major components/patterns/decisions
|
|
555
|
+
- Include rich observations with keywords and metadata
|
|
556
|
+
2. THEN: Call create_relations to link related entities
|
|
557
|
+
- Link entities to session entity
|
|
558
|
+
- Create tool chain relationships via parent_tool_use_id
|
|
559
|
+
- Connect new entities to existing ones (if incremental)
|
|
560
|
+
3. FINALLY: Return JSONL summaries referencing created entities
|
|
561
|
+
|
|
562
|
+
INDEXING PRIORITIES:
|
|
563
|
+
- Prioritize entities that will be valuable for future code recall
|
|
564
|
+
- Focus on reusable patterns and solutions
|
|
565
|
+
- Capture decision rationale and trade-offs
|
|
566
|
+
- Include error patterns and their fixes
|
|
567
|
+
- Document integration points and API usage
|
|
568
|
+
- Note performance optimizations and their impact
|
|
569
|
+
- Create entities for tool chains and workflows
|
|
570
|
+
|
|
571
|
+
Project: ${projectName}
|
|
572
|
+
Session ID: ${sessionId}
|
|
573
|
+
|
|
574
|
+
CRITICAL REQUIREMENTS:
|
|
575
|
+
- Create 3-15 entities depending on conversation complexity
|
|
576
|
+
- Create 5-20 relationships showing entity connections
|
|
577
|
+
- Return 3-10 JSONL index entries (one JSON object per line, NOT an array)
|
|
578
|
+
- Each line must be valid JSON parseable with JSON.parse()
|
|
579
|
+
- Each line must have ALL required fields (timestamp, session_id, project, summary, nodes, keywords, message_count, uuid, archive_path)
|
|
580
|
+
- Use EXACT session ID without prefixes
|
|
581
|
+
- Focus on creating a searchable index for future development
|
|
582
|
+
- Original transcript will be archived as ${sessionId}.jsonl.archive
|
|
583
|
+
|
|
584
|
+
Conversation to compress:
|
|
585
|
+
${conversationText}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
formatConversationForPrompt(messages, hasCompressedContent) {
|
|
589
|
+
const jsonlLines = [];
|
|
590
|
+
|
|
591
|
+
messages.forEach((m, index) => {
|
|
592
|
+
let role = 'unknown';
|
|
593
|
+
let messageType = m.type;
|
|
594
|
+
|
|
595
|
+
if (m.type === 'assistant') {
|
|
596
|
+
role = 'assistant';
|
|
597
|
+
} else if (m.type === 'user') {
|
|
598
|
+
role = 'user';
|
|
599
|
+
} else if (m.type === 'result') {
|
|
600
|
+
role = 'system';
|
|
601
|
+
} else if (m.type === 'system') {
|
|
602
|
+
role = 'system';
|
|
603
|
+
} else {
|
|
604
|
+
role = m.message?.role || m.role || 'unknown';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let content = this.extractContent(m);
|
|
608
|
+
|
|
609
|
+
if (!content || content.trim() === '') {
|
|
610
|
+
debugLog(` ⚠️ Skipping message ${index + 1}: empty content`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const timestamp = this.normalizeTimestamp(m);
|
|
615
|
+
|
|
616
|
+
const messageObj = {
|
|
617
|
+
type: messageType,
|
|
618
|
+
role: role,
|
|
619
|
+
content: content,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
if (m.uuid) messageObj.uuid = m.uuid;
|
|
623
|
+
if (m.session_id) messageObj.session_id = m.session_id;
|
|
624
|
+
if (m.parent_tool_use_id)
|
|
625
|
+
messageObj.parent_tool_use_id = m.parent_tool_use_id;
|
|
626
|
+
|
|
627
|
+
if (timestamp) {
|
|
628
|
+
messageObj.ts = timestamp;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
jsonlLines.push(JSON.stringify(messageObj));
|
|
634
|
+
} catch (e) {
|
|
635
|
+
const safeObj = {
|
|
636
|
+
type: messageType,
|
|
637
|
+
role: role,
|
|
638
|
+
content: String(content).replace(/[\u0000-\u001F\u007F-\u009F]/g, ''),
|
|
639
|
+
error: 'content_sanitized',
|
|
640
|
+
};
|
|
641
|
+
if (m.uuid) safeObj.uuid = m.uuid;
|
|
642
|
+
if (m.session_id) safeObj.session_id = m.session_id;
|
|
643
|
+
if (timestamp) safeObj.ts = timestamp;
|
|
644
|
+
jsonlLines.push(JSON.stringify(safeObj));
|
|
645
|
+
debugLog(
|
|
646
|
+
` ⚠️ Message ${index + 1} sanitized due to JSON error: ${e.message}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
debugLog(
|
|
652
|
+
`📊 Field filtering complete: ${jsonlLines.length} messages processed`
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
return `\`\`\`\n${jsonlLines.join('\n')}\n\`\`\`\n\n* All dates/times are in UTC`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
extractContent(m) {
|
|
659
|
+
if (m.type === 'assistant' || m.type === 'user') {
|
|
660
|
+
const messageContent = m.message?.content;
|
|
661
|
+
if (messageContent) {
|
|
662
|
+
if (Array.isArray(messageContent)) {
|
|
663
|
+
return messageContent
|
|
664
|
+
.map((item) => {
|
|
665
|
+
if (typeof item === 'string') return item;
|
|
666
|
+
if (item.text) return item.text;
|
|
667
|
+
if (item.content) return item.content;
|
|
668
|
+
return '';
|
|
669
|
+
})
|
|
670
|
+
.filter(Boolean)
|
|
671
|
+
.join(' ');
|
|
672
|
+
}
|
|
673
|
+
return String(messageContent).trim();
|
|
674
|
+
}
|
|
675
|
+
} else if (m.type === 'result') {
|
|
676
|
+
if (m.subtype === 'success' && m.result) {
|
|
677
|
+
return `[Result: ${m.result}]`;
|
|
678
|
+
} else if (m.subtype === 'error_max_turns') {
|
|
679
|
+
return '[Error: Maximum turns reached]';
|
|
680
|
+
} else if (m.subtype === 'error_during_execution') {
|
|
681
|
+
return '[Error during execution]';
|
|
682
|
+
}
|
|
683
|
+
} else if (m.type === 'system' && m.subtype === 'init') {
|
|
684
|
+
return `[System initialized: ${m.model}, tools: ${m.tools?.length || 0}, MCP servers: ${m.mcp_servers?.length || 0}]`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let content = m.message?.content || m.content || '';
|
|
688
|
+
|
|
689
|
+
if (Array.isArray(content)) {
|
|
690
|
+
content = content
|
|
691
|
+
.map((item) => {
|
|
692
|
+
if (typeof item === 'string') return item;
|
|
693
|
+
if (item.text) return item.text;
|
|
694
|
+
if (item.content) return item.content;
|
|
695
|
+
return '';
|
|
696
|
+
})
|
|
697
|
+
.filter(Boolean)
|
|
698
|
+
.join(' ');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (m.toolUseResult) {
|
|
702
|
+
const toolSummary = this.summarizeToolResult(m.toolUseResult, content);
|
|
703
|
+
if (toolSummary) {
|
|
704
|
+
content = content ? `${content}\n\n${toolSummary}` : toolSummary;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return String(content).trim();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
summarizeToolResult(toolResult, existingContent) {
|
|
712
|
+
if (!toolResult) return '';
|
|
713
|
+
|
|
714
|
+
const summaryParts = [];
|
|
715
|
+
|
|
716
|
+
if (toolResult.stdout) {
|
|
717
|
+
const stdout = String(toolResult.stdout);
|
|
718
|
+
if (stdout.length > 200) {
|
|
719
|
+
const lineCount = stdout.split('\n').length;
|
|
720
|
+
const charCount = stdout.length;
|
|
721
|
+
|
|
722
|
+
const lines = stdout.split('\n');
|
|
723
|
+
const preview = lines.slice(0, 3).join('\n');
|
|
724
|
+
const suffix =
|
|
725
|
+
lines.length > 6 ? `\n...\n${lines.slice(-2).join('\n')}` : '';
|
|
726
|
+
|
|
727
|
+
summaryParts.push(
|
|
728
|
+
`Result: ${preview}${suffix} (${lineCount} lines, ${charCount} chars)`
|
|
729
|
+
);
|
|
730
|
+
} else {
|
|
731
|
+
summaryParts.push(`Result: ${stdout}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (toolResult.stderr && toolResult.stderr.trim()) {
|
|
736
|
+
summaryParts.push(
|
|
737
|
+
`Error: ${toolResult.stderr.substring(0, 150)}${toolResult.stderr.length > 150 ? '...' : ''}`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (toolResult.interrupted) {
|
|
742
|
+
summaryParts.push('(interrupted)');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (toolResult.isImage) {
|
|
746
|
+
summaryParts.push('(image output)');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return summaryParts.join('\n');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
normalizeTimestamp(m) {
|
|
753
|
+
const ts =
|
|
754
|
+
m.timestamp ||
|
|
755
|
+
m.message?.timestamp ||
|
|
756
|
+
m.created_at ||
|
|
757
|
+
m.message?.created_at;
|
|
758
|
+
|
|
759
|
+
if (!ts) return '';
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
const date = new Date(ts);
|
|
763
|
+
if (isNaN(date.getTime())) return '';
|
|
764
|
+
|
|
765
|
+
return date.toISOString().slice(0, 19).replace('T', ' ');
|
|
766
|
+
} catch (e) {
|
|
767
|
+
debugLog(` ⚠️ Invalid timestamp format: ${ts}`);
|
|
768
|
+
return '';
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
createArchive(transcriptPath, projectName, sessionId, content) {
|
|
773
|
+
const projectArchiveDir = path.join(ARCHIVES_DIR, projectName);
|
|
774
|
+
if (!fs.existsSync(projectArchiveDir)) {
|
|
775
|
+
fs.mkdirSync(projectArchiveDir, { recursive: true });
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const archivePath = path.join(
|
|
779
|
+
projectArchiveDir,
|
|
780
|
+
`${sessionId}.jsonl.archive`
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
fs.writeFileSync(archivePath, content);
|
|
784
|
+
|
|
785
|
+
debugLog(`📦 Created archive: ${archivePath}`);
|
|
786
|
+
return archivePath;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
appendToIndex(summaries, projectName, sessionId, messages, archivePath) {
|
|
790
|
+
const indexDir = path.dirname(INDEX_PATH);
|
|
791
|
+
if (!fs.existsSync(indexDir)) {
|
|
792
|
+
fs.mkdirSync(indexDir, { recursive: true });
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const messageCount = messages.length;
|
|
796
|
+
|
|
797
|
+
const indexEntries = summaries.map((entry) => {
|
|
798
|
+
return {
|
|
799
|
+
...entry,
|
|
800
|
+
|
|
801
|
+
session_id: sessionId,
|
|
802
|
+
|
|
803
|
+
project: projectName,
|
|
804
|
+
|
|
805
|
+
message_count: entry.message_count || messageCount,
|
|
806
|
+
|
|
807
|
+
archive_path: archivePath,
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const indexContent =
|
|
812
|
+
indexEntries.map((entry) => JSON.stringify(entry)).join('\n') + '\n';
|
|
813
|
+
fs.appendFileSync(INDEX_PATH, indexContent);
|
|
814
|
+
|
|
815
|
+
debugLog(`📝 Appended ${indexEntries.length} entries to index`);
|
|
816
|
+
debugLog(` Index path: ${INDEX_PATH}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function main() {
|
|
821
|
+
initDebugLog();
|
|
822
|
+
|
|
823
|
+
const args = process.argv.slice(2);
|
|
824
|
+
|
|
825
|
+
if (args.length === 0) {
|
|
826
|
+
console.error(
|
|
827
|
+
'Usage: node src/claude-mem.js <transcript-path> [session-id]'
|
|
828
|
+
);
|
|
829
|
+
closeDebugLog();
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const transcriptPath = args[0];
|
|
834
|
+
const sessionId = args[1] || path.basename(transcriptPath, '.jsonl');
|
|
835
|
+
|
|
836
|
+
debugLog(`🚀 Starting compression for: ${transcriptPath}`);
|
|
837
|
+
debugLog(`📋 Session ID: ${sessionId}`);
|
|
838
|
+
|
|
839
|
+
const compressor = new TranscriptCompressor();
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
await compressor.compress(transcriptPath, sessionId);
|
|
843
|
+
debugLog('✅ Compression successful');
|
|
844
|
+
closeDebugLog();
|
|
845
|
+
process.exit(0);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
debugLog(`❌ Compression failed: ${error.message}`);
|
|
848
|
+
debugLog(`💥 Stack trace: ${error.stack}`);
|
|
849
|
+
closeDebugLog();
|
|
850
|
+
console.error('❌ Compression failed:', error.message);
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export { TranscriptCompressor };
|
|
856
|
+
|
|
857
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
858
|
+
main();
|
|
859
|
+
}
|