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 CHANGED
@@ -3,158 +3,374 @@
3
3
  /**
4
4
  * Claude Memory Agent CLI
5
5
  *
6
- * This is the npm entry point that wraps the Python CLI.
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 status
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
- // Check Python availability
24
- function getPython() {
25
- const pythonCommands = ['python3', 'python', 'py'];
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
- for (const cmd of pythonCommands) {
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 result = execSync(`${cmd} --version`, {
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 (result.includes('Python 3')) {
34
- return cmd;
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
- // Check if Python dependencies are installed
44
- function checkDependencies(python) {
45
- try {
46
- execSync(`${python} -c "import fastapi, uvicorn, dotenv"`, {
47
- cwd: AGENT_DIR,
48
- stdio: ['pipe', 'pipe', 'pipe']
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 (e) {
133
+ } catch (_) {
52
134
  return false;
53
135
  }
54
136
  }
55
137
 
56
- // Install Python dependencies
57
- function installDependencies(python) {
58
- console.log('Installing Python dependencies...');
138
+ /** Kill a process by PID (cross-platform) */
139
+ function killProcess(pid) {
59
140
  try {
60
- execSync(`${python} -m pip install -r requirements.txt -q`, {
61
- cwd: AGENT_DIR,
62
- stdio: 'inherit'
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 (e) {
66
- console.error('Failed to install dependencies:', e.message);
147
+ } catch (_) {
67
148
  return false;
68
149
  }
69
150
  }
70
151
 
71
- // Run Python script
72
- function runPython(script, scriptArgs = []) {
73
- const python = getPython();
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
- if (!python) {
76
- console.error('Error: Python 3 is required but not found.');
77
- console.error('Please install Python 3.9+ from https://python.org/');
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
- const proc = spawn(python, [script, ...scriptArgs], {
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: 'inherit',
84
- shell: process.platform === 'win32'
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
- proc.on('close', (code) => {
88
- process.exit(code || 0);
89
- });
198
+ const child = spawn(python, [mainPy], spawnOpts);
199
+ child.unref();
90
200
 
91
- proc.on('error', (err) => {
92
- console.error('Failed to start:', err.message);
93
- process.exit(1);
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('\n🩺 Claude Memory Agent - System Check\n');
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
- // Check Python
317
+ // Python
106
318
  process.stdout.write('Python 3.9+........... ');
107
319
  const python = getPython();
108
320
  if (python) {
109
- console.log('OK');
321
+ console.log('OK');
110
322
  } else {
111
- console.log('NOT FOUND');
323
+ console.log('NOT FOUND');
112
324
  console.log(' Install from: https://python.org/');
113
325
  issues++;
114
326
  }
115
327
 
116
- // Check Ollama
328
+ // Ollama
117
329
  process.stdout.write('Ollama................ ');
118
- const ollamaOk = await checkUrl('http://localhost:11434/api/tags');
330
+ const { ok: ollamaOk } = await httpGet('http://localhost:11434/api/tags');
119
331
  if (ollamaOk) {
120
- console.log('Running');
332
+ console.log('Running');
121
333
  } else {
122
- console.log('NOT RUNNING');
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
- // Check embedding model
340
+ // Embedding model
129
341
  if (ollamaOk) {
130
342
  process.stdout.write('Embedding model....... ');
131
- const modelOk = await checkOllamaModel();
132
- if (modelOk) {
133
- console.log('✓ nomic-embed-text');
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('NOT INSTALLED');
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
- // Check Memory Agent
358
+ // Memory Agent
142
359
  process.stdout.write('Memory Agent.......... ');
143
- const agentOk = await checkUrl('http://localhost:8102/health');
144
- if (agentOk) {
145
- console.log('✓ Running');
360
+ if (await isAgentRunning()) {
361
+ console.log('Running');
146
362
  } else {
147
- console.log('NOT RUNNING');
363
+ console.log('NOT RUNNING');
148
364
  console.log(' Run: claude-memory-agent start');
149
365
  warnings++;
150
366
  }
151
367
 
152
- // Check .env file
368
+ // .env
153
369
  process.stdout.write('.env file............. ');
154
- if (fs.existsSync(path.join(AGENT_DIR, '.env'))) {
155
- console.log('Exists');
370
+ if (fs.existsSync(ENV_FILE)) {
371
+ console.log('Exists');
156
372
  } else {
157
- console.log('MISSING');
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('All systems operational!\n');
381
+ console.log('All systems operational!\n');
166
382
  } else if (issues === 0) {
167
- console.log(`⚠️ ${warnings} warning(s) - agent may not be running\n`);
383
+ console.log(`${warnings} warning(s) - agent may not be running\n`);
168
384
  } else {
169
- console.log(`❌ ${issues} issue(s) found - fix above problems\n`);
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.3
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
- // Main
246
- function main() {
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 (e) {
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
- runPython('memory-agent', ['start', ...args.slice(1)]);
447
+ await cmdStart();
265
448
  break;
266
449
 
267
450
  case 'stop':
268
- runPython('memory-agent', ['stop', ...args.slice(1)]);
451
+ await cmdStop();
452
+ break;
453
+
454
+ case 'restart':
455
+ await cmdRestart();
269
456
  break;
270
457
 
271
458
  case 'status':
272
- runPython('memory-agent', ['status', ...args.slice(1)]);
459
+ await cmdStatus();
273
460
  break;
274
461
 
275
462
  case 'dashboard':
276
- runPython('memory-agent', ['dashboard', ...args.slice(1)]);
463
+ await cmdDashboard();
277
464
  break;
278
465
 
279
466
  case 'logs':
280
- runPython('memory-agent', ['logs', ...args.slice(1)]);
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 in background)
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.3');
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
 
@@ -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."""