claude-mem 3.3.7 → 3.3.9

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.
Files changed (96) hide show
  1. package/README.md +183 -46
  2. package/dist/bin/cli.d.ts +2 -0
  3. package/dist/bin/cli.js +179 -0
  4. package/dist/commands/compress.d.ts +2 -0
  5. package/dist/commands/compress.js +27 -0
  6. package/dist/commands/hooks.d.ts +19 -0
  7. package/dist/commands/hooks.js +131 -0
  8. package/dist/commands/install.d.ts +2 -0
  9. package/dist/commands/install.js +836 -0
  10. package/dist/commands/load-context.d.ts +2 -0
  11. package/dist/commands/load-context.js +151 -0
  12. package/dist/commands/logs.d.ts +2 -0
  13. package/dist/commands/logs.js +76 -0
  14. package/dist/commands/migrate-to-jsonl.d.ts +5 -0
  15. package/dist/commands/migrate-to-jsonl.js +99 -0
  16. package/dist/commands/restore.d.ts +1 -0
  17. package/dist/commands/restore.js +20 -0
  18. package/dist/commands/status.d.ts +1 -0
  19. package/dist/commands/status.js +136 -0
  20. package/dist/commands/trash-empty.d.ts +3 -0
  21. package/dist/commands/trash-empty.js +56 -0
  22. package/dist/commands/trash-view.d.ts +1 -0
  23. package/dist/commands/trash-view.js +101 -0
  24. package/dist/commands/trash.d.ts +6 -0
  25. package/dist/commands/trash.js +49 -0
  26. package/dist/commands/uninstall.d.ts +2 -0
  27. package/dist/commands/uninstall.js +107 -0
  28. package/dist/constants.d.ts +271 -0
  29. package/dist/constants.js +199 -0
  30. package/dist/core/compression/TranscriptCompressor.d.ts +79 -0
  31. package/dist/core/compression/TranscriptCompressor.js +585 -0
  32. package/dist/core/orchestration/PromptOrchestrator.d.ts +165 -0
  33. package/dist/core/orchestration/PromptOrchestrator.js +182 -0
  34. package/dist/lib/time-utils.d.ts +5 -0
  35. package/dist/lib/time-utils.js +70 -0
  36. package/dist/prompts/constants.d.ts +126 -0
  37. package/dist/prompts/constants.js +161 -0
  38. package/dist/prompts/index.d.ts +10 -0
  39. package/dist/prompts/index.js +11 -0
  40. package/dist/prompts/templates/analysis/AnalysisTemplates.d.ts +13 -0
  41. package/dist/prompts/templates/analysis/AnalysisTemplates.js +94 -0
  42. package/dist/prompts/templates/context/ContextTemplates.d.ts +119 -0
  43. package/dist/prompts/templates/context/ContextTemplates.js +399 -0
  44. package/dist/prompts/templates/hooks/HookTemplates.d.ts +175 -0
  45. package/dist/prompts/templates/hooks/HookTemplates.js +394 -0
  46. package/dist/prompts/templates/hooks/HookTemplates.test.d.ts +7 -0
  47. package/dist/prompts/templates/hooks/HookTemplates.test.js +127 -0
  48. package/dist/shared/config.d.ts +4 -0
  49. package/dist/shared/config.js +41 -0
  50. package/dist/shared/error-handler.d.ts +22 -0
  51. package/dist/shared/error-handler.js +142 -0
  52. package/dist/shared/logger.d.ts +19 -0
  53. package/dist/shared/logger.js +51 -0
  54. package/dist/shared/paths.d.ts +28 -0
  55. package/dist/shared/paths.js +100 -0
  56. package/dist/shared/settings.d.ts +41 -0
  57. package/dist/shared/settings.js +81 -0
  58. package/dist/shared/types.d.ts +145 -0
  59. package/dist/shared/types.js +78 -0
  60. package/docs/STATUS.md +155 -0
  61. package/docs/chroma-backend-migration.md +161 -0
  62. package/docs/landing-page-outline.md +287 -0
  63. package/docs/multi-platform-builds.md +96 -0
  64. package/docs/plans/cloud-service-plan.md +274 -0
  65. package/docs/plans/fix-response-format-issue.md +61 -0
  66. package/docs/plans/restructure-session-hook-output.md +102 -0
  67. package/docs/plans/session-start-hook-investigation.md +45 -0
  68. package/docs/plans/src-reorganization-plan.md +181 -0
  69. package/docs/plans/terminal-effects-decision.md +22 -0
  70. package/docs/plans/terminal-effects-integration.md +82 -0
  71. package/docs/plans/trash-bin-feature-plan.md +240 -0
  72. package/docs/plans/trash-bin-minimal-plan.md +102 -0
  73. package/docs/reference/bun-single-executable.md +584 -0
  74. package/docs/reference/cc-output-styles.md +99 -0
  75. package/docs/reference/chroma-mcp-project-memory-example.md +80 -0
  76. package/docs/reference/chroma-mcp-team-example.md +92 -0
  77. package/docs/reference/claude-code/cc-hooks.md +787 -0
  78. package/docs/reference/claude-code/cc-status-line.md +202 -0
  79. package/docs/reference/claude-code/hook-configuration.md +173 -0
  80. package/docs/reference/claude-code/hook-responses.md +127 -0
  81. package/docs/reference/claude-code/hooks.md +175 -0
  82. package/docs/reference/claude-code/mcp-configuration.md +133 -0
  83. package/docs/reference/claude-code/session-start-hook.md +82 -0
  84. package/docs/reference/load-context-format-example.md +33 -0
  85. package/docs/reference/mcp-sdk/mcp-typescript-sdk-readme.md +1323 -0
  86. package/docs/reference/mcp-sdk/server-implementation.md +286 -0
  87. package/docs/reference/mcp-sdk/stdio-transport.md +345 -0
  88. package/docs/todos/fix-response-format-tasks.md +43 -0
  89. package/docs/todos/implementation-todos.md +280 -0
  90. package/docs/todos/restructure-hook-output-tasks.md +103 -0
  91. package/docs/todos/session-start-hook-fix.md +26 -0
  92. package/docs/todos/terminal-effects-tasks.md +42 -0
  93. package/docs/todos/trash-bin-implementation-todos.md +348 -0
  94. package/docs/todos/trash-bin-minimal-todos.md +27 -0
  95. package/package.json +56 -6
  96. package/claude-mem +0 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Compression options for the TranscriptCompressor
