claude-memory-agent 2.2.3 → 3.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.
- package/bin/cli.js +312 -136
- package/config.py +6 -0
- package/hooks/auto_capture.py +58 -1
- package/hooks/grounding-hook-v2.py +129 -0
- package/hooks/grounding-hook.py +95 -0
- package/hooks/session_end_hook.py +35 -0
- package/hooks/session_start.py +56 -0
- package/main.py +411 -1
- package/mcp_proxy.py +307 -0
- package/mcp_server.py +54 -0
- package/mcp_server_full.py +497 -0
- package/package.json +1 -1
- package/services/database.py +306 -0
- package/services/native_memory_sync.py +66 -310
- package/services/session_awareness.py +181 -0
package/bin/cli.js
CHANGED
|
@@ -3,158 +3,374 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Claude Memory Agent CLI
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Cross-platform entry point that manages the Python memory agent server.
|
|
7
7
|
* Usage:
|
|
8
8
|
* claude-memory-agent install - Run installation wizard
|
|
9
|
-
* claude-memory-agent start - Start the agent
|
|
10
|
-
* claude-memory-agent stop - Stop the agent
|
|
11
|
-
* claude-memory-agent status - Check
|
|
12
|
-
* claude-memory-agent dashboard - Open dashboard
|
|
9
|
+
* claude-memory-agent start - Start the agent in background
|
|
10
|
+
* claude-memory-agent stop - Stop the running agent
|
|
11
|
+
* claude-memory-agent status - Check if agent is running
|
|
12
|
+
* claude-memory-agent dashboard - Open the web dashboard
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const { spawn, execSync } = require('child_process');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const fs = require('fs');
|
|
18
|
+
const http = require('http');
|
|
18
19
|
|
|
19
20
|
const AGENT_DIR = path.dirname(__dirname);
|
|
21
|
+
const PID_FILE = path.join(AGENT_DIR, 'memory-agent.pid');
|
|
22
|
+
const LOG_FILE = path.join(AGENT_DIR, 'memory-agent.log');
|
|
23
|
+
const ENV_FILE = path.join(AGENT_DIR, '.env');
|
|
20
24
|
const args = process.argv.slice(2);
|
|
21
25
|
const command = args[0] || 'help';
|
|
22
26
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Read PORT from .env file, default 8102 */
|
|
32
|
+
function getPort() {
|
|
33
|
+
try {
|
|
34
|
+
const env = fs.readFileSync(ENV_FILE, 'utf8');
|
|
35
|
+
const match = env.match(/^PORT=(\d+)/m);
|
|
36
|
+
if (match) return parseInt(match[1], 10);
|
|
37
|
+
} catch (_) {}
|
|
38
|
+
return parseInt(process.env.PORT || '8102', 10);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read HOST from .env file, default 127.0.0.1 */
|
|
42
|
+
function getHost() {
|
|
43
|
+
try {
|
|
44
|
+
const env = fs.readFileSync(ENV_FILE, 'utf8');
|
|
45
|
+
const match = env.match(/^HOST=(.+)/m);
|
|
46
|
+
if (match) {
|
|
47
|
+
const h = match[1].trim();
|
|
48
|
+
return (h === '0.0.0.0') ? 'localhost' : h;
|
|
49
|
+
}
|
|
50
|
+
} catch (_) {}
|
|
51
|
+
return 'localhost';
|
|
52
|
+
}
|
|
26
53
|
|
|
27
|
-
|
|
54
|
+
/** Detect Python 3 command */
|
|
55
|
+
function getPython() {
|
|
56
|
+
const commands = ['python3', 'python', 'py'];
|
|
57
|
+
for (const cmd of commands) {
|
|
28
58
|
try {
|
|
29
|
-
const
|
|
59
|
+
const out = execSync(`${cmd} --version`, {
|
|
30
60
|
encoding: 'utf8',
|
|
31
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
timeout: 5000,
|
|
32
63
|
});
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
} catch (e) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
64
|
+
if (out && out.includes('Python 3')) return cmd;
|
|
65
|
+
} catch (_) {}
|
|
39
66
|
}
|
|
40
67
|
return null;
|
|
41
68
|
}
|
|
42
69
|
|
|
43
|
-
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
/** Require Python or exit */
|
|
71
|
+
function requirePython() {
|
|
72
|
+
const py = getPython();
|
|
73
|
+
if (!py) {
|
|
74
|
+
console.error('Error: Python 3 is required but not found.');
|
|
75
|
+
console.error('Install from: https://python.org/');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
return py;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Run a Python script with inherited stdio */
|
|
82
|
+
function runPython(script, scriptArgs = []) {
|
|
83
|
+
const python = requirePython();
|
|
84
|
+
const proc = spawn(python, [script, ...scriptArgs], {
|
|
85
|
+
cwd: AGENT_DIR,
|
|
86
|
+
stdio: 'inherit',
|
|
87
|
+
shell: process.platform === 'win32',
|
|
88
|
+
});
|
|
89
|
+
proc.on('close', (code) => process.exit(code || 0));
|
|
90
|
+
proc.on('error', (err) => {
|
|
91
|
+
console.error('Failed to start:', err.message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** HTTP GET with timeout, resolves { ok, status, body } */
|
|
97
|
+
function httpGet(url, timeout = 3000) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const req = http.get(url, { timeout }, (res) => {
|
|
100
|
+
let body = '';
|
|
101
|
+
res.on('data', (c) => (body += c));
|
|
102
|
+
res.on('end', () => resolve({ ok: res.statusCode === 200, status: res.statusCode, body }));
|
|
49
103
|
});
|
|
104
|
+
req.on('error', () => resolve({ ok: false, status: 0, body: '' }));
|
|
105
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, status: 0, body: '' }); });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Check if the agent is responding on its health endpoint */
|
|
110
|
+
async function isAgentRunning() {
|
|
111
|
+
const port = getPort();
|
|
112
|
+
const host = getHost();
|
|
113
|
+
const { ok } = await httpGet(`http://${host}:${port}/health`);
|
|
114
|
+
return ok;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Read PID from file */
|
|
118
|
+
function readPid() {
|
|
119
|
+
try {
|
|
120
|
+
if (fs.existsSync(PID_FILE)) {
|
|
121
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
122
|
+
if (!isNaN(pid)) return pid;
|
|
123
|
+
}
|
|
124
|
+
} catch (_) {}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Check if a process with given PID is alive */
|
|
129
|
+
function isProcessAlive(pid) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
50
132
|
return true;
|
|
51
|
-
} catch (
|
|
133
|
+
} catch (_) {
|
|
52
134
|
return false;
|
|
53
135
|
}
|
|
54
136
|
}
|
|
55
137
|
|
|
56
|
-
|
|
57
|
-
function
|
|
58
|
-
console.log('Installing Python dependencies...');
|
|
138
|
+
/** Kill a process by PID (cross-platform) */
|
|
139
|
+
function killProcess(pid) {
|
|
59
140
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
141
|
+
if (process.platform === 'win32') {
|
|
142
|
+
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'pipe' });
|
|
143
|
+
} else {
|
|
144
|
+
process.kill(pid, 'SIGTERM');
|
|
145
|
+
}
|
|
64
146
|
return true;
|
|
65
|
-
} catch (
|
|
66
|
-
console.error('Failed to install dependencies:', e.message);
|
|
147
|
+
} catch (_) {
|
|
67
148
|
return false;
|
|
68
149
|
}
|
|
69
150
|
}
|
|
70
151
|
|
|
71
|
-
|
|
72
|
-
function
|
|
73
|
-
const
|
|
152
|
+
/** Open a URL in the default browser */
|
|
153
|
+
function openBrowser(url) {
|
|
154
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
155
|
+
: process.platform === 'win32' ? 'start'
|
|
156
|
+
: 'xdg-open';
|
|
157
|
+
try {
|
|
158
|
+
execSync(`${cmd} ${url}`, { stdio: 'ignore', shell: true });
|
|
159
|
+
} catch (_) {
|
|
160
|
+
console.log(`Open in browser: ${url}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
74
163
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Commands
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
async function cmdStart() {
|
|
169
|
+
// Already running?
|
|
170
|
+
if (await isAgentRunning()) {
|
|
171
|
+
console.log('Memory agent is already running.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const python = requirePython();
|
|
176
|
+
const mainPy = path.join(AGENT_DIR, 'main.py');
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(mainPy)) {
|
|
179
|
+
console.error('Error: main.py not found in ' + AGENT_DIR);
|
|
78
180
|
process.exit(1);
|
|
79
181
|
}
|
|
80
182
|
|
|
81
|
-
|
|
183
|
+
console.log('Starting Memory Agent...');
|
|
184
|
+
|
|
185
|
+
const logFd = fs.openSync(LOG_FILE, 'w');
|
|
186
|
+
const spawnOpts = {
|
|
82
187
|
cwd: AGENT_DIR,
|
|
83
|
-
stdio: '
|
|
84
|
-
|
|
85
|
-
|
|
188
|
+
stdio: ['ignore', logFd, logFd],
|
|
189
|
+
detached: true,
|
|
190
|
+
shell: process.platform === 'win32',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Windows needs special flags
|
|
194
|
+
if (process.platform === 'win32') {
|
|
195
|
+
spawnOpts.windowsHide = true;
|
|
196
|
+
}
|
|
86
197
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
198
|
+
const child = spawn(python, [mainPy], spawnOpts);
|
|
199
|
+
child.unref();
|
|
90
200
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
201
|
+
// Write PID
|
|
202
|
+
fs.writeFileSync(PID_FILE, String(child.pid), 'utf8');
|
|
203
|
+
|
|
204
|
+
// Wait up to 8 seconds for health check
|
|
205
|
+
for (let i = 0; i < 16; i++) {
|
|
206
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
207
|
+
if (await isAgentRunning()) {
|
|
208
|
+
const port = getPort();
|
|
209
|
+
console.log(`Memory agent started (PID: ${child.pid}, port: ${port})`);
|
|
210
|
+
fs.closeSync(logFd);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fs.closeSync(logFd);
|
|
216
|
+
console.log(`Agent started (PID: ${child.pid}) but health check not responding yet.`);
|
|
217
|
+
console.log(`Check logs: claude-memory-agent logs`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function cmdStop() {
|
|
221
|
+
const pid = readPid();
|
|
222
|
+
|
|
223
|
+
if (!pid) {
|
|
224
|
+
// Try health check anyway
|
|
225
|
+
if (await isAgentRunning()) {
|
|
226
|
+
console.log('Agent is running but no PID file found. Cannot stop automatically.');
|
|
227
|
+
console.log('Find the process manually and kill it.');
|
|
228
|
+
} else {
|
|
229
|
+
console.log('Memory agent is not running.');
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!isProcessAlive(pid)) {
|
|
235
|
+
console.log('Memory agent is not running (stale PID file).');
|
|
236
|
+
try { fs.unlinkSync(PID_FILE); } catch (_) {}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(`Stopping Memory Agent (PID: ${pid})...`);
|
|
241
|
+
if (killProcess(pid)) {
|
|
242
|
+
// Wait for it to die
|
|
243
|
+
for (let i = 0; i < 10; i++) {
|
|
244
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
245
|
+
if (!isProcessAlive(pid)) {
|
|
246
|
+
console.log('Memory agent stopped.');
|
|
247
|
+
try { fs.unlinkSync(PID_FILE); } catch (_) {}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
console.log('Sent kill signal but process may still be shutting down.');
|
|
252
|
+
} else {
|
|
253
|
+
console.log('Failed to stop agent. Try killing PID ' + pid + ' manually.');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function cmdStatus() {
|
|
258
|
+
const port = getPort();
|
|
259
|
+
const host = getHost();
|
|
260
|
+
const pid = readPid();
|
|
261
|
+
const running = await isAgentRunning();
|
|
262
|
+
|
|
263
|
+
if (running) {
|
|
264
|
+
console.log(`Memory agent is RUNNING`);
|
|
265
|
+
console.log(` URL: http://${host}:${port}`);
|
|
266
|
+
if (pid) console.log(` PID: ${pid}`);
|
|
267
|
+
} else {
|
|
268
|
+
console.log('Memory agent is NOT running.');
|
|
269
|
+
if (pid && isProcessAlive(pid)) {
|
|
270
|
+
console.log(` PID ${pid} exists but health check failed.`);
|
|
271
|
+
console.log(' Check logs: claude-memory-agent logs');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function cmdDashboard() {
|
|
277
|
+
const port = getPort();
|
|
278
|
+
const host = getHost();
|
|
279
|
+
const url = `http://${host}:${port}/dashboard`;
|
|
280
|
+
|
|
281
|
+
if (!(await isAgentRunning())) {
|
|
282
|
+
console.log('Memory agent is not running. Start it first:');
|
|
283
|
+
console.log(' claude-memory-agent start');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(`Opening dashboard: ${url}`);
|
|
288
|
+
openBrowser(url);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function cmdLogs() {
|
|
292
|
+
if (!fs.existsSync(LOG_FILE)) {
|
|
293
|
+
console.log('No log file found. Start the agent first.');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const content = fs.readFileSync(LOG_FILE, 'utf8');
|
|
297
|
+
const lines = content.split('\n');
|
|
298
|
+
// Show last 50 lines
|
|
299
|
+
const tail = lines.slice(-50).join('\n');
|
|
300
|
+
console.log(tail || '(empty log)');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function cmdRestart() {
|
|
304
|
+
console.log('Restarting Memory Agent...');
|
|
305
|
+
await cmdStop();
|
|
306
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
307
|
+
await cmdStart();
|
|
95
308
|
}
|
|
96
309
|
|
|
97
|
-
// Doctor - diagnose issues (async)
|
|
98
310
|
async function runDoctor() {
|
|
99
|
-
console.log('\
|
|
311
|
+
console.log('\nClaude Memory Agent - System Check\n');
|
|
100
312
|
console.log('='.repeat(50));
|
|
101
313
|
|
|
102
314
|
let issues = 0;
|
|
103
315
|
let warnings = 0;
|
|
104
316
|
|
|
105
|
-
//
|
|
317
|
+
// Python
|
|
106
318
|
process.stdout.write('Python 3.9+........... ');
|
|
107
319
|
const python = getPython();
|
|
108
320
|
if (python) {
|
|
109
|
-
console.log('
|
|
321
|
+
console.log('OK');
|
|
110
322
|
} else {
|
|
111
|
-
console.log('
|
|
323
|
+
console.log('NOT FOUND');
|
|
112
324
|
console.log(' Install from: https://python.org/');
|
|
113
325
|
issues++;
|
|
114
326
|
}
|
|
115
327
|
|
|
116
|
-
//
|
|
328
|
+
// Ollama
|
|
117
329
|
process.stdout.write('Ollama................ ');
|
|
118
|
-
const ollamaOk = await
|
|
330
|
+
const { ok: ollamaOk } = await httpGet('http://localhost:11434/api/tags');
|
|
119
331
|
if (ollamaOk) {
|
|
120
|
-
console.log('
|
|
332
|
+
console.log('Running');
|
|
121
333
|
} else {
|
|
122
|
-
console.log('
|
|
334
|
+
console.log('NOT RUNNING');
|
|
123
335
|
console.log(' Install from: https://ollama.ai/download');
|
|
124
336
|
console.log(' Then run: ollama serve');
|
|
125
337
|
issues++;
|
|
126
338
|
}
|
|
127
339
|
|
|
128
|
-
//
|
|
340
|
+
// Embedding model
|
|
129
341
|
if (ollamaOk) {
|
|
130
342
|
process.stdout.write('Embedding model....... ');
|
|
131
|
-
const modelOk = await
|
|
132
|
-
|
|
133
|
-
|
|
343
|
+
const { ok: modelOk, body } = await httpGet('http://localhost:11434/api/tags');
|
|
344
|
+
let hasModel = false;
|
|
345
|
+
try {
|
|
346
|
+
const models = JSON.parse(body).models || [];
|
|
347
|
+
hasModel = models.some((m) => m.name && m.name.includes('nomic-embed-text'));
|
|
348
|
+
} catch (_) {}
|
|
349
|
+
if (hasModel) {
|
|
350
|
+
console.log('nomic-embed-text');
|
|
134
351
|
} else {
|
|
135
|
-
console.log('
|
|
352
|
+
console.log('NOT INSTALLED');
|
|
136
353
|
console.log(' Run: ollama pull nomic-embed-text');
|
|
137
354
|
issues++;
|
|
138
355
|
}
|
|
139
356
|
}
|
|
140
357
|
|
|
141
|
-
//
|
|
358
|
+
// Memory Agent
|
|
142
359
|
process.stdout.write('Memory Agent.......... ');
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log('✓ Running');
|
|
360
|
+
if (await isAgentRunning()) {
|
|
361
|
+
console.log('Running');
|
|
146
362
|
} else {
|
|
147
|
-
console.log('
|
|
363
|
+
console.log('NOT RUNNING');
|
|
148
364
|
console.log(' Run: claude-memory-agent start');
|
|
149
365
|
warnings++;
|
|
150
366
|
}
|
|
151
367
|
|
|
152
|
-
//
|
|
368
|
+
// .env
|
|
153
369
|
process.stdout.write('.env file............. ');
|
|
154
|
-
if (fs.existsSync(
|
|
155
|
-
console.log('
|
|
370
|
+
if (fs.existsSync(ENV_FILE)) {
|
|
371
|
+
console.log('Exists');
|
|
156
372
|
} else {
|
|
157
|
-
console.log('
|
|
373
|
+
console.log('MISSING');
|
|
158
374
|
console.log(' Run: claude-memory-agent install');
|
|
159
375
|
issues++;
|
|
160
376
|
}
|
|
@@ -162,51 +378,17 @@ async function runDoctor() {
|
|
|
162
378
|
// Summary
|
|
163
379
|
console.log('\n' + '='.repeat(50));
|
|
164
380
|
if (issues === 0 && warnings === 0) {
|
|
165
|
-
console.log('
|
|
381
|
+
console.log('All systems operational!\n');
|
|
166
382
|
} else if (issues === 0) {
|
|
167
|
-
console.log(
|
|
383
|
+
console.log(`${warnings} warning(s) - agent may not be running\n`);
|
|
168
384
|
} else {
|
|
169
|
-
console.log(
|
|
385
|
+
console.log(`${issues} issue(s) found - fix above problems\n`);
|
|
170
386
|
}
|
|
171
387
|
}
|
|
172
388
|
|
|
173
|
-
// Helper: Check if URL responds
|
|
174
|
-
function checkUrl(url) {
|
|
175
|
-
return new Promise((resolve) => {
|
|
176
|
-
const http = require('http');
|
|
177
|
-
const req = http.get(url, { timeout: 2000 }, (res) => {
|
|
178
|
-
resolve(res.statusCode === 200);
|
|
179
|
-
});
|
|
180
|
-
req.on('error', () => resolve(false));
|
|
181
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Helper: Check if Ollama has embedding model
|
|
186
|
-
function checkOllamaModel() {
|
|
187
|
-
return new Promise((resolve) => {
|
|
188
|
-
const http = require('http');
|
|
189
|
-
http.get('http://localhost:11434/api/tags', (res) => {
|
|
190
|
-
let data = '';
|
|
191
|
-
res.on('data', chunk => data += chunk);
|
|
192
|
-
res.on('end', () => {
|
|
193
|
-
try {
|
|
194
|
-
const json = JSON.parse(data);
|
|
195
|
-
const models = json.models || [];
|
|
196
|
-
const hasModel = models.some(m => m.name && m.name.includes('nomic-embed-text'));
|
|
197
|
-
resolve(hasModel);
|
|
198
|
-
} catch (e) {
|
|
199
|
-
resolve(false);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}).on('error', () => resolve(false));
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Print help
|
|
207
389
|
function printHelp() {
|
|
208
390
|
console.log(`
|
|
209
|
-
Claude Memory Agent v2.2.
|
|
391
|
+
Claude Memory Agent v2.2.4
|
|
210
392
|
Persistent semantic memory for Claude Code sessions
|
|
211
393
|
|
|
212
394
|
USAGE:
|
|
@@ -242,58 +424,52 @@ For more info: https://www.npmjs.com/package/claude-memory-agent
|
|
|
242
424
|
`);
|
|
243
425
|
}
|
|
244
426
|
|
|
245
|
-
//
|
|
246
|
-
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Main dispatch
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
async function main() {
|
|
247
432
|
switch (command) {
|
|
248
433
|
case 'install':
|
|
249
434
|
case 'setup':
|
|
250
|
-
// Run the onboarding wizard
|
|
251
435
|
try {
|
|
252
436
|
const { main: runOnboarding } = require('./onboarding');
|
|
253
|
-
runOnboarding().catch(err => {
|
|
437
|
+
runOnboarding().catch((err) => {
|
|
254
438
|
console.error('Setup error:', err.message);
|
|
255
439
|
process.exit(1);
|
|
256
440
|
});
|
|
257
|
-
} catch (
|
|
258
|
-
// Fallback to Python installer if onboarding deps missing
|
|
441
|
+
} catch (_) {
|
|
259
442
|
runPython('install.py', args.slice(1));
|
|
260
443
|
}
|
|
261
444
|
break;
|
|
262
445
|
|
|
263
446
|
case 'start':
|
|
264
|
-
|
|
447
|
+
await cmdStart();
|
|
265
448
|
break;
|
|
266
449
|
|
|
267
450
|
case 'stop':
|
|
268
|
-
|
|
451
|
+
await cmdStop();
|
|
452
|
+
break;
|
|
453
|
+
|
|
454
|
+
case 'restart':
|
|
455
|
+
await cmdRestart();
|
|
269
456
|
break;
|
|
270
457
|
|
|
271
458
|
case 'status':
|
|
272
|
-
|
|
459
|
+
await cmdStatus();
|
|
273
460
|
break;
|
|
274
461
|
|
|
275
462
|
case 'dashboard':
|
|
276
|
-
|
|
463
|
+
await cmdDashboard();
|
|
277
464
|
break;
|
|
278
465
|
|
|
279
466
|
case 'logs':
|
|
280
|
-
|
|
281
|
-
break;
|
|
282
|
-
|
|
283
|
-
case 'restart':
|
|
284
|
-
// Stop then start
|
|
285
|
-
console.log('Restarting Memory Agent...');
|
|
286
|
-
try {
|
|
287
|
-
execSync(`${getPython()} memory-agent stop`, { cwd: AGENT_DIR, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
288
|
-
} catch (e) { /* ignore stop errors */ }
|
|
289
|
-
setTimeout(() => {
|
|
290
|
-
runPython('memory-agent', ['start']);
|
|
291
|
-
}, 1000);
|
|
467
|
+
cmdLogs();
|
|
292
468
|
break;
|
|
293
469
|
|
|
294
470
|
case 'doctor':
|
|
295
471
|
case 'diagnose':
|
|
296
|
-
runDoctor();
|
|
472
|
+
await runDoctor();
|
|
297
473
|
break;
|
|
298
474
|
|
|
299
475
|
case 'uninstall':
|
|
@@ -302,7 +478,7 @@ function main() {
|
|
|
302
478
|
|
|
303
479
|
case 'run':
|
|
304
480
|
case 'server':
|
|
305
|
-
// Run directly (not
|
|
481
|
+
// Run main.py directly (foreground, not detached)
|
|
306
482
|
runPython('main.py', args.slice(1));
|
|
307
483
|
break;
|
|
308
484
|
|
|
@@ -314,7 +490,7 @@ function main() {
|
|
|
314
490
|
|
|
315
491
|
case '--version':
|
|
316
492
|
case '-v':
|
|
317
|
-
console.log('claude-memory-agent v2.2.
|
|
493
|
+
console.log('claude-memory-agent v2.2.4');
|
|
318
494
|
break;
|
|
319
495
|
|
|
320
496
|
default:
|
package/config.py
CHANGED
|
@@ -107,6 +107,12 @@ class Config:
|
|
|
107
107
|
# Adaptive search ranking
|
|
108
108
|
self.DEFAULT_SEARCH_TEMPERATURE = float(os.getenv("DEFAULT_SEARCH_TEMPERATURE", "1.0"))
|
|
109
109
|
|
|
110
|
+
# Cross-session awareness
|
|
111
|
+
self.SESSION_IDLE_THRESHOLD_MINUTES = int(os.getenv("SESSION_IDLE_THRESHOLD_MINUTES", "10"))
|
|
112
|
+
self.SESSION_COMPLETED_THRESHOLD_MINUTES = int(os.getenv("SESSION_COMPLETED_THRESHOLD_MINUTES", "30"))
|
|
113
|
+
self.SESSION_ACTIVITY_MAX_AGE_HOURS = int(os.getenv("SESSION_ACTIVITY_MAX_AGE_HOURS", "24"))
|
|
114
|
+
self.SESSION_CLEANUP_INTERVAL_SECONDS = int(os.getenv("SESSION_CLEANUP_INTERVAL_SECONDS", "300"))
|
|
115
|
+
|
|
110
116
|
# Validate configuration
|
|
111
117
|
self._validate()
|
|
112
118
|
|
package/hooks/auto_capture.py
CHANGED
|
@@ -23,7 +23,7 @@ import asyncio
|
|
|
23
23
|
import logging
|
|
24
24
|
from datetime import datetime
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import Dict, Any, Optional
|
|
26
|
+
from typing import Dict, Any, Optional, List
|
|
27
27
|
|
|
28
28
|
# Add parent to path for imports
|
|
29
29
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
@@ -128,6 +128,53 @@ async def send_to_memory(
|
|
|
128
128
|
return False
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
async def post_session_activity(session_id: str, project_path: str, event_type: str, summary: str, files: List[str] = None):
|
|
132
|
+
"""Post a cross-session activity event and track modified files."""
|
|
133
|
+
if not session_id or not project_path:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
headers = {"Content-Type": "application/json"}
|
|
137
|
+
if API_KEY:
|
|
138
|
+
headers["X-Memory-Key"] = API_KEY
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
async with httpx.AsyncClient(timeout=3.0) as client:
|
|
142
|
+
# Post activity event
|
|
143
|
+
await client.post(
|
|
144
|
+
f"{MEMORY_AGENT_URL}/api/sessions/activity",
|
|
145
|
+
json={
|
|
146
|
+
"session_id": session_id,
|
|
147
|
+
"project_path": project_path,
|
|
148
|
+
"event_type": event_type,
|
|
149
|
+
"summary": summary,
|
|
150
|
+
"files": files or [],
|
|
151
|
+
},
|
|
152
|
+
headers=headers,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Append files to session's modified files list
|
|
156
|
+
if files:
|
|
157
|
+
for f in files:
|
|
158
|
+
await client.post(
|
|
159
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
160
|
+
json={
|
|
161
|
+
"jsonrpc": "2.0",
|
|
162
|
+
"method": "skills/call",
|
|
163
|
+
"params": {
|
|
164
|
+
"skill_id": "session_append_file",
|
|
165
|
+
"params": {
|
|
166
|
+
"session_id": session_id,
|
|
167
|
+
"file_path": f,
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"id": f"auto-capture-file-{datetime.now().isoformat()}"
|
|
171
|
+
},
|
|
172
|
+
headers=headers,
|
|
173
|
+
)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.debug(f"Cross-session activity post failed: {e}")
|
|
176
|
+
|
|
177
|
+
|
|
131
178
|
async def capture_tool_use(hook_data: Dict[str, Any]):
|
|
132
179
|
"""Capture a tool execution event."""
|
|
133
180
|
tool_name = hook_data.get("tool_name", "Unknown")
|
|
@@ -182,6 +229,16 @@ async def capture_tool_use(hook_data: Dict[str, Any]):
|
|
|
182
229
|
|
|
183
230
|
await send_to_memory(content, mem_type, importance, metadata, project_path)
|
|
184
231
|
|
|
232
|
+
# ============================================================
|
|
233
|
+
# CROSS-SESSION AWARENESS: Post file changes to activity feed
|
|
234
|
+
# ============================================================
|
|
235
|
+
if tool_name in ("Write", "Edit") and session_id and project_path:
|
|
236
|
+
file_path = tool_input.get("file_path", "")
|
|
237
|
+
if file_path:
|
|
238
|
+
event_type = "file_change"
|
|
239
|
+
summary = f"{'Created' if tool_name == 'Write' else 'Edited'} {file_path}"
|
|
240
|
+
await post_session_activity(session_id, project_path, event_type, summary, [file_path])
|
|
241
|
+
|
|
185
242
|
|
|
186
243
|
async def capture_notification(hook_data: Dict[str, Any]):
|
|
187
244
|
"""Capture a notification/error event."""
|