agent-working-memory 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +87 -46
  2. package/dist/api/routes.d.ts.map +1 -1
  3. package/dist/api/routes.js +21 -5
  4. package/dist/api/routes.js.map +1 -1
  5. package/dist/cli.js +67 -67
  6. package/dist/coordination/index.d.ts +11 -0
  7. package/dist/coordination/index.d.ts.map +1 -0
  8. package/dist/coordination/index.js +39 -0
  9. package/dist/coordination/index.js.map +1 -0
  10. package/dist/coordination/mcp-tools.d.ts +8 -0
  11. package/dist/coordination/mcp-tools.d.ts.map +1 -0
  12. package/dist/coordination/mcp-tools.js +216 -0
  13. package/dist/coordination/mcp-tools.js.map +1 -0
  14. package/dist/coordination/routes.d.ts +9 -0
  15. package/dist/coordination/routes.d.ts.map +1 -0
  16. package/dist/coordination/routes.js +434 -0
  17. package/dist/coordination/routes.js.map +1 -0
  18. package/dist/coordination/schema.d.ts +12 -0
  19. package/dist/coordination/schema.d.ts.map +1 -0
  20. package/dist/coordination/schema.js +91 -0
  21. package/dist/coordination/schema.js.map +1 -0
  22. package/dist/coordination/schemas.d.ts +208 -0
  23. package/dist/coordination/schemas.d.ts.map +1 -0
  24. package/dist/coordination/schemas.js +109 -0
  25. package/dist/coordination/schemas.js.map +1 -0
  26. package/dist/coordination/stale.d.ts +25 -0
  27. package/dist/coordination/stale.d.ts.map +1 -0
  28. package/dist/coordination/stale.js +53 -0
  29. package/dist/coordination/stale.js.map +1 -0
  30. package/dist/index.js +21 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp.js +90 -79
  33. package/dist/mcp.js.map +1 -1
  34. package/dist/storage/sqlite.d.ts +3 -0
  35. package/dist/storage/sqlite.d.ts.map +1 -1
  36. package/dist/storage/sqlite.js +285 -281
  37. package/dist/storage/sqlite.js.map +1 -1
  38. package/package.json +55 -55
  39. package/src/api/index.ts +3 -3
  40. package/src/api/routes.ts +551 -536
  41. package/src/cli.ts +397 -397
  42. package/src/coordination/index.ts +47 -0
  43. package/src/coordination/mcp-tools.ts +313 -0
  44. package/src/coordination/routes.ts +656 -0
  45. package/src/coordination/schema.ts +94 -0
  46. package/src/coordination/schemas.ts +136 -0
  47. package/src/coordination/stale.ts +89 -0
  48. package/src/core/decay.ts +63 -63
  49. package/src/core/embeddings.ts +88 -88
  50. package/src/core/hebbian.ts +93 -93
  51. package/src/core/index.ts +5 -5
  52. package/src/core/logger.ts +36 -36
  53. package/src/core/query-expander.ts +66 -66
  54. package/src/core/reranker.ts +101 -101
  55. package/src/engine/activation.ts +656 -656
  56. package/src/engine/connections.ts +103 -103
  57. package/src/engine/consolidation-scheduler.ts +125 -125
  58. package/src/engine/eval.ts +102 -102
  59. package/src/engine/eviction.ts +101 -101
  60. package/src/engine/index.ts +8 -8
  61. package/src/engine/retraction.ts +100 -100
  62. package/src/engine/staging.ts +74 -74
  63. package/src/index.ts +137 -121
  64. package/src/mcp.ts +1024 -1013
  65. package/src/storage/index.ts +3 -3
  66. package/src/storage/sqlite.ts +968 -963
  67. package/src/types/agent.ts +67 -67
  68. package/src/types/checkpoint.ts +46 -46
  69. package/src/types/engram.ts +217 -217
  70. package/src/types/eval.ts +100 -100
  71. package/src/types/index.ts +6 -6