3
+ */
4
+ export interface CompressionOptions {
5
+ output?: string;
6
+ dryRun?: boolean;
7
+ verbose?: boolean;
8
+ }
9
+ /**
10
+ * TranscriptCompressor handles the analysis and compression of Claude Code conversation transcripts
11
+ * into a searchable memory database format using the Model Context Protocol.
12
+ */
13
+ export declare class TranscriptCompressor {
14
+ private paths;
15
+ private logStream;
16
+ private logFile;
17
+ private promptOrchestrator;
18
+ constructor(options?: CompressionOptions);
19
+ /**
20
+ * Ensures that the required directory structure exists
21
+ */
22
+ private ensureClaudeMemStructure;
23
+ private initializeLogging;
24
+ private debugLog;
25
+ private closeLogging;
26
+ /**
27
+ * Main compression method that processes a transcript and creates compressed memories
28
+ */
29
+ compress(transcriptPath: string, sessionId?: string): Promise<string>;
30
+ /**
31
+ * Finds MCP configuration file
32
+ */
33
+ private findMCPConfig;
34
+ /**
35
+ * Processes Claude response to extract summaries from JSON
36
+ */
37
+ private processClaudeResponse;
38
+ /**
39
+ * Extracts content from response
40
+ */
41
+ private extractResponseContent;
42
+ /**
43
+ * Extracts content from a single message
44
+ */
45
+ private extractMessageContent;
46
+ /**
47
+ * Extracts JSON response and returns raw JSON objects
48
+ */
49
+ private extractJSONResponse;
50
+ /**
51
+ * Legacy fallback for pipe-separated format
52
+ */
53
+ private extractLegacyPipeSeparatedLines;
54
+ /**
55
+ * Formats conversation messages for analysis prompt
56
+ */
57
+ private formatConversationForPrompt;
58
+ /**
59
+ * Extracts content from message object
60
+ */
61
+ private extractContent;
62
+ /**
63
+ * Summarizes tool use results
64
+ */
65
+ private summarizeToolResult;
66
+ /**
67
+ * Normalizes timestamp formats
68
+ */
69
+ private normalizeTimestamp;
70
+ /**
71
+ * Creates an archive file of the original transcript
72
+ */
73
+ private createArchive;
74
+ /**
75
+ * Appends summaries in JSONL format to the index file
76
+ * Each line is a JSON object with type field for easy parsing
77
+ */
78
+ private appendToIndex;
79
+ }
@@ -0,0 +1,585 @@
1
+ import { query } from '@anthropic-ai/claude-code';
2
+ import fs, { createWriteStream } from 'fs';
3
+ import path, { join } from 'path';
4
+ import os from 'os';
5
+ import { PathResolver } from '../../shared/paths.js';
6
+ import { PromptOrchestrator, createAnalysisContext } from '../orchestration/PromptOrchestrator.js';
7
+ import { DEBUG_MESSAGES } from '../../constants.js';
8
+ import { log } from '../../shared/logger.js';
9
+ import { getClaudePath } from '../../shared/settings.js';
10
+ /**
11
+ * TranscriptCompressor handles the analysis and compression of Claude Code conversation transcripts
12
+ * into a searchable memory database format using the Model Context Protocol.
13
+ */
14
+ export class TranscriptCompressor {
15
+ paths;
16
+ logStream = null;
17
+ logFile = null;
18
+ promptOrchestrator;
19
+ // <Block> 1.1 ====================================
20
+ // Constructor Initialization - Natural flow (8/10)
21
+ constructor(options = {}) {
22
+ this.paths = new PathResolver();
23
+ this.promptOrchestrator = new PromptOrchestrator();
24
+ this.ensureClaudeMemStructure();
25
+ this.initializeLogging();
26
+ log.debug('🤖 TranscriptCompressor initialized');
27
+ }
28
+ // </Block> =======================================
29
+ // <Block> 1.2 ====================================
30
+ // Directory Structure Validation - Natural flow (8/10)
31
+ /**
32
+ * Ensures that the required directory structure exists
33
+ */
34
+ ensureClaudeMemStructure() {
35
+ const configDir = this.paths.getConfigDir();
36
+ const indexDir = this.paths.getIndexDir();
37
+ const archiveDir = this.paths.getArchiveDir();
38
+ const logsDir = this.paths.getLogsDir();
39
+ PathResolver.ensureDirectories([configDir, indexDir, archiveDir, logsDir]);
40
+ }
41
+ initializeLogging() {
42
+ const logsDir = this.paths.getLogsDir();
43
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
44
+ this.logFile = join(logsDir, `claude-mem-${timestamp}.log`);
45
+ this.logStream = createWriteStream(this.logFile, { flags: 'a' });
46
+ this.debugLog('🚀 DEBUG LOG STARTED');
47
+ this.debugLog(`📁 Log file: ${this.logFile}`);
48
+ this.debugLog('═'.repeat(60));
49
+ }
50
+ debugLog(message) {
51
+ if (!this.logStream)
52
+ return;
53
+ const timestamp = new Date().toISOString();
54
+ const logLine = `[${timestamp}] ${message}\n`;
55
+ this.logStream.write(logLine);
56
+ }
57
+ closeLogging() {
58
+ if (this.logStream) {
59
+ this.debugLog('✅ DEBUG LOG ENDED');
60
+ this.logStream.end();
61
+ }
62
+ }
63
+ // </Block> =======================================
64
+ // <Block> 1.3 ====================================
65
+ // </Block> =======================================
66
+ // <Block> 1.4 ====================================
67
+ // Main Compression Flow - DEBUG GUARDS INTERRUPT FLOW (5/10)
68
+ /**
69
+ * Main compression method that processes a transcript and creates compressed memories
70
+ */
71
+ async compress(transcriptPath, sessionId) {
72
+ this.debugLog(`🚀 Starting compression for: ${transcriptPath}`);
73
+ this.debugLog(`📋 Session ID: ${sessionId || 'auto-generated'}`);
74
+ try {
75
+ const projectPrefix = PathResolver.getCurrentProjectPrefix();
76
+ log.debug(DEBUG_MESSAGES.PROJECT_NAME(projectPrefix));
77
+ this.debugLog(`📝 PROJECT PREFIX: ${projectPrefix}`);
78
+ // Read and parse transcript
79
+ const content = fs.readFileSync(transcriptPath, 'utf-8');
80
+ this.debugLog(`📖 Reading transcript: ${content.length} bytes`);
81
+ const lines = content.trim().split('\n').filter(line => line.trim());
82
+ const messages = [];
83
+ let parseErrors = 0;
84
+ for (let i = 0; i < lines.length; i++) {
85
+ try {
86
+ const parsed = JSON.parse(lines[i]);
87
+ messages.push(parsed);
88
+ }
89
+ catch (e) {
90
+ parseErrors++;
91
+ log.debug(`Parse error on line ${i + 1}: ${e.message}`);
92
+ }
93
+ }
94
+ log.debug(DEBUG_MESSAGES.TRANSCRIPT_STATS(content.length, messages.length));
95
+ if (parseErrors > 0) {
96
+ log.debug(`Parse errors: ${parseErrors}`);
97
+ }
98
+ this.debugLog(`📊 Transcript loaded: ${lines.length} lines, ${messages.length} messages, ${parseErrors} parse errors`);
99
+ // Generate final session ID
100
+ const finalSessionId = sessionId || path.basename(transcriptPath, '.jsonl');
101
+ // Get timestamp from first message or use current time
102
+ const firstMessage = messages.find(m => m.timestamp);
103
+ let timestamp = new Date().toISOString();
104
+ if (firstMessage?.timestamp) {
105
+ const ts = Number(firstMessage.timestamp);
106
+ // Check if timestamp is in seconds (Unix) or milliseconds
107
+ // Unix timestamps are typically 10 digits, JS timestamps are 13
108
+ const dateValue = ts < 10000000000 ? ts * 1000 : ts;
109
+ try {
110
+ timestamp = new Date(dateValue).toISOString();
111
+ }
112
+ catch (e) {
113
+ // Fall back to current time if timestamp is invalid
114
+ this.debugLog(`⚠️ Invalid timestamp: ${firstMessage.timestamp}, using current time`);
115
+ }
116
+ }
117
+ // Archive filename for reference
118
+ const archiveFilename = `${finalSessionId}.jsonl.archive`;
119
+ // Format conversation for analysis
120
+ const conversationText = this.formatConversationForPrompt(messages);
121
+ // Create analysis prompt using PromptOrchestrator
122
+ const analysisContext = createAnalysisContext(conversationText, finalSessionId, {
123
+ projectName: projectPrefix,
124
+ trigger: 'manual'
125
+ });
126
+ const analysisPrompt = this.promptOrchestrator.createAnalysisPrompt(analysisContext);
127
+ log.debug('📤 Analysis prompt created');
128
+ log.debug(`📊 Prompt length: ${analysisPrompt.prompt.length} characters`);
129
+ // LOG THE FULL PROMPT TO DEBUG FILE
130
+ const promptDebugPath = path.join(this.paths.getLogsDir(), `claude-prompt-${Date.now()}.txt`);
131
+ fs.writeFileSync(promptDebugPath, `=== CLAUDE ANALYSIS PROMPT ===\n${analysisPrompt.prompt}\n`);
132
+ this.debugLog(`📝 Full prompt saved to: ${promptDebugPath}`);
133
+ // Find MCP config and get Claude path from settings
134
+ const claudePath = getClaudePath();
135
+ const mcpConfigPath = this.findMCPConfig();
136
+ log.debug(DEBUG_MESSAGES.CLAUDE_PATH_FOUND(claudePath));
137
+ if (mcpConfigPath) {
138
+ log.debug(DEBUG_MESSAGES.MCP_CONFIG_USED(mcpConfigPath));
139
+ }
140
+ // Call Claude SDK for analysis
141
+ this.debugLog('🤖 Calling Claude SDK with MCP tools...');
142
+ const response = await query({
143
+ prompt: analysisPrompt.prompt,
144
+ options: {
145
+ allowedTools: [
146
+ 'mcp__claude-mem__chroma_list_collections',
147
+ 'mcp__claude-mem__chroma_create_collection',
148
+ 'mcp__claude-mem__chroma_peek_collection',
149
+ 'mcp__claude-mem__chroma_get_collection_info',
150
+ 'mcp__claude-mem__chroma_get_collection_count',
151
+ 'mcp__claude-mem__chroma_modify_collection',
152
+ 'mcp__claude-mem__chroma_fork_collection',
153
+ 'mcp__claude-mem__chroma_delete_collection',
154
+ 'mcp__claude-mem__chroma_add_documents',
155
+ 'mcp__claude-mem__chroma_query_documents',
156
+ 'mcp__claude-mem__chroma_get_documents',
157
+ 'mcp__claude-mem__chroma_update_documents',
158
+ 'mcp__claude-mem__chroma_delete_documents',
159
+ ],
160
+ pathToClaudeCodeExecutable: getClaudePath(),
161
+ },
162
+ });
163
+ this.debugLog('✅ Claude SDK response received');
164
+ // Process response and extract summaries from JSON
165
+ this.debugLog('🔄 Processing Claude JSON response...');
166
+ const extractResult = await this.processClaudeResponse(response);
167
+ this.debugLog(`📋 Extracted ${extractResult.summaries.length} summaries from JSON`);
168
+ if (extractResult.overview) {
169
+ this.debugLog(`📝 Overview: ${extractResult.overview}`);
170
+ }
171
+ log.debug(DEBUG_MESSAGES.COMPRESSION_COMPLETE(extractResult.summaries.length));
172
+ // Continue processing even with zero summaries - let the natural flow handle empty results
173
+ // Create archive and update index
174
+ const archivePath = this.createArchive(transcriptPath, projectPrefix, finalSessionId, content);
175
+ this.debugLog(`📦 Archive created: ${archivePath}`);
176
+ this.appendToIndex(extractResult.summaries, extractResult.overview, projectPrefix, finalSessionId, messages, archivePath);
177
+ this.debugLog(`📥 Written ${extractResult.summaries.length} summaries to index`);
178
+ log.debug(`✅ SUCCESS`);
179
+ log.debug(`Archive created: ${archivePath}`);
180
+ log.debug(`Summaries created: ${extractResult.summaries.length}`);
181
+ this.debugLog('✅ Compression completed successfully');
182
+ this.closeLogging();
183
+ return archivePath;
184
+ }
185
+ catch (error) {
186
+ log.error('COMPRESSION FAILED', error, {
187
+ transcriptPath,
188
+ sessionId
189
+ });
190
+ this.debugLog(`❌ ERROR: ${error instanceof Error ? error.message : String(error)}`);
191
+ this.closeLogging();
192
+ throw error;
193
+ }
194
+ }
195
+ // </Block> =======================================
196
+ /**
197
+ * Finds MCP configuration file
198
+ */
199
+ findMCPConfig() {
200
+ const possibleConfigs = [
201
+ join(process.cwd(), '.mcp.json'),
202
+ join(os.homedir(), '.claude.json'),
203
+ join(os.homedir(), '.claude', '.mcp.json'),
204
+ ];
205
+ const mcpConfigPath = possibleConfigs.find(fs.existsSync);
206
+ return mcpConfigPath || join(os.homedir(), '.claude.json');
207
+ }
208
+ // <Block> 1.5 ====================================
209
+ // Claude Response Processing - JSON extraction with pipe-separated output (9/10)
210
+ /**
211
+ * Processes Claude response to extract summaries from JSON
212
+ */
213
+ async processClaudeResponse(response) {
214
+ let fullContent = '';
215
+ // Extract content using polymorphic handlers
216
+ fullContent = await this.extractResponseContent(response, []);
217
+ // DEBUG: Log the full content to see what Claude is returning
218
+ this.debugLog(`🔍 Claude response content length: ${fullContent.length}`);
219
+ // Write raw response to debug file for troubleshooting
220
+ const debugPath = path.join(this.paths.getLogsDir(), `claude-response-${Date.now()}.txt`);
221
+ fs.writeFileSync(debugPath, `=== CLAUDE RAW RESPONSE ===\n${fullContent}\n`);
222
+ this.debugLog(`📝 Raw response saved to: ${debugPath}`);
223
+ // Extract JSON from response tags
224
+ const extractResult = this.extractJSONResponse(fullContent);
225
+ this.debugLog(`📊 Extracted ${extractResult.summaries.length} summaries from JSON`);
226
+ if (extractResult.summaries.length === 0) {
227
+ this.debugLog(`⚠️ No summaries found in JSON response`);
228
+ }
229
+ return extractResult;
230
+ }
231
+ /**
232
+ * Extracts content from response
233
+ */
234
+ async extractResponseContent(response, summaries) {
235
+ // Handle streaming response
236
+ if (response && typeof response === 'object' && Symbol.asyncIterator in response) {
237
+ let content = '';
238
+ for await (const message of response) {
239
+ content += this.extractMessageContent(message);
240
+ if (message?.type === 'result' && message?.result) {
241
+ content = message.result;
242
+ }
243
+ }
244
+ return content;
245
+ }
246
+ // Handle string response
247
+ if (typeof response === 'string') {
248
+ return response;
249
+ }
250
+ // Handle array response
251
+ if (Array.isArray(response)) {
252
+ return response.map(item => {
253
+ if (typeof item === 'string')
254
+ return item;
255
+ if (item?.text)
256
+ return item.text;
257
+ if (item?.content)
258
+ return item.content;
259
+ return '';
260
+ }).filter(Boolean).join('\n');
261
+ }
262
+ // Handle object response
263
+ if (typeof response === 'object' && response !== null) {
264
+ if (response?.text)
265
+ return response.text;
266
+ if (response?.content)
267
+ return response.content;
268
+ if (response?.message)
269
+ return response.message;
270
+ return '';
271
+ }
272
+ return '';
273
+ }
274
+ /**
275
+ * Extracts content from a single message
276
+ */
277
+ extractMessageContent(message) {
278
+ let content = '';
279
+ if (message?.content)
280
+ content += message.content;
281
+ if (message?.text)
282
+ content += message.text;
283
+ if (message?.data)
284
+ content += message.data;
285
+ if (message?.message?.content && Array.isArray(message.message.content)) {
286
+ message.message.content.forEach((item) => {
287
+ if (item.type === 'text' && item.text) {
288
+ content += item.text;
289
+ }
290
+ });
291
+ }
292
+ return content;
293
+ }
294
+ /**
295
+ * Extracts JSON response and returns raw JSON objects
296
+ */
297
+ extractJSONResponse(content) {
298
+ try {
299
+ // Extract JSON from response tags
300
+ const jsonMatch = content.match(/<JSONResponse>([\s\S]*?)<\/JSONResponse>/);
301
+ if (!jsonMatch) {
302
+ this.debugLog(`⚠️ No <JSONResponse> tags found in response`);
303
+ return { overview: null, summaries: [] };
304
+ }
305
+ const jsonContent = jsonMatch[1].trim();
306
+ this.debugLog(`✅ Found JSON response: ${jsonContent.length} chars`);
307
+ // Parse the JSON
308
+ const parsed = JSON.parse(jsonContent);
309
+ if (!parsed.summaries || !Array.isArray(parsed.summaries)) {
310
+ this.debugLog(`⚠️ Invalid JSON structure: missing summaries array`);
311
+ return { overview: null, summaries: [] };
312
+ }
313
+ // Return raw JSON objects instead of converting to pipe-separated format
314
+ const validSummaries = [];
315
+ parsed.summaries.forEach((summary, index) => {
316
+ if (!summary.text || !summary.document_id) {
317
+ this.debugLog(`⚠️ Skipping invalid summary at index ${index}`);
318
+ return;
319
+ }
320
+ // Ensure required fields are present
321
+ const validSummary = {
322
+ text: summary.text,
323
+ document_id: summary.document_id,
324
+ keywords: summary.keywords || '',
325
+ timestamp: summary.timestamp || new Date().toISOString(),
326
+ archive: summary.archive || `${summary.document_id}.jsonl.archive`
327
+ };
328
+ validSummaries.push(validSummary);
329
+ this.debugLog(`✅ Valid summary ${index + 1}: ${summary.document_id}`);
330
+ });
331
+ // Store overview if present
332
+ if (parsed.overview) {
333
+ this.debugLog(`📝 Session overview: ${parsed.overview}`);
334
+ }
335
+ return { overview: parsed.overview || null, summaries: validSummaries };
336
+ }
337
+ catch (error) {
338
+ this.debugLog(`❌ Failed to parse JSON response: ${error}`);
339
+ // Fallback: try to extract any pipe-separated lines that might exist
340
+ this.debugLog(`🔄 Attempting fallback to pipe-separated format...`);
341
+ const legacyLines = this.extractLegacyPipeSeparatedLines(content);
342
+ // Convert legacy lines to JSON format for consistency
343
+ const legacySummaries = legacyLines.map((line, index) => {
344
+ const parts = line.split(' | ');
345
+ return {
346
+ text: parts[0] || '',
347
+ document_id: parts[1] || `legacy_${Date.now()}_${index}`,
348
+ keywords: parts[2] || '',
349
+ timestamp: parts[3] || new Date().toISOString(),
350
+ archive: parts[4] || `legacy_${Date.now()}_${index}.jsonl.archive`
351
+ };
352
+ });
353
+ return { overview: null, summaries: legacySummaries };
354
+ }
355
+ }
356
+ /**
357
+ * Legacy fallback for pipe-separated format
358
+ */
359
+ extractLegacyPipeSeparatedLines(content) {
360
+ const lines = content.split('\n');
361
+ const pipeLines = [];
362
+ lines.forEach((line) => {
363
+ const trimmed = line.trim();
364
+ if (trimmed && trimmed.includes(' | ') && trimmed.split(' | ').length >= 3) {
365
+ pipeLines.push(trimmed);
366
+ }
367
+ });
368
+ this.debugLog(`📊 Fallback extracted ${pipeLines.length} pipe-separated lines`);
369
+ return pipeLines;
370
+ }
371
+ // </Block> =======================================
372
+ // <Block> 1.7 ====================================
373
+ // Conversation Formatting - LONG BUT MOSTLY NATURAL (6/10)
374
+ /**
375
+ * Formats conversation messages for analysis prompt
376
+ */
377
+ formatConversationForPrompt(messages) {
378
+ const pipeLines = [];
379
+ messages.forEach((m, index) => {
380
+ const role = m.type === 'assistant' ? 'assistant'
381
+ : m.type === 'user' ? 'user'
382
+ : (m.type === 'result' || m.type === 'system' || m.type === 'summary') ? 'system'
383
+ : m.message?.role || m.role;
384
+ const content = this.extractContent(m);
385
+ const sessionId = m.session_id || '';
386
+ const timestamp = this.normalizeTimestamp(m);
387
+ const messageUuid = m.uuid || '';
388
+ // Escape pipe characters in content to prevent format corruption
389
+ const escapedContent = content.replace(/\|/g, '\\|');
390
+ // Format: content | session_id | role | timestamp | message_uuid
391
+ const pipeLine = `${escapedContent} | ${sessionId} | ${role} | ${timestamp} | ${messageUuid}`;
392
+ pipeLines.push(pipeLine);
393
+ });
394
+ log.debug(`Field filtering complete: ${pipeLines.length} messages processed`);
395
+ return `<!-- TRANSCRIPT -->\n${pipeLines.join('\n')}\n<!-- /TRANSCRIPT -->`;
396
+ }
397
+ // </Block> =======================================
398
+ // <Block> 1.6 ====================================
399
+ // Message Content Extraction - Simplified (8/10)
400
+ /**
401
+ * Extracts content from message object
402
+ */
403
+ extractContent(m) {
404
+ let content = '';
405
+ // Extract by type
406
+ if (m.type === 'assistant' || m.type === 'user') {
407
+ const messageContent = m.message?.content;
408
+ if (Array.isArray(messageContent)) {
409
+ content = messageContent
410
+ .map((item) => item.text || item.content || '')
411
+ .filter(Boolean)
412
+ .join(' ');
413
+ }
414
+ else if (messageContent) {
415
+ content = String(messageContent).trim();
416
+ }
417
+ }
418
+ else if (m.type === 'summary') {
419
+ // Handle summary messages that have a different structure
420
+ content = m.summary || '';
421
+ }
422
+ else if (m.type === 'result') {
423
+ if (m.subtype === 'success' && m.result) {
424
+ content = `[Result: ${m.result}]`;
425
+ }
426
+ else if (m.subtype === 'error_max_turns') {
427
+ content = '[Error: Maximum turns reached]';
428
+ }
429
+ else if (m.subtype === 'error_during_execution') {
430
+ content = '[Error during execution]';
431
+ }
432
+ }
433
+ else if (m.type === 'system') {
434
+ if (m.subtype === 'init') {
435
+ content = `[System initialized: ${m.model}, tools: ${m.tools?.length || 0}, MCP servers: ${m.mcp_servers?.length || 0}]`;
436
+ }
437
+ else {
438
+ // Handle other system messages
439
+ content = String(m.content || '').trim();
440
+ }
441
+ }
442
+ // Fallback to generic content extraction
443
+ if (!content) {
444
+ content = String(m.message?.content || m.content || '');
445
+ if (Array.isArray(content)) {
446
+ content = content
447
+ .map((item) => item.text || item.content || '')
448
+ .filter(Boolean)
449
+ .join(' ');
450
+ }
451
+ }
452
+ // Append tool use result if present
453
+ if (m.toolUseResult) {
454
+ const toolSummary = this.summarizeToolResult(m.toolUseResult, content);
455
+ if (toolSummary) {
456
+ content = content ? `${content}\n\n${toolSummary}` : toolSummary;
457
+ }
458
+ }
459
+ return String(content).trim();
460
+ }
461
+ // </Block> =======================================
462
+ /**
463
+ * Summarizes tool use results
464
+ */
465
+ summarizeToolResult(toolResult, existingContent) {
466
+ const summaryParts = [];
467
+ if (toolResult.stdout) {
468
+ const stdout = String(toolResult.stdout);
469
+ if (stdout.length > 200) {
470
+ const lineCount = stdout.split('\n').length;
471
+ const charCount = stdout.length;
472
+ const lines = stdout.split('\n');
473
+ const preview = lines.slice(0, 3).join('\n');
474
+ const suffix = lines.length > 6 ? `\n...\n${lines.slice(-2).join('\n')}` : '';
475
+ summaryParts.push(`Result: ${preview}${suffix} (${lineCount} lines, ${charCount} chars)`);
476
+ }
477
+ else {
478
+ summaryParts.push(`Result: ${stdout}`);
479
+ }
480
+ }
481
+ if (toolResult.stderr && toolResult.stderr.trim()) {
482
+ summaryParts.push(`Error: ${toolResult.stderr}`);
483
+ }
484
+ if (toolResult.interrupted) {
485
+ summaryParts.push('(interrupted)');
486
+ }
487
+ if (toolResult.isImage) {
488
+ summaryParts.push('(image output)');
489
+ }
490
+ return summaryParts.join('\n');
491
+ }
492
+ /**
493
+ * Normalizes timestamp formats
494
+ */
495
+ normalizeTimestamp(m) {
496
+ const ts = m.timestamp || m.message?.timestamp || m.created_at || m.message?.created_at;
497
+ if (!ts)
498
+ return '';
499
+ try {
500
+ const date = new Date(ts);
501
+ if (isNaN(date.getTime()))
502
+ return '';
503
+ return date.toISOString().replace('T', ' ');
504
+ }
505
+ catch (e) {
506
+ log.debug(`Invalid timestamp format: ${ts}`);
507
+ return '';
508
+ }
509
+ }
510
+ // <Block> 1.8 ====================================
511
+ // Archive Creation - Natural flow (9/10)
512
+ /**
513
+ * Creates an archive file of the original transcript
514
+ */
515
+ createArchive(transcriptPath, projectPrefix, sessionId, content) {
516
+ const projectArchiveDir = this.paths.getProjectArchiveDir(projectPrefix);
517
+ PathResolver.ensureDirectory(projectArchiveDir);
518
+ const archivePath = join(projectArchiveDir, `${sessionId}.jsonl.archive`);
519
+ fs.writeFileSync(archivePath, content);
520
+ log.debug(`📦 Created archive: ${archivePath}`);
521
+ return archivePath;
522
+ }
523
+ // </Block> =======================================
524
+ /**
525
+ * Appends summaries in JSONL format to the index file
526
+ * Each line is a JSON object with type field for easy parsing
527
+ */
528
+ appendToIndex(summaries, overview, projectPrefix, sessionId, messages, archivePath) {
529
+ // Use PathResolver's getIndexPath() for consistency
530
+ const indexPath = this.paths.getIndexPath();
531
+ const indexDir = this.paths.getConfigDir();
532
+ PathResolver.ensureDirectory(indexDir);
533
+ const timestamp = new Date().toISOString();
534
+ // Write session header as JSON object
535
+ const sessionHeader = {
536
+ type: "session",
537
+ session_id: sessionId,
538
+ timestamp: timestamp,
539
+ project: projectPrefix
540
+ };
541
+ fs.appendFileSync(indexPath, JSON.stringify(sessionHeader) + '\n');
542
+ // Add overview as JSON object if present
543
+ if (overview) {
544
+ const overviewObj = {
545
+ type: "overview",
546
+ content: overview,
547
+ session_id: sessionId,
548
+ project: projectPrefix,
549
+ timestamp: timestamp
550
+ };
551
+ fs.appendFileSync(indexPath, JSON.stringify(overviewObj) + '\n');
552
+ }
553
+ // If no summaries from Claude, write diagnostic info
554
+ if (!summaries || summaries.length === 0) {
555
+ log.debug('📝 No summaries extracted from JSON response');
556
+ const diagnosticObj = {
557
+ type: "diagnostic",
558
+ message: "NO SUMMARIES EXTRACTED - Check logs for valid JSON response",
559
+ session_id: sessionId,
560
+ project: projectPrefix,
561
+ timestamp: timestamp
562
+ };
563
+ fs.appendFileSync(indexPath, JSON.stringify(diagnosticObj) + '\n');
564
+ this.debugLog(`⚠️ No summaries for session ${sessionId} - Check if Claude returned valid JSON in <JSONResponse> tags`);
565
+ }
566
+ else {
567
+ // Write each summary as JSONL memory object
568
+ summaries.forEach((summary) => {
569
+ const memoryObj = {
570
+ type: "memory",
571
+ text: summary.text,
572
+ document_id: summary.document_id,
573
+ keywords: summary.keywords,
574
+ session_id: sessionId,
575
+ project: projectPrefix,
576
+ timestamp: summary.timestamp || timestamp,
577
+ archive: path.basename(archivePath)
578
+ };
579
+ fs.appendFileSync(indexPath, JSON.stringify(memoryObj) + '\n');
580
+ });
581
+ log.debug(`📝 Appended ${summaries.length} summaries to index as JSONL`);
582
+ }
583
+ log.debug(`Index path: ${indexPath}`);
584
+ }
585
+ }