@alex900530/claude-persistent-memory 1.0.0

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,151 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse Memory Hook - Result-triggered memory Push
4
+ *
5
+ * How it works:
6
+ * 1. Reads {tool_name, tool_input, tool_response} from stdin
7
+ * 2. Concatenates all context into a query (tool name, file paths, commands, content, output, etc.)
8
+ * 3. Performs vector search via embedding service (TCP)
9
+ * 4. Injects found memories via JSON stdout additionalContext into Claude context
10
+ *
11
+ * Performance budget: < 300ms
12
+ */
13
+
14
+ const net = require('net');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const config = require('../config');
19
+
20
+ const EMBEDDING_PORT = config.embeddingPort;
21
+ const TIMEOUT_MS = config.timeout.hookPostTool;
22
+ const MAX_RESULTS = config.search.maxResults;
23
+ const MIN_SIMILARITY = config.search.minSimilarity;
24
+ const LOG_FILE = path.join(config.logDir, 'hook-inject.log');
25
+
26
+ function output(additionalContext) {
27
+ const result = {};
28
+ if (additionalContext) {
29
+ result.hookSpecificOutput = {
30
+ hookEventName: 'PostToolUse',
31
+ additionalContext,
32
+ };
33
+ }
34
+ console.log(JSON.stringify(result));
35
+ }
36
+
37
+ /**
38
+ * Concatenate tool_input + tool_response fields into a single query string
39
+ */
40
+ function buildQuery(tool, toolInput, toolResponse) {
41
+ const parts = [tool];
42
+ if (toolInput.file_path) parts.push(toolInput.file_path);
43
+ if (toolInput.command) parts.push(toolInput.command);
44
+ if (toolInput.description) parts.push(toolInput.description);
45
+ if (toolInput.old_string) parts.push(toolInput.old_string);
46
+ if (toolInput.new_string) parts.push(toolInput.new_string);
47
+ if (toolInput.content) parts.push(toolInput.content);
48
+ if (toolInput.pattern) parts.push(toolInput.pattern);
49
+ if (toolInput.path) parts.push(toolInput.path);
50
+ if (toolResponse) {
51
+ const responseStr = typeof toolResponse === 'string' ? toolResponse : JSON.stringify(toolResponse);
52
+ parts.push(responseStr.slice(0, 500));
53
+ }
54
+ return parts.join(' ');
55
+ }
56
+
57
+ function searchViaEmbedding(query, limit) {
58
+ return new Promise((resolve) => {
59
+ const socket = new net.Socket();
60
+ let buffer = '';
61
+ let resolved = false;
62
+
63
+ const timer = setTimeout(() => {
64
+ if (!resolved) { resolved = true; socket.destroy(); resolve(null); }
65
+ }, TIMEOUT_MS - 50);
66
+
67
+ socket.connect(EMBEDDING_PORT, '127.0.0.1', () => {
68
+ socket.write(JSON.stringify({ action: 'search', query, limit }) + '\n');
69
+ });
70
+
71
+ socket.on('data', (data) => {
72
+ buffer += data.toString();
73
+ if (buffer.includes('\n') && !resolved) {
74
+ resolved = true;
75
+ clearTimeout(timer);
76
+ socket.destroy();
77
+ try {
78
+ const response = JSON.parse(buffer.split('\n')[0]);
79
+ resolve(response.success ? response.results : null);
80
+ } catch { resolve(null); }
81
+ }
82
+ });
83
+
84
+ socket.on('error', () => {
85
+ if (!resolved) { resolved = true; clearTimeout(timer); resolve(null); }
86
+ });
87
+ });
88
+ }
89
+
90
+ function formatReminder(memories) {
91
+ const lines = ['<memory_context source="post-tool">'];
92
+ for (const m of memories) {
93
+ const content = m.content || m.rawContent || '';
94
+ const sim = m.vectorSimilarity ? m.vectorSimilarity.toFixed(2) : '?';
95
+ lines.push(`[#${m.id} ${m.type || '?'}/${m.domain || '?'} sim=${sim}]`);
96
+ lines.push(content);
97
+ lines.push('');
98
+ }
99
+ lines.push('</memory_context>');
100
+ return lines.join('\n');
101
+ }
102
+
103
+ async function main() {
104
+ let input = '';
105
+ for await (const chunk of process.stdin) { input += chunk; }
106
+
107
+ const failsafe = setTimeout(() => {
108
+ output();
109
+ process.exit(0);
110
+ }, TIMEOUT_MS);
111
+
112
+ try {
113
+ const data = JSON.parse(input.trim());
114
+ const tool = data.tool_name;
115
+ const toolInput = data.tool_input || {};
116
+ const toolResponse = data.tool_response || '';
117
+
118
+ if (!['Bash', 'Edit', 'Write'].includes(tool)) {
119
+ clearTimeout(failsafe);
120
+ output();
121
+ return;
122
+ }
123
+
124
+ const query = buildQuery(tool, toolInput, toolResponse);
125
+ if (!query || query.length < 5) {
126
+ clearTimeout(failsafe);
127
+ output();
128
+ return;
129
+ }
130
+
131
+ const raw = await searchViaEmbedding(query, MAX_RESULTS);
132
+ clearTimeout(failsafe);
133
+ const results = raw ? raw.filter(m => (m.vectorSimilarity || 0) >= MIN_SIMILARITY) : [];
134
+
135
+ if (results.length > 0) {
136
+ const ids = results.map(m => `#${m.id}(${(m.vectorSimilarity||0).toFixed(2)})`).join(' ');
137
+ try { fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} [post-tool] injected: ${ids}\n`); } catch {}
138
+ output(formatReminder(results));
139
+ } else {
140
+ output();
141
+ }
142
+ } catch {
143
+ clearTimeout(failsafe);
144
+ output();
145
+ }
146
+ }
147
+
148
+ main().catch(() => {
149
+ output();
150
+ process.exit(0);
151
+ });
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreCompact Hook - Async session analysis
4
+ *
5
+ * Simplified: immediately outputs stdin (does not block compact),
6
+ * then spawns compact-analyzer in the background for LLM analysis.
7
+ * All analysis logic is in compact-analyzer.js, handled by LLM in full.
8
+ */
9
+
10
+ const { spawn } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+
14
+ const config = require('../config');
15
+
16
+ const LOG_FILE = path.join(config.logDir, 'compact-analyzer.log');
17
+
18
+ function log(msg) {
19
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
20
+ try { fs.appendFileSync(LOG_FILE, line); } catch (e) {}
21
+ }
22
+
23
+ async function main() {
24
+ let input = '';
25
+ for await (const chunk of process.stdin) {
26
+ input += chunk;
27
+ }
28
+
29
+ // Output immediately, do not block compact
30
+ console.log(input);
31
+
32
+ try {
33
+ const data = JSON.parse(input.trim());
34
+ const transcriptPath = data.transcript_path;
35
+ const sessionId = data.session_id || 'unknown';
36
+ const cwd = data.cwd || process.cwd();
37
+
38
+ if (!transcriptPath) return;
39
+
40
+ // Spawn analyzer in background
41
+ const analyzerPath = path.join(__dirname, '..', 'lib', 'compact-analyzer.js');
42
+ const child = spawn('node', [analyzerPath, transcriptPath, sessionId, cwd], {
43
+ detached: true,
44
+ stdio: 'ignore',
45
+ env: process.env
46
+ });
47
+ child.unref();
48
+
49
+ log(`[PreCompact] Spawned analyzer (PID ${child.pid}) for session ${sessionId}`);
50
+ } catch (e) {
51
+ log(`[PreCompact] Error: ${e.message}`);
52
+ }
53
+ }
54
+
55
+ // Preserve loadMessagesFromTranscript export (backward compatibility)
56
+ const { loadMessagesFromTranscript } = require('../lib/compact-analyzer');
57
+ module.exports = { loadMessagesFromTranscript };
58
+
59
+ if (require.main === module) {
60
+ main().catch(() => {});
61
+ }
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse Memory Hook - Action-triggered memory Push
4
+ *
5
+ * How it works:
6
+ * 1. Reads {tool_name, tool_input} from stdin
7
+ * 2. Concatenates all context into a query (tool name, file paths, commands, content, etc.)
8
+ * 3. Performs vector search via embedding service (TCP)
9
+ * 4. Injects found memories via JSON stdout additionalContext into Claude context
10
+ *
11
+ * Performance budget: < 300ms
12
+ */
13
+
14
+ const net = require('net');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const config = require('../config');
19
+
20
+ const EMBEDDING_PORT = config.embeddingPort;
21
+ const TIMEOUT_MS = config.timeout.hookPreTool;
22
+ const MAX_RESULTS = config.search.maxResults;
23
+ const MIN_SIMILARITY = config.search.minSimilarity;
24
+ const LOG_FILE = path.join(config.logDir, 'hook-inject.log');
25
+
26
+ function output(additionalContext) {
27
+ const result = {
28
+ hookSpecificOutput: {
29
+ hookEventName: 'PreToolUse',
30
+ permissionDecision: 'allow',
31
+ }
32
+ };
33
+ if (additionalContext) {
34
+ result.hookSpecificOutput.additionalContext = additionalContext;
35
+ }
36
+ console.log(JSON.stringify(result));
37
+ }
38
+
39
+ /**
40
+ * Concatenate all tool_input fields into a single query string
41
+ */
42
+ function buildQuery(tool, toolInput) {
43
+ const parts = [tool];
44
+ if (toolInput.file_path) parts.push(toolInput.file_path);
45
+ if (toolInput.command) parts.push(toolInput.command);
46
+ if (toolInput.description) parts.push(toolInput.description);
47
+ if (toolInput.old_string) parts.push(toolInput.old_string);
48
+ if (toolInput.new_string) parts.push(toolInput.new_string);
49
+ if (toolInput.content) parts.push(toolInput.content);
50
+ if (toolInput.pattern) parts.push(toolInput.pattern);
51
+ if (toolInput.path) parts.push(toolInput.path);
52
+ return parts.join(' ');
53
+ }
54
+
55
+ function searchViaEmbedding(query, limit) {
56
+ return new Promise((resolve) => {
57
+ const socket = new net.Socket();
58
+ let buffer = '';
59
+ let resolved = false;
60
+
61
+ const timer = setTimeout(() => {
62
+ if (!resolved) { resolved = true; socket.destroy(); resolve(null); }
63
+ }, TIMEOUT_MS - 50);
64
+
65
+ socket.connect(EMBEDDING_PORT, '127.0.0.1', () => {
66
+ socket.write(JSON.stringify({ action: 'search', query, limit }) + '\n');
67
+ });
68
+
69
+ socket.on('data', (data) => {
70
+ buffer += data.toString();
71
+ if (buffer.includes('\n') && !resolved) {
72
+ resolved = true;
73
+ clearTimeout(timer);
74
+ socket.destroy();
75
+ try {
76
+ const response = JSON.parse(buffer.split('\n')[0]);
77
+ resolve(response.success ? response.results : null);
78
+ } catch { resolve(null); }
79
+ }
80
+ });
81
+
82
+ socket.on('error', () => {
83
+ if (!resolved) { resolved = true; clearTimeout(timer); resolve(null); }
84
+ });
85
+ });
86
+ }
87
+
88
+ function formatReminder(memories) {
89
+ const lines = ['<memory_context source="pre-tool">'];
90
+ for (const m of memories) {
91
+ const content = m.content || m.rawContent || '';
92
+ const sim = m.vectorSimilarity ? m.vectorSimilarity.toFixed(2) : '?';
93
+ lines.push(`[#${m.id} ${m.type || '?'}/${m.domain || '?'} sim=${sim}]`);
94
+ lines.push(content);
95
+ lines.push('');
96
+ }
97
+ lines.push('</memory_context>');
98
+ return lines.join('\n');
99
+ }
100
+
101
+ async function main() {
102
+ let input = '';
103
+ for await (const chunk of process.stdin) { input += chunk; }
104
+
105
+ const failsafe = setTimeout(() => {
106
+ output();
107
+ process.exit(0);
108
+ }, TIMEOUT_MS);
109
+
110
+ try {
111
+ const data = JSON.parse(input.trim());
112
+ const tool = data.tool_name;
113
+ const toolInput = data.tool_input || {};
114
+
115
+ if (!['Edit', 'Write', 'Bash'].includes(tool)) {
116
+ clearTimeout(failsafe);
117
+ output();
118
+ return;
119
+ }
120
+
121
+ const query = buildQuery(tool, toolInput);
122
+ if (!query || query.length < 5) {
123
+ clearTimeout(failsafe);
124
+ output();
125
+ return;
126
+ }
127
+
128
+ const raw = await searchViaEmbedding(query, MAX_RESULTS);
129
+ clearTimeout(failsafe);
130
+ const results = raw ? raw.filter(m => (m.vectorSimilarity || 0) >= MIN_SIMILARITY) : [];
131
+
132
+ if (results.length > 0) {
133
+ const ids = results.map(m => `#${m.id}(${(m.vectorSimilarity||0).toFixed(2)})`).join(' ');
134
+ try { fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} [pre-tool] injected: ${ids}\n`); } catch {}
135
+ output(formatReminder(results));
136
+ } else {
137
+ output();
138
+ }
139
+ } catch {
140
+ clearTimeout(failsafe);
141
+ output();
142
+ }
143
+ }
144
+
145
+ main().catch(() => {
146
+ output();
147
+ process.exit(0);
148
+ });
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionEnd Hook - Incremental transcript analysis + clustering + mature cluster merging
4
+ *
5
+ * Features:
6
+ * - Incremental transcript analysis: extract error patterns, code changes, user preferences
7
+ * - Process unclustered memories, attempt to create new clusters
8
+ * - Mature cluster memories automatically merged into a single high-confidence memory
9
+ */
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+
14
+ const config = require('../config');
15
+ const { log } = require('../lib/utils');
16
+
17
+ const DATA_DIR = config.dataDir;
18
+
19
+ // Clustering configuration
20
+ const MIN_CLUSTER_CONFIDENCE = 0.6;
21
+
22
+ let memoryDb = null;
23
+
24
+ function getMemoryDb() {
25
+ if (memoryDb) return memoryDb;
26
+ try {
27
+ memoryDb = require('../lib/memory-db');
28
+ return memoryDb;
29
+ } catch (e) {
30
+ log(`[SessionEnd] Warning: memory-db not available: ${e.message}`);
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Incremental transcript analysis: from last compact position to session end
37
+ */
38
+ async function analyzeTranscriptIncremental(sessionId, transcriptPath) {
39
+ const db = getMemoryDb();
40
+ if (!db || !transcriptPath || !sessionId) return;
41
+
42
+ try {
43
+ const { analyzeAndSave } = require('../lib/compact-analyzer');
44
+
45
+ const result = await analyzeAndSave(transcriptPath, sessionId, { memoryDb: db });
46
+ log(`[SessionEnd] Transcript analysis: saved ${result.saved || 0} memories`);
47
+
48
+ // Clean up state file (session ended, no longer needed)
49
+ const stateFile = path.join(DATA_DIR, `compact-state-${sessionId}.json`);
50
+ try {
51
+ if (fs.existsSync(stateFile)) {
52
+ fs.unlinkSync(stateFile);
53
+ log(`[SessionEnd] Cleaned up state file`);
54
+ }
55
+ } catch (e) {}
56
+ } catch (e) {
57
+ log(`[SessionEnd] Error in analyzeTranscriptIncremental: ${e.message}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Process unclustered memories, attempt to create new clusters
63
+ */
64
+ async function processUnclusteredMemories() {
65
+ const db = getMemoryDb();
66
+ if (!db) return;
67
+
68
+ try {
69
+ log('[SessionEnd] Processing unclustered memories...');
70
+
71
+ const result = await db.autoCluster({
72
+ minConfidence: MIN_CLUSTER_CONFIDENCE,
73
+ hoursBack: 24
74
+ });
75
+
76
+ if (result.length === 0) {
77
+ log('[SessionEnd] No new clusters created');
78
+ return;
79
+ }
80
+
81
+ for (const c of result) {
82
+ log(`[SessionEnd] Created new cluster: ${c.theme} (${c.memberCount} members, ${c.status})`);
83
+ }
84
+ } catch (e) {
85
+ log(`[SessionEnd] Error in processUnclusteredMemories: ${e.message}`);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Mature cluster memory merging: multiple memories merged into one high-confidence memory
91
+ */
92
+ async function mergeMatureClusters() {
93
+ const db = getMemoryDb();
94
+ if (!db) return;
95
+
96
+ try {
97
+ const matureClusters = db.getMatureClusters();
98
+ if (matureClusters.length === 0) {
99
+ log('[SessionEnd] No mature clusters to merge');
100
+ return;
101
+ }
102
+
103
+ for (const cluster of matureClusters) {
104
+ const result = await db.mergeClusterMemories(cluster.id);
105
+ if (result) {
106
+ log(`[SessionEnd] Merged cluster #${cluster.id}: "${result.summary}" (${result.memberCount} memories -> ID ${result.memoryId})`);
107
+ }
108
+ }
109
+ } catch (e) {
110
+ log(`[SessionEnd] Error in mergeMatureClusters: ${e.message}`);
111
+ }
112
+ }
113
+
114
+ async function main() {
115
+ log('[SessionEnd] Processing session end...');
116
+
117
+ // Read stdin for session data
118
+ let sessionData = {};
119
+ try {
120
+ let input = '';
121
+ for await (const chunk of process.stdin) {
122
+ input += chunk;
123
+ }
124
+ if (input.trim()) {
125
+ sessionData = JSON.parse(input);
126
+ }
127
+ } catch (e) {
128
+ log(`[SessionEnd] Failed to parse stdin: ${e.message}`);
129
+ }
130
+
131
+ const sessionId = sessionData.session_id;
132
+ const transcriptPath = sessionData.transcript_path;
133
+
134
+ log(`[SessionEnd] Session: ${sessionId || 'unknown'}, transcript: ${transcriptPath || 'none'}`);
135
+
136
+ // 1. Incremental transcript analysis (from last compact position to session end)
137
+ if (sessionId && transcriptPath) {
138
+ await analyzeTranscriptIncremental(sessionId, transcriptPath);
139
+ }
140
+
141
+ // 2. Process unclustered memories, attempt to create new clusters
142
+ await processUnclusteredMemories();
143
+
144
+ // 3. Mature cluster memory merging
145
+ await mergeMatureClusters();
146
+
147
+ // Close database
148
+ if (memoryDb) {
149
+ memoryDb.closeDb();
150
+ }
151
+
152
+ log('[SessionEnd] Session end processing complete');
153
+ process.exit(0);
154
+ }
155
+
156
+ main().catch(err => {
157
+ console.error('[SessionEnd] Error:', err.message);
158
+ process.exit(0);
159
+ });
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * User Prompt Hook - Memory retrieval on user prompt
4
+ *
5
+ * Stripped-down version: only performs memory search via embedding service.
6
+ * No compact suggestion logic (that is an optional add-on, not part of core memory).
7
+ *
8
+ * How it works:
9
+ * 1. Reads raw user prompt from stdin
10
+ * 2. Extracts the actual prompt text
11
+ * 3. Searches memories via embedding service (TCP)
12
+ * 4. Outputs memory context + original prompt via stdout
13
+ */
14
+
15
+ const net = require('net');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const config = require('../config');
20
+
21
+ const EMBEDDING_PORT = config.embeddingPort;
22
+ const TIMEOUT_MS = config.timeout.hookUserPrompt;
23
+ const SEARCH_TIMEOUT_MS = config.timeout.embeddingSearch;
24
+ const MAX_RESULTS = config.search.maxResults;
25
+ const MIN_SIMILARITY = config.search.minSimilarity;
26
+ const LOG_FILE = path.join(config.logDir, 'hook-inject.log');
27
+
28
+ /**
29
+ * Extract the actual user prompt from raw stdin input.
30
+ * The input may be wrapped in JSON with a "prompt" field.
31
+ */
32
+ function extractUserPrompt(rawInput) {
33
+ try {
34
+ const jsonMatch = rawInput.match(/\{[\s\S]*"prompt"\s*:\s*"[\s\S]*"\s*[\s\S]*\}$/);
35
+ if (jsonMatch) {
36
+ const parsed = JSON.parse(jsonMatch[0]);
37
+ if (parsed.prompt) return parsed.prompt;
38
+ }
39
+ } catch (e) {}
40
+ return rawInput;
41
+ }
42
+
43
+ // --- Embedding memory search ---
44
+
45
+ function searchViaEmbedding(query, limit) {
46
+ return new Promise((resolve) => {
47
+ const socket = new net.Socket();
48
+ let buffer = '';
49
+ let resolved = false;
50
+
51
+ const timer = setTimeout(() => {
52
+ if (!resolved) { resolved = true; socket.destroy(); resolve(null); }
53
+ }, SEARCH_TIMEOUT_MS);
54
+
55
+ socket.connect(EMBEDDING_PORT, '127.0.0.1', () => {
56
+ socket.write(JSON.stringify({ action: 'search', query, limit }) + '\n');
57
+ });
58
+
59
+ socket.on('data', (data) => {
60
+ buffer += data.toString();
61
+ if (buffer.includes('\n') && !resolved) {
62
+ resolved = true;
63
+ clearTimeout(timer);
64
+ socket.destroy();
65
+ try {
66
+ const response = JSON.parse(buffer.split('\n')[0]);
67
+ resolve(response.success ? response.results : null);
68
+ } catch { resolve(null); }
69
+ }
70
+ });
71
+
72
+ socket.on('error', () => {
73
+ if (!resolved) { resolved = true; clearTimeout(timer); resolve(null); }
74
+ });
75
+ });
76
+ }
77
+
78
+ function formatReminder(memories) {
79
+ const lines = ['<memory_context source="user-prompt">'];
80
+ for (const m of memories) {
81
+ const content = m.content || m.rawContent || '';
82
+ const sim = m.vectorSimilarity ? m.vectorSimilarity.toFixed(2) : '?';
83
+ lines.push(`[#${m.id} ${m.type || '?'}/${m.domain || '?'} sim=${sim}]`);
84
+ lines.push(content);
85
+ lines.push('');
86
+ }
87
+ lines.push('</memory_context>');
88
+ return lines.join('\n');
89
+ }
90
+
91
+ // --- Main ---
92
+
93
+ async function main() {
94
+ let userMessage = '';
95
+ for await (const chunk of process.stdin) {
96
+ userMessage += chunk;
97
+ }
98
+ userMessage = userMessage.trim();
99
+
100
+ let resolved = false;
101
+ const timer = setTimeout(() => {
102
+ if (!resolved) {
103
+ resolved = true;
104
+ console.log(userMessage);
105
+ process.exit(0);
106
+ }
107
+ }, TIMEOUT_MS);
108
+
109
+ try {
110
+ if (!userMessage || userMessage.length < 10) {
111
+ clearTimeout(timer);
112
+ if (!resolved) { resolved = true; console.log(userMessage); }
113
+ return;
114
+ }
115
+
116
+ const actualPrompt = extractUserPrompt(userMessage);
117
+
118
+ // Embedding memory search: use the full user prompt as query
119
+ const query = actualPrompt;
120
+ let memoryContext = '';
121
+ if (query.length >= 5) {
122
+ const raw = await searchViaEmbedding(query, MAX_RESULTS);
123
+ const results = raw ? raw.filter(m => (m.vectorSimilarity || 0) >= MIN_SIMILARITY) : [];
124
+ if (results.length > 0) {
125
+ const ids = results.map(m => `#${m.id}(${(m.vectorSimilarity||0).toFixed(2)})`).join(' ');
126
+ try { fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} [user-prompt] injected: ${ids}\n`); } catch {}
127
+ memoryContext = formatReminder(results);
128
+ }
129
+ }
130
+
131
+ clearTimeout(timer);
132
+ if (resolved) return;
133
+ resolved = true;
134
+
135
+ let output = '';
136
+ if (memoryContext) {
137
+ output += memoryContext + '\n\n';
138
+ }
139
+
140
+ console.log(output + userMessage);
141
+ } catch (e) {
142
+ clearTimeout(timer);
143
+ if (!resolved) { resolved = true; console.log(userMessage); }
144
+ }
145
+ }
146
+
147
+ main().catch(() => {
148
+ let data = '';
149
+ process.stdin.on('data', chunk => data += chunk);
150
+ process.stdin.on('end', () => console.log(data));
151
+ });