package/src/cli.ts CHANGED
@@ -1,397 +1,397 @@
1
- #!/usr/bin/env node
2
- // Copyright 2026 Robert Winter / Complete Ideas
3
- // SPDX-License-Identifier: Apache-2.0
4
-
5
- /**
6
- * CLI entrypoint for AgentWorkingMemory.
7
- *
8
- * Commands:
9
- * awm setup — configure MCP for the current project
10
- * awm mcp — start the MCP server (called by Claude Code)
11
- * awm serve — start the HTTP API server
12
- * awm health — check if a running server is healthy
13
- */
14
-
15
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
16
- import { resolve, basename, join, dirname } from 'node:path';
17
- import { execSync } from 'node:child_process';
18
- import { randomBytes } from 'node:crypto';
19
- import { fileURLToPath } from 'node:url';
20
- import { homedir as osHomedir } from 'node:os';
21
-
22
- const __filename = fileURLToPath(import.meta.url);
23
- const __dirname = dirname(__filename);
24
-
25
- // Load .env if present
26
- try {
27
- const envPath = resolve(process.cwd(), '.env');
28
- const envContent = readFileSync(envPath, 'utf-8');
29
- for (const line of envContent.split('\n')) {
30
- const trimmed = line.trim();
31
- if (!trimmed || trimmed.startsWith('#')) continue;
32
- const eqIdx = trimmed.indexOf('=');
33
- if (eqIdx === -1) continue;
34
- const key = trimmed.slice(0, eqIdx).trim();
35
- const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
36
- if (!process.env[key]) process.env[key] = val;
37
- }
38
- } catch { /* No .env file */ }
39
-
40
- const args = process.argv.slice(2);
41
- const command = args[0];
42
-
43
- function printUsage() {
44
- console.log(`
45
- AgentWorkingMemory — Cognitive memory for AI agents
46
-
47
- Usage:
48
- awm setup [--global] [--agent-id <id>] [--db-path <path>] [--no-claude-md]
49
- [--no-hooks] [--hook-port <port>] Configure MCP for Claude Code
50
- awm mcp Start MCP server (used by Claude Code)
51
- awm serve [--port <port>] Start HTTP API server
52
- awm health [--port <port>] Check server health
53
-
54
- Setup:
55
- awm setup --global Recommended. Writes ~/.mcp.json so AWM is available
56
- in every project — one brain across all your work.
57
-
58
- awm setup Project-level. Writes .mcp.json in the current directory
59
- and appends workflow instructions to CLAUDE.md.
60
-
61
- --no-claude-md Skip CLAUDE.md modification
62
- --no-hooks Skip hook installation (no auto-checkpoint)
63
- --hook-port PORT Sidecar port for hooks (default: 8401)
64
-
65
- Restart Claude Code after setup to pick up the new MCP server.
66
- `.trim());
67
- }
68
-
69
- // ─── SETUP ──────────────────────────────────────
70
-
71
- function setup() {
72
- const cwd = process.cwd();
73
- const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
74
-
75
- // Parse flags
76
- let agentId = projectName;
77
- let dbPath: string | null = null;
78
- let skipClaudeMd = false;
79
- let isGlobal = false;
80
- let skipHooks = false;
81
- let hookPort = '8401';
82
-
83
- for (let i = 1; i < args.length; i++) {
84
- if (args[i] === '--agent-id' && args[i + 1]) {
85
- agentId = args[++i];
86
- } else if (args[i] === '--db-path' && args[i + 1]) {
87
- dbPath = args[++i];
88
- } else if (args[i] === '--no-claude-md') {
89
- skipClaudeMd = true;
90
- } else if (args[i] === '--no-hooks') {
91
- skipHooks = true;
92
- } else if (args[i] === '--hook-port' && args[i + 1]) {
93
- hookPort = args[++i];
94
- } else if (args[i] === '--global') {
95
- isGlobal = true;
96
- agentId = 'claude'; // unified agent ID for global setup
97
- }
98
- }
99
-
100
- // Find the package root (where src/mcp.ts lives)
101
- const packageRoot = resolve(__dirname, '..');
102
- const mcpScript = join(packageRoot, 'src', 'mcp.ts');
103
- const mcpDist = join(packageRoot, 'dist', 'mcp.js');
104
-
105
- // Determine DB path — default to <awm-root>/data/memory.db (shared across projects)
106
- if (!dbPath) {
107
- dbPath = join(packageRoot, 'data', 'memory.db');
108
- }
109
- const dbDir = dirname(dbPath);
110
-
111
- // Ensure data directory exists
112
- if (!existsSync(dbDir)) {
113
- mkdirSync(dbDir, { recursive: true });
114
- console.log(`Created data directory: ${dbDir}`);
115
- }
116
-
117
- // Generate hook secret (or reuse existing one)
118
- let hookSecret = '';
119
- const secretPath = join(dirname(dbPath!), '.awm-hook-secret');
120
- if (existsSync(secretPath)) {
121
- hookSecret = readFileSync(secretPath, 'utf-8').trim();
122
- }
123
- if (!hookSecret) {
124
- hookSecret = randomBytes(32).toString('hex');
125
- mkdirSync(dirname(secretPath), { recursive: true });
126
- writeFileSync(secretPath, hookSecret + '\n');
127
- }
128
-
129
- // Determine command based on platform and whether dist exists
130
- const isWindows = process.platform === 'win32';
131
- const hasDist = existsSync(mcpDist);
132
-
133
- const envVars: Record<string, string> = {
134
- AWM_DB_PATH: (isWindows ? dbPath!.replace(/\\/g, '/') : dbPath!),
135
- AWM_AGENT_ID: agentId,
136
- AWM_HOOK_PORT: hookPort,
137
- AWM_HOOK_SECRET: hookSecret,
138
- };
139
-
140
- let mcpConfig: { command: string; args: string[]; env: Record<string, string> };
141
-
142
- if (hasDist) {
143
- mcpConfig = {
144
- command: 'node',
145
- args: [mcpDist.replace(/\\/g, '/')],
146
- env: envVars,
147
- };
148
- } else if (isWindows) {
149
- mcpConfig = {
150
- command: 'cmd',
151
- args: ['/c', 'npx', 'tsx', mcpScript.replace(/\\/g, '/')],
152
- env: envVars,
153
- };
154
- } else {
155
- mcpConfig = {
156
- command: 'npx',
157
- args: ['tsx', mcpScript],
158
- env: envVars,
159
- };
160
- }
161
-
162
- // Read or create .mcp.json
163
- const mcpJsonPath = isGlobal ? join(osHomedir(), '.mcp.json') : join(cwd, '.mcp.json');
164
- let existing: any = { mcpServers: {} };
165
- if (existsSync(mcpJsonPath)) {
166
- try {
167
- existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
168
- if (!existing.mcpServers) existing.mcpServers = {};
169
- } catch {
170
- existing = { mcpServers: {} };
171
- }
172
- }
173
-
174
- existing.mcpServers['agent-working-memory'] = mcpConfig;
175
- writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
176
-
177
- // Auto-append CLAUDE.md snippet unless --no-claude-md
178
- let claudeMdAction = '';
179
- const claudeMdSnippet = `
180
-
181
- ## Memory (AWM)
182
- You have persistent memory via the agent-working-memory MCP server.
183
-
184
- ### Lifecycle (always do these)
185
- - Session start: call memory_restore to recover previous context
186
- - Starting a task: call memory_task_begin (checkpoints + recalls relevant memories)
187
- - Finishing a task: call memory_task_end with a summary
188
- - Auto-checkpoint: hooks handle compaction, session end, and 15-min timer (no action needed)
189
-
190
- ### Write memory when:
191
- - A project decision is made or changed
192
- - A root cause is discovered after debugging
193
- - A reusable implementation pattern is established
194
- - A user preference, constraint, or requirement is clarified
195
- - A prior assumption is found to be wrong
196
- - A significant piece of work is completed
197
-
198
- ### Recall memory when:
199
- - Starting work on a new task or subsystem
200
- - Re-entering code you haven't touched recently
201
- - After a failed attempt — check if there's prior knowledge
202
- - Before refactoring or making architectural changes
203
- - When a topic comes up that you might have prior context on
204
-
205
- ### Also:
206
- - After using a recalled memory: call memory_feedback (useful/not-useful)
207
- - To correct wrong info: call memory_retract
208
- - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
209
- `;
210
-
211
- // For global: write to ~/.claude/CLAUDE.md (loaded by Claude Code in every session)
212
- // For project: write to ./CLAUDE.md in the current directory
213
- const claudeMdPath = isGlobal
214
- ? join(osHomedir(), '.claude', 'CLAUDE.md')
215
- : join(cwd, 'CLAUDE.md');
216
-
217
- // Ensure parent directory exists (for ~/.claude/CLAUDE.md)
218
- const claudeMdDir = dirname(claudeMdPath);
219
- if (!existsSync(claudeMdDir)) {
220
- mkdirSync(claudeMdDir, { recursive: true });
221
- }
222
-
223
- if (skipClaudeMd) {
224
- claudeMdAction = ' CLAUDE.md: skipped (--no-claude-md)';
225
- } else if (existsSync(claudeMdPath)) {
226
- const content = readFileSync(claudeMdPath, 'utf-8');
227
- if (content.includes('## Memory (AWM)')) {
228
- claudeMdAction = ' CLAUDE.md: already has AWM section (skipped)';
229
- } else {
230
- writeFileSync(claudeMdPath, content.trimEnd() + '\n' + claudeMdSnippet);
231
- claudeMdAction = ' CLAUDE.md: appended AWM workflow section';
232
- }
233
- } else {
234
- const title = isGlobal ? '# Global Instructions' : `# ${basename(cwd)}`;
235
- writeFileSync(claudeMdPath, `${title}\n${claudeMdSnippet}`);
236
- claudeMdAction = ' CLAUDE.md: created with AWM workflow section';
237
- }
238
-
239
- // --- Hook configuration ---
240
- let hookAction = '';
241
- if (skipHooks) {
242
- hookAction = ' Hooks: skipped (--no-hooks)';
243
- } else {
244
- // Write hooks to Claude Code settings (~/.claude/settings.json)
245
- const settingsPath = join(osHomedir(), '.claude', 'settings.json');
246
- let settings: any = {};
247
- if (existsSync(settingsPath)) {
248
- try {
249
- settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
250
- } catch {
251
- settings = {};
252
- }
253
- }
254
-
255
- if (!settings.hooks) settings.hooks = {};
256
-
257
- const hookUrl = `http://127.0.0.1:${hookPort}/hooks/checkpoint`;
258
-
259
- // Stop — remind Claude to write/recall/switch tasks after each response
260
- settings.hooks.Stop = [{
261
- matcher: '',
262
- hooks: [{
263
- type: 'command',
264
- command: 'echo "MEMORY: (1) Did you learn anything new? Call memory_write. (2) Are you about to work on a topic you might have prior knowledge about? Call memory_recall. (3) Switching tasks? Call memory_task_begin."',
265
- timeout: 5,
266
- async: true,
267
- }],
268
- }];
269
-
270
- // Build hook command with multi-port fallback for separate memory pools.
271
- // When users have work (port 8401) and personal (port 8402) pools via
272
- // per-folder .mcp.json, the hook needs to try both ports since the global
273
- // settings.json can't know which pool is active in the current session.
274
- const altPort = hookPort === '8401' ? '8402' : '8401';
275
- const hookUrlAlt = `http://127.0.0.1:${altPort}/hooks/checkpoint`;
276
- const buildHookCmd = (event: string, maxTime: number) => {
277
- const primary = `curl -sf -X POST ${hookUrl} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
278
- const fallback = `curl -sf -X POST ${hookUrlAlt} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
279
- return `${primary} || ${fallback}`;
280
- };
281
-
282
- // PreCompact — auto-checkpoint before context compaction
283
- settings.hooks.PreCompact = [{
284
- matcher: '',
285
- hooks: [{
286
- type: 'command',
287
- command: buildHookCmd('PreCompact', 5),
288
- timeout: 10,
289
- }],
290
- }];
291
-
292
- // SessionEnd — auto-checkpoint on session close (fast timeout to avoid cancellation)
293
- settings.hooks.SessionEnd = [{
294
- matcher: '',
295
- hooks: [{
296
- type: 'command',
297
- command: buildHookCmd('SessionEnd', 2),
298
- timeout: 5,
299
- }],
300
- }];
301
-
302
- // Ensure settings directory exists
303
- const settingsDir = dirname(settingsPath);
304
- if (!existsSync(settingsDir)) {
305
- mkdirSync(settingsDir, { recursive: true });
306
- }
307
-
308
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
309
- hookAction = ` Hooks: Stop (memory reminder) + PreCompact + SessionEnd → auto-checkpoint (port ${hookPort})`;
310
- }
311
-
312
- const scope = isGlobal ? 'globally (all projects)' : cwd;
313
- console.log(`
314
- AWM configured ${isGlobal ? 'globally' : 'for: ' + cwd}
315
-
316
- Agent ID: ${agentId}
317
- DB path: ${dbPath}
318
- MCP config: ${mcpJsonPath}
319
- Hook port: ${hookPort}
320
- Hook secret: ${hookSecret.slice(0, 8)}...
321
- ${claudeMdAction}
322
- ${hookAction}
323
-
324
- Next steps:
325
- 1. Restart Claude Code to pick up the MCP server
326
- 2. The memory tools will appear automatically
327
- 3. Hooks auto-checkpoint on context compaction and session end${isGlobal ? '\n 4. One brain across all your projects — no per-project setup needed' : ''}
328
- `.trim());
329
- }
330
-
331
- // ─── MCP ──────────────────────────────────────
332
-
333
- async function mcp() {
334
- // Dynamic import to avoid loading heavy deps for setup/health commands
335
- await import('./mcp.js');
336
- }
337
-
338
- // ─── SERVE ──────────────────────────────────────
339
-
340
- async function serve() {
341
- // Parse --port flag
342
- for (let i = 1; i < args.length; i++) {
343
- if (args[i] === '--port' && args[i + 1]) {
344
- process.env.AWM_PORT = args[++i];
345
- }
346
- }
347
- await import('./index.js');
348
- }
349
-
350
- // ─── HEALTH ──────────────────────────────────────
351
-
352
- function health() {
353
- let port = '8400';
354
- for (let i = 1; i < args.length; i++) {
355
- if (args[i] === '--port' && args[i + 1]) {
356
- port = args[++i];
357
- }
358
- }
359
-
360
- try {
361
- const result = execSync(`curl -sf http://localhost:${port}/health`, {
362
- encoding: 'utf8',
363
- timeout: 5000,
364
- });
365
- const data = JSON.parse(result);
366
- console.log(`OK — v${data.version} (${data.timestamp})`);
367
- } catch {
368
- console.error(`Cannot reach AWM server on port ${port}`);
369
- process.exit(1);
370
- }
371
- }
372
-
373
- // ─── Dispatch ──────────────────────────────────────
374
-
375
- switch (command) {
376
- case 'setup':
377
- setup();
378
- break;
379
- case 'mcp':
380
- mcp();
381
- break;
382
- case 'serve':
383
- serve();
384
- break;
385
- case 'health':
386
- health();
387
- break;
388
- case '--help':
389
- case '-h':
390
- case undefined:
391
- printUsage();
392
- break;
393
- default:
394
- console.error(`Unknown command: ${command}`);
395
- printUsage();
396
- process.exit(1);
397
- }
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 Robert Winter / Complete Ideas
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ /**
6
+ * CLI entrypoint for AgentWorkingMemory.
7
+ *
8
+ * Commands:
9
+ * awm setup — configure MCP for the current project
10
+ * awm mcp — start the MCP server (called by Claude Code)
11
+ * awm serve — start the HTTP API server
12
+ * awm health — check if a running server is healthy
13
+ */
14
+
15
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
16
+ import { resolve, basename, join, dirname } from 'node:path';
17
+ import { execSync } from 'node:child_process';
18
+ import { randomBytes } from 'node:crypto';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { homedir as osHomedir } from 'node:os';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ // Load .env if present
26
+ try {
27
+ const envPath = resolve(process.cwd(), '.env');
28
+ const envContent = readFileSync(envPath, 'utf-8');
29
+ for (const line of envContent.split('\n')) {
30
+ const trimmed = line.trim();
31
+ if (!trimmed || trimmed.startsWith('#')) continue;
32
+ const eqIdx = trimmed.indexOf('=');
33
+ if (eqIdx === -1) continue;
34
+ const key = trimmed.slice(0, eqIdx).trim();
35
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
36
+ if (!process.env[key]) process.env[key] = val;
37
+ }
38
+ } catch { /* No .env file */ }
39
+
40
+ const args = process.argv.slice(2);
41
+ const command = args[0];
42
+
43
+ function printUsage() {
44
+ console.log(`
45
+ AgentWorkingMemory — Cognitive memory for AI agents
46
+
47
+ Usage:
48
+ awm setup [--global] [--agent-id <id>] [--db-path <path>] [--no-claude-md]
49
+ [--no-hooks] [--hook-port <port>] Configure MCP for Claude Code
50
+ awm mcp Start MCP server (used by Claude Code)
51
+ awm serve [--port <port>] Start HTTP API server
52
+ awm health [--port <port>] Check server health
53
+
54
+ Setup:
55
+ awm setup --global Recommended. Writes ~/.mcp.json so AWM is available
56
+ in every project — one brain across all your work.
57
+
58
+ awm setup Project-level. Writes .mcp.json in the current directory
59
+ and appends workflow instructions to CLAUDE.md.
60
+
61
+ --no-claude-md Skip CLAUDE.md modification
62
+ --no-hooks Skip hook installation (no auto-checkpoint)
63
+ --hook-port PORT Sidecar port for hooks (default: 8401)
64
+
65
+ Restart Claude Code after setup to pick up the new MCP server.
66
+ `.trim());
67
+ }
68
+
69
+ // ─── SETUP ──────────────────────────────────────
70
+
71
+ function setup() {
72
+ const cwd = process.cwd();
73
+ const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
74
+
75
+ // Parse flags
76
+ let agentId = projectName;
77
+ let dbPath: string | null = null;
78
+ let skipClaudeMd = false;
79
+ let isGlobal = false;
80
+ let skipHooks = false;
81
+ let hookPort = '8401';
82
+
83
+ for (let i = 1; i < args.length; i++) {
84
+ if (args[i] === '--agent-id' && args[i + 1]) {
85
+ agentId = args[++i];
86
+ } else if (args[i] === '--db-path' && args[i + 1]) {
87
+ dbPath = args[++i];
88
+ } else if (args[i] === '--no-claude-md') {
89
+ skipClaudeMd = true;
90
+ } else if (args[i] === '--no-hooks') {
91
+ skipHooks = true;
92
+ } else if (args[i] === '--hook-port' && args[i + 1]) {
93
+ hookPort = args[++i];
94
+ } else if (args[i] === '--global') {
95
+ isGlobal = true;
96
+ agentId = 'claude'; // unified agent ID for global setup
97
+ }
98
+ }
99
+
100
+ // Find the package root (where src/mcp.ts lives)
101
+ const packageRoot = resolve(__dirname, '..');
102
+ const mcpScript = join(packageRoot, 'src', 'mcp.ts');
103
+ const mcpDist = join(packageRoot, 'dist', 'mcp.js');
104
+
105
+ // Determine DB path — default to <awm-root>/data/memory.db (shared across projects)
106
+ if (!dbPath) {
107
+ dbPath = join(packageRoot, 'data', 'memory.db');
108
+ }
109
+ const dbDir = dirname(dbPath);
110
+
111
+ // Ensure data directory exists
112
+ if (!existsSync(dbDir)) {
113
+ mkdirSync(dbDir, { recursive: true });
114
+ console.log(`Created data directory: ${dbDir}`);
115
+ }
116
+
117
+ // Generate hook secret (or reuse existing one)
118
+ let hookSecret = '';
119
+ const secretPath = join(dirname(dbPath!), '.awm-hook-secret');
120
+ if (existsSync(secretPath)) {
121
+ hookSecret = readFileSync(secretPath, 'utf-8').trim();
122
+ }
123
+ if (!hookSecret) {
124
+ hookSecret = randomBytes(32).toString('hex');
125
+ mkdirSync(dirname(secretPath), { recursive: true });
126
+ writeFileSync(secretPath, hookSecret + '\n');
127
+ }
128
+
129
+ // Determine command based on platform and whether dist exists
130
+ const isWindows = process.platform === 'win32';
131
+ const hasDist = existsSync(mcpDist);
132
+
133
+ const envVars: Record<string, string> = {
134
+ AWM_DB_PATH: (isWindows ? dbPath!.replace(/\\/g, '/') : dbPath!),
135
+ AWM_AGENT_ID: agentId,
136
+ AWM_HOOK_PORT: hookPort,
137
+ AWM_HOOK_SECRET: hookSecret,
138
+ };
139
+
140
+ let mcpConfig: { command: string; args: string[]; env: Record<string, string> };
141
+
142
+ if (hasDist) {
143
+ mcpConfig = {
144
+ command: 'node',
145
+ args: [mcpDist.replace(/\\/g, '/')],
146
+ env: envVars,
147
+ };
148
+ } else if (isWindows) {
149
+ mcpConfig = {
150
+ command: 'cmd',
151
+ args: ['/c', 'npx', 'tsx', mcpScript.replace(/\\/g, '/')],
152
+ env: envVars,
153
+ };
154
+ } else {
155
+ mcpConfig = {
156
+ command: 'npx',
157
+ args: ['tsx', mcpScript],
158
+ env: envVars,
159
+ };
160
+ }
161
+
162
+ // Read or create .mcp.json
163
+ const mcpJsonPath = isGlobal ? join(osHomedir(), '.mcp.json') : join(cwd, '.mcp.json');
164
+ let existing: any = { mcpServers: {} };
165
+ if (existsSync(mcpJsonPath)) {
166
+ try {
167
+ existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
168
+ if (!existing.mcpServers) existing.mcpServers = {};
169
+ } catch {
170
+ existing = { mcpServers: {} };
171
+ }
172
+ }
173
+
174
+ existing.mcpServers['agent-working-memory'] = mcpConfig;
175
+ writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
176
+
177
+ // Auto-append CLAUDE.md snippet unless --no-claude-md
178
+ let claudeMdAction = '';
179
+ const claudeMdSnippet = `
180
+
181
+ ## Memory (AWM)
182
+ You have persistent memory via the agent-working-memory MCP server.
183
+
184
+ ### Lifecycle (always do these)
185
+ - Session start: call memory_restore to recover previous context
186
+ - Starting a task: call memory_task_begin (checkpoints + recalls relevant memories)
187
+ - Finishing a task: call memory_task_end with a summary
188
+ - Auto-checkpoint: hooks handle compaction, session end, and 15-min timer (no action needed)
189
+
190
+ ### Write memory when:
191
+ - A project decision is made or changed
192
+ - A root cause is discovered after debugging
193
+ - A reusable implementation pattern is established
194
+ - A user preference, constraint, or requirement is clarified
195
+ - A prior assumption is found to be wrong
196
+ - A significant piece of work is completed
197
+
198
+ ### Recall memory when:
199
+ - Starting work on a new task or subsystem
200
+ - Re-entering code you haven't touched recently
201
+ - After a failed attempt — check if there's prior knowledge
202
+ - Before refactoring or making architectural changes
203
+ - When a topic comes up that you might have prior context on
204
+
205
+ ### Also:
206
+ - After using a recalled memory: call memory_feedback (useful/not-useful)
207
+ - To correct wrong info: call memory_retract
208
+ - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
209
+ `;
210
+
211
+ // For global: write to ~/.claude/CLAUDE.md (loaded by Claude Code in every session)
212
+ // For project: write to ./CLAUDE.md in the current directory
213
+ const claudeMdPath = isGlobal
214
+ ? join(osHomedir(), '.claude', 'CLAUDE.md')
215
+ : join(cwd, 'CLAUDE.md');
216
+
217
+ // Ensure parent directory exists (for ~/.claude/CLAUDE.md)
218
+ const claudeMdDir = dirname(claudeMdPath);
219
+ if (!existsSync(claudeMdDir)) {
220
+ mkdirSync(claudeMdDir, { recursive: true });
221
+ }
222
+
223
+ if (skipClaudeMd) {
224
+ claudeMdAction = ' CLAUDE.md: skipped (--no-claude-md)';
225
+ } else if (existsSync(claudeMdPath)) {
226
+ const content = readFileSync(claudeMdPath, 'utf-8');
227
+ if (content.includes('## Memory (AWM)')) {
228
+ claudeMdAction = ' CLAUDE.md: already has AWM section (skipped)';
229
+ } else {
230
+ writeFileSync(claudeMdPath, content.trimEnd() + '\n' + claudeMdSnippet);
231
+ claudeMdAction = ' CLAUDE.md: appended AWM workflow section';
232
+ }
233
+ } else {
234
+ const title = isGlobal ? '# Global Instructions' : `# ${basename(cwd)}`;
235
+ writeFileSync(claudeMdPath, `${title}\n${claudeMdSnippet}`);
236
+ claudeMdAction = ' CLAUDE.md: created with AWM workflow section';
237
+ }
238
+
239
+ // --- Hook configuration ---
240
+ let hookAction = '';
241
+ if (skipHooks) {
242
+ hookAction = ' Hooks: skipped (--no-hooks)';
243
+ } else {
244
+ // Write hooks to Claude Code settings (~/.claude/settings.json)
245
+ const settingsPath = join(osHomedir(), '.claude', 'settings.json');
246
+ let settings: any = {};
247
+ if (existsSync(settingsPath)) {
248
+ try {
249
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
250
+ } catch {
251
+ settings = {};
252
+ }
253
+ }
254
+
255
+ if (!settings.hooks) settings.hooks = {};
256
+
257
+ const hookUrl = `http://127.0.0.1:${hookPort}/hooks/checkpoint`;
258
+
259
+ // Stop — remind Claude to write/recall/switch tasks after each response
260
+ settings.hooks.Stop = [{
261
+ matcher: '',
262
+ hooks: [{
263
+ type: 'command',
264
+ command: 'echo "MEMORY: (1) Did you learn anything new? Call memory_write. (2) Are you about to work on a topic you might have prior knowledge about? Call memory_recall. (3) Switching tasks? Call memory_task_begin."',
265
+ timeout: 5,
266
+ async: true,
267
+ }],
268
+ }];
269
+
270
+ // Build hook command with multi-port fallback for separate memory pools.
271
+ // When users have work (port 8401) and personal (port 8402) pools via
272
+ // per-folder .mcp.json, the hook needs to try both ports since the global
273
+ // settings.json can't know which pool is active in the current session.
274
+ const altPort = hookPort === '8401' ? '8402' : '8401';
275
+ const hookUrlAlt = `http://127.0.0.1:${altPort}/hooks/checkpoint`;
276
+ const buildHookCmd = (event: string, maxTime: number) => {
277
+ const primary = `curl -sf -X POST ${hookUrl} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
278
+ const fallback = `curl -sf -X POST ${hookUrlAlt} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
279
+ return `${primary} || ${fallback}`;
280
+ };
281
+
282
+ // PreCompact — auto-checkpoint before context compaction
283
+ settings.hooks.PreCompact = [{
284
+ matcher: '',
285
+ hooks: [{
286
+ type: 'command',
287
+ command: buildHookCmd('PreCompact', 5),
288
+ timeout: 10,
289
+ }],
290
+ }];
291
+
292
+ // SessionEnd — auto-checkpoint on session close (fast timeout to avoid cancellation)
293
+ settings.hooks.SessionEnd = [{
294
+ matcher: '',
295
+ hooks: [{
296
+ type: 'command',
297
+ command: buildHookCmd('SessionEnd', 2),
298
+ timeout: 5,
299
+ }],
300
+ }];
301
+
302
+ // Ensure settings directory exists
303
+ const settingsDir = dirname(settingsPath);
304
+ if (!existsSync(settingsDir)) {
305
+ mkdirSync(settingsDir, { recursive: true });
306
+ }
307
+
308
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
309
+ hookAction = ` Hooks: Stop (memory reminder) + PreCompact + SessionEnd → auto-checkpoint (port ${hookPort})`;
310
+ }
311
+
312
+ const scope = isGlobal ? 'globally (all projects)' : cwd;
313
+ console.log(`
314
+ AWM configured ${isGlobal ? 'globally' : 'for: ' + cwd}
315
+
316
+ Agent ID: ${agentId}
317
+ DB path: ${dbPath}
318
+ MCP config: ${mcpJsonPath}
319
+ Hook port: ${hookPort}
320
+ Hook secret: ${hookSecret.slice(0, 8)}...
321
+ ${claudeMdAction}
322
+ ${hookAction}
323
+
324
+ Next steps:
325
+ 1. Restart Claude Code to pick up the MCP server
326
+ 2. The memory tools will appear automatically
327
+ 3. Hooks auto-checkpoint on context compaction and session end${isGlobal ? '\n 4. One brain across all your projects — no per-project setup needed' : ''}
328
+ `.trim());
329
+ }
330
+
331
+ // ─── MCP ──────────────────────────────────────
332
+
333
+ async function mcp() {
334
+ // Dynamic import to avoid loading heavy deps for setup/health commands
335
+ await import('./mcp.js');
336
+ }
337
+
338
+ // ─── SERVE ──────────────────────────────────────
339
+
340
+ async function serve() {
341
+ // Parse --port flag
342
+ for (let i = 1; i < args.length; i++) {
343
+ if (args[i] === '--port' && args[i + 1]) {
344
+ process.env.AWM_PORT = args[++i];
345
+ }
346
+ }
347
+ await import('./index.js');
348
+ }
349
+
350
+ // ─── HEALTH ──────────────────────────────────────
351
+
352
+ function health() {
353
+ let port = '8400';
354
+ for (let i = 1; i < args.length; i++) {
355
+ if (args[i] === '--port' && args[i + 1]) {
356
+ port = args[++i];
357
+ }
358
+ }
359
+
360
+ try {
361
+ const result = execSync(`curl -sf http://localhost:${port}/health`, {
362
+ encoding: 'utf8',
363
+ timeout: 5000,
364
+ });
365
+ const data = JSON.parse(result);
366
+ console.log(`OK — v${data.version} (${data.timestamp})`);
367
+ } catch {
368
+ console.error(`Cannot reach AWM server on port ${port}`);
369
+ process.exit(1);
370
+ }
371
+ }
372
+
373
+ // ─── Dispatch ──────────────────────────────────────
374
+
375
+ switch (command) {
376
+ case 'setup':
377
+ setup();
378
+ break;
379
+ case 'mcp':
380
+ mcp();
381
+ break;
382
+ case 'serve':
383
+ serve();
384
+ break;
385
+ case 'health':
386
+ health();
387
+ break;
388
+ case '--help':
389
+ case '-h':
390
+ case undefined:
391
+ printUsage();
392
+ break;
393
+ default:
394
+ console.error(`Unknown command: ${command}`);
395
+ printUsage();
396
+ process.exit(1);
397
+ }