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.
@@ -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
+ }