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.
- package/README.md +183 -46
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +179 -0
- package/dist/commands/compress.d.ts +2 -0
- package/dist/commands/compress.js +27 -0
- package/dist/commands/hooks.d.ts +19 -0
- package/dist/commands/hooks.js +131 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +836 -0
- package/dist/commands/load-context.d.ts +2 -0
- package/dist/commands/load-context.js +151 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +76 -0
- package/dist/commands/migrate-to-jsonl.d.ts +5 -0
- package/dist/commands/migrate-to-jsonl.js +99 -0
- package/dist/commands/restore.d.ts +1 -0
- package/dist/commands/restore.js +20 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +136 -0
- package/dist/commands/trash-empty.d.ts +3 -0
- package/dist/commands/trash-empty.js +56 -0
- package/dist/commands/trash-view.d.ts +1 -0
- package/dist/commands/trash-view.js +101 -0
- package/dist/commands/trash.d.ts +6 -0
- package/dist/commands/trash.js +49 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +107 -0
- package/dist/constants.d.ts +271 -0
- package/dist/constants.js +199 -0
- package/dist/core/compression/TranscriptCompressor.d.ts +79 -0
- package/dist/core/compression/TranscriptCompressor.js +585 -0
- package/dist/core/orchestration/PromptOrchestrator.d.ts +165 -0
- package/dist/core/orchestration/PromptOrchestrator.js +182 -0
- package/dist/lib/time-utils.d.ts +5 -0
- package/dist/lib/time-utils.js +70 -0
- package/dist/prompts/constants.d.ts +126 -0
- package/dist/prompts/constants.js +161 -0
- package/dist/prompts/index.d.ts +10 -0
- package/dist/prompts/index.js +11 -0
- package/dist/prompts/templates/analysis/AnalysisTemplates.d.ts +13 -0
- package/dist/prompts/templates/analysis/AnalysisTemplates.js +94 -0
- package/dist/prompts/templates/context/ContextTemplates.d.ts +119 -0
- package/dist/prompts/templates/context/ContextTemplates.js +399 -0
- package/dist/prompts/templates/hooks/HookTemplates.d.ts +175 -0
- package/dist/prompts/templates/hooks/HookTemplates.js +394 -0
- package/dist/prompts/templates/hooks/HookTemplates.test.d.ts +7 -0
- package/dist/prompts/templates/hooks/HookTemplates.test.js +127 -0
- package/dist/shared/config.d.ts +4 -0
- package/dist/shared/config.js +41 -0
- package/dist/shared/error-handler.d.ts +22 -0
- package/dist/shared/error-handler.js +142 -0
- package/dist/shared/logger.d.ts +19 -0
- package/dist/shared/logger.js +51 -0
- package/dist/shared/paths.d.ts +28 -0
- package/dist/shared/paths.js +100 -0
- package/dist/shared/settings.d.ts +41 -0
- package/dist/shared/settings.js +81 -0
- package/dist/shared/types.d.ts +145 -0
- package/dist/shared/types.js +78 -0
- package/docs/STATUS.md +155 -0
- package/docs/chroma-backend-migration.md +161 -0
- package/docs/landing-page-outline.md +287 -0
- package/docs/multi-platform-builds.md +96 -0
- package/docs/plans/cloud-service-plan.md +274 -0
- package/docs/plans/fix-response-format-issue.md +61 -0
- package/docs/plans/restructure-session-hook-output.md +102 -0
- package/docs/plans/session-start-hook-investigation.md +45 -0
- package/docs/plans/src-reorganization-plan.md +181 -0
- package/docs/plans/terminal-effects-decision.md +22 -0
- package/docs/plans/terminal-effects-integration.md +82 -0
- package/docs/plans/trash-bin-feature-plan.md +240 -0
- package/docs/plans/trash-bin-minimal-plan.md +102 -0
- package/docs/reference/bun-single-executable.md +584 -0
- package/docs/reference/cc-output-styles.md +99 -0
- package/docs/reference/chroma-mcp-project-memory-example.md +80 -0
- package/docs/reference/chroma-mcp-team-example.md +92 -0
- package/docs/reference/claude-code/cc-hooks.md +787 -0
- package/docs/reference/claude-code/cc-status-line.md +202 -0
- package/docs/reference/claude-code/hook-configuration.md +173 -0
- package/docs/reference/claude-code/hook-responses.md +127 -0
- package/docs/reference/claude-code/hooks.md +175 -0
- package/docs/reference/claude-code/mcp-configuration.md +133 -0
- package/docs/reference/claude-code/session-start-hook.md +82 -0
- package/docs/reference/load-context-format-example.md +33 -0
- package/docs/reference/mcp-sdk/mcp-typescript-sdk-readme.md +1323 -0
- package/docs/reference/mcp-sdk/server-implementation.md +286 -0
- package/docs/reference/mcp-sdk/stdio-transport.md +345 -0
- package/docs/todos/fix-response-format-tasks.md +43 -0
- package/docs/todos/implementation-todos.md +280 -0
- package/docs/todos/restructure-hook-output-tasks.md +103 -0
- package/docs/todos/session-start-hook-fix.md +26 -0
- package/docs/todos/terminal-effects-tasks.md +42 -0
- package/docs/todos/trash-bin-implementation-todos.md +348 -0
- package/docs/todos/trash-bin-minimal-todos.md +27 -0
- package/package.json +56 -6
- 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
|
+
}
|