agent-working-memory 0.7.0 → 0.7.2

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 (40) hide show
  1. package/README.md +20 -5
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +9 -1
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/api/routes.d.ts.map +1 -1
  6. package/dist/api/routes.js +107 -10
  7. package/dist/api/routes.js.map +1 -1
  8. package/dist/cli.js +103 -103
  9. package/dist/core/auto-tagger.d.ts +29 -0
  10. package/dist/core/auto-tagger.d.ts.map +1 -0
  11. package/dist/core/auto-tagger.js +139 -0
  12. package/dist/core/auto-tagger.js.map +1 -0
  13. package/dist/core/query-expander.d.ts.map +1 -1
  14. package/dist/core/query-expander.js.map +1 -1
  15. package/dist/core/reranker.d.ts.map +1 -1
  16. package/dist/core/reranker.js.map +1 -1
  17. package/dist/engine/consolidation.d.ts +1 -0
  18. package/dist/engine/consolidation.d.ts.map +1 -1
  19. package/dist/engine/consolidation.js +149 -9
  20. package/dist/engine/consolidation.js.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/mcp.js +114 -83
  23. package/dist/mcp.js.map +1 -1
  24. package/dist/storage/sqlite.d.ts.map +1 -1
  25. package/dist/storage/sqlite.js +6 -5
  26. package/dist/storage/sqlite.js.map +1 -1
  27. package/dist/types/engram.d.ts +1 -0
  28. package/dist/types/engram.d.ts.map +1 -1
  29. package/package.json +57 -57
  30. package/src/adapters/common.ts +9 -1
  31. package/src/api/routes.ts +723 -602
  32. package/src/cli.ts +719 -719
  33. package/src/core/auto-tagger.ts +168 -0
  34. package/src/core/query-expander.ts +0 -1
  35. package/src/core/reranker.ts +0 -1
  36. package/src/engine/consolidation.ts +165 -9
  37. package/src/index.ts +199 -199
  38. package/src/mcp.ts +1192 -1166
  39. package/src/storage/sqlite.ts +6 -5
  40. package/src/types/engram.ts +1 -0
package/src/cli.ts CHANGED
@@ -1,719 +1,719 @@
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, join, dirname } from 'node:path';
17
- import { execSync } from 'node:child_process';
18
- import { randomUUID } from 'node:crypto';
19
- import { fileURLToPath } from 'node:url';
20
-
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = dirname(__filename);
23
-
24
- // Load .env if present
25
- try {
26
- const envPath = resolve(process.cwd(), '.env');
27
- const envContent = readFileSync(envPath, 'utf-8');
28
- for (const line of envContent.split('\n')) {
29
- const trimmed = line.trim();
30
- if (!trimmed || trimmed.startsWith('#')) continue;
31
- const eqIdx = trimmed.indexOf('=');
32
- if (eqIdx === -1) continue;
33
- const key = trimmed.slice(0, eqIdx).trim();
34
- const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
35
- if (!process.env[key]) process.env[key] = val;
36
- }
37
- } catch { /* No .env file */ }
38
-
39
- const args = process.argv.slice(2);
40
- const command = args[0];
41
-
42
- function printUsage() {
43
- console.log(`
44
- AgentWorkingMemory — Cognitive memory for AI agents
45
-
46
- Usage:
47
- awm setup [target] [options] Configure AWM for an AI CLI
48
- awm doctor [target|--all] Validate AWM integrations
49
- awm mcp Start MCP server (stdio)
50
- awm serve [--port <port>] Start HTTP API server
51
- awm health [--port <port>] Check server health
52
- awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
53
- Export memories to JSON
54
- awm import <file> --db <path> [--remap-agent <id>] [--dedupe] [--dry-run]
55
- Import memories from JSON
56
- awm merge --target <db> --source <db> [--source ...]
57
- [--remap uuid=name] [--remap-all-uuids <name>]
58
- [--dedupe] [--dry-run] Merge multiple memory DBs
59
-
60
- Setup targets:
61
- claude-code (default) .mcp.json + CLAUDE.md + hooks
62
- codex ~/.codex/config.toml + AGENTS.md
63
- cursor .cursor/mcp.json + .cursorrules
64
- http Connection info for HTTP API
65
-
66
- Setup options:
67
- --global Use global scope (recommended for claude-code)
68
- --agent-id <id> Agent identifier (default: project name)
69
- --db-path <path> Database path (default: <awm>/data/memory.db)
70
- --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
71
- --no-claude-md Alias for --no-instructions
72
- --no-hooks Skip hook installation
73
- --hook-port PORT Sidecar port for hooks (default: 8401)
74
-
75
- Examples:
76
- awm setup --global Claude Code, global (recommended)
77
- awm setup codex Codex CLI
78
- awm setup cursor Cursor IDE
79
- awm setup http Generic HTTP integration
80
- awm doctor --all Check all configured targets
81
- `.trim());
82
- }
83
-
84
- // ─── SETUP ──────────────────────────────────────
85
-
86
- async function setup() {
87
- // Parse flags
88
- let target = 'claude-code';
89
- let agentId: string | undefined;
90
- let dbPath: string | null = null;
91
- let skipInstructions = false;
92
- let isGlobal = false;
93
- let skipHooks = false;
94
- let hookPort = '8401';
95
-
96
- for (let i = 1; i < args.length; i++) {
97
- if (args[i] === '--agent-id' && args[i + 1]) {
98
- agentId = args[++i];
99
- } else if (args[i] === '--db-path' && args[i + 1]) {
100
- dbPath = args[++i];
101
- } else if (args[i] === '--no-claude-md' || args[i] === '--no-instructions') {
102
- skipInstructions = true;
103
- } else if (args[i] === '--no-hooks') {
104
- skipHooks = true;
105
- } else if (args[i] === '--hook-port' && args[i + 1]) {
106
- hookPort = args[++i];
107
- } else if (args[i] === '--global') {
108
- isGlobal = true;
109
- } else if (!args[i].startsWith('--')) {
110
- // Positional arg = target
111
- target = args[i];
112
- }
113
- }
114
-
115
- // Load adapter
116
- const { getAdapter } = await import('./adapters/index.js');
117
- const { buildSetupContext } = await import('./adapters/common.js');
118
-
119
- let adapter;
120
- try {
121
- adapter = await getAdapter(target);
122
- } catch (e: any) {
123
- console.error(e.message);
124
- process.exit(1);
125
- }
126
-
127
- // Force global for adapters that don't support project scope
128
- if (!adapter.supportsProjectScope && !isGlobal) {
129
- isGlobal = true;
130
- }
131
-
132
- // Build context
133
- const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort });
134
-
135
- // Run adapter
136
- const configAction = adapter.writeMcpConfig(ctx);
137
- const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
138
- const hooksAction = adapter.writeHooks(ctx, skipHooks);
139
-
140
- console.log(`
141
- AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
142
-
143
- Agent ID: ${ctx.agentId}
144
- DB path: ${ctx.dbPath}
145
- ${configAction}
146
- ${instructionsAction}
147
- ${hooksAction}
148
-
149
- Next steps:
150
- 1. Restart ${adapter.name} to pick up the MCP server
151
- 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
152
- `.trim());
153
- }
154
-
155
- // ─── DOCTOR ──────────────────────────────────────
156
-
157
- async function doctor() {
158
- const { getAdapter, listAdapters } = await import('./adapters/index.js');
159
- const { buildSetupContext } = await import('./adapters/common.js');
160
-
161
- let targets: string[] = [];
162
- let checkAll = false;
163
-
164
- for (let i = 1; i < args.length; i++) {
165
- if (args[i] === '--all') {
166
- checkAll = true;
167
- } else if (!args[i].startsWith('--')) {
168
- targets.push(args[i]);
169
- }
170
- }
171
-
172
- if (checkAll) {
173
- targets = listAdapters();
174
- } else if (targets.length === 0) {
175
- targets = listAdapters();
176
- }
177
-
178
- const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
179
-
180
- console.log('AWM Doctor\n');
181
-
182
- for (const targetId of targets) {
183
- let adapter;
184
- try {
185
- adapter = await getAdapter(targetId);
186
- } catch {
187
- console.log(` ? ${targetId}: unknown target (skipped)`);
188
- continue;
189
- }
190
-
191
- console.log(` ${adapter.name}:`);
192
- const results = adapter.diagnose(ctx);
193
- for (const r of results) {
194
- const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
195
- console.log(` [${icon}] ${r.check}: ${r.message}`);
196
- if (r.fix) {
197
- console.log(` Fix: ${r.fix}`);
198
- }
199
- }
200
- console.log();
201
- }
202
- }
203
-
204
- // ─── MCP ──────────────────────────────────────
205
-
206
- async function mcp() {
207
- // Dynamic import to avoid loading heavy deps for setup/health commands
208
- await import('./mcp.js');
209
- }
210
-
211
- // ─── SERVE ──────────────────────────────────────
212
-
213
- async function serve() {
214
- // Parse --port flag
215
- for (let i = 1; i < args.length; i++) {
216
- if (args[i] === '--port' && args[i + 1]) {
217
- process.env.AWM_PORT = args[++i];
218
- }
219
- }
220
- await import('./index.js');
221
- }
222
-
223
- // ─── HEALTH ──────────────────────────────────────
224
-
225
- function health() {
226
- let port = '8400';
227
- for (let i = 1; i < args.length; i++) {
228
- if (args[i] === '--port' && args[i + 1]) {
229
- port = args[++i];
230
- }
231
- }
232
-
233
- try {
234
- const result = execSync(`curl -sf http://localhost:${port}/health`, {
235
- encoding: 'utf8',
236
- timeout: 5000,
237
- });
238
- const data = JSON.parse(result);
239
- console.log(`OK — v${data.version} (${data.timestamp})`);
240
- } catch {
241
- console.error(`Cannot reach AWM server on port ${port}`);
242
- process.exit(1);
243
- }
244
- }
245
-
246
- // ─── EXPORT ──────────────────────────────────────
247
-
248
- async function exportMemories() {
249
- let dbPath = '';
250
- let agentFilter: string | null = null;
251
- let outputPath: string | null = null;
252
- let activeOnly = false;
253
-
254
- for (let i = 1; i < args.length; i++) {
255
- if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
256
- else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
257
- else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
258
- else if (args[i] === '--active-only') activeOnly = true;
259
- }
260
-
261
- if (!dbPath) {
262
- console.error('Error: --db <path> is required');
263
- process.exit(1);
264
- }
265
-
266
- if (!existsSync(dbPath)) {
267
- console.error(`Error: database not found: ${dbPath}`);
268
- process.exit(1);
269
- }
270
-
271
- // Dynamic import to avoid loading better-sqlite3 for other commands
272
- const Database = (await import('better-sqlite3')).default;
273
- const db = new Database(dbPath, { readonly: true });
274
-
275
- // Build memory query
276
- let memQuery = 'SELECT * FROM engrams';
277
- const conditions: string[] = [];
278
- const params: any[] = [];
279
-
280
- if (agentFilter) {
281
- conditions.push('agent_id = ?');
282
- params.push(agentFilter);
283
- }
284
- if (activeOnly) {
285
- conditions.push('retracted = 0');
286
- }
287
-
288
- if (conditions.length > 0) {
289
- memQuery += ' WHERE ' + conditions.join(' AND ');
290
- }
291
- memQuery += ' ORDER BY created_at ASC';
292
-
293
- const rows = db.prepare(memQuery).all(...params) as any[];
294
-
295
- // Build memory objects (exclude embedding blobs)
296
- const memories = rows.map((r: any) => ({
297
- id: r.id,
298
- agent_id: r.agent_id,
299
- concept: r.concept,
300
- content: r.content,
301
- confidence: r.confidence,
302
- salience: r.salience,
303
- access_count: r.access_count,
304
- last_accessed: r.last_accessed,
305
- created_at: r.created_at,
306
- stage: r.stage,
307
- tags: r.tags ? JSON.parse(r.tags) : [],
308
- memory_class: r.memory_class ?? 'working',
309
- episode_id: r.episode_id ?? null,
310
- task_status: r.task_status ?? null,
311
- task_priority: r.task_priority ?? null,
312
- supersedes: r.supersedes ?? null,
313
- superseded_by: r.superseded_by ?? null,
314
- retracted: r.retracted ?? 0,
315
- }));
316
-
317
- // Get memory IDs for association filtering
318
- const memIds = new Set(memories.map((m: any) => m.id));
319
-
320
- // Build associations
321
- let assocQuery = 'SELECT * FROM associations';
322
- const allAssocs = db.prepare(assocQuery).all() as any[];
323
- const associations = allAssocs
324
- .filter((a: any) => memIds.has(a.from_engram_id) && memIds.has(a.to_engram_id))
325
- .map((a: any) => ({
326
- from_id: a.from_engram_id,
327
- to_id: a.to_engram_id,
328
- weight: a.weight,
329
- type: a.type ?? 'hebbian',
330
- activation_count: a.activation_count ?? 0,
331
- }));
332
-
333
- // Collect unique agents
334
- const agents = [...new Set(memories.map((m: any) => m.agent_id))];
335
-
336
- const exportData = {
337
- version: '0.6.0',
338
- exported_at: new Date().toISOString(),
339
- source_db: dbPath,
340
- agent_filter: agentFilter,
341
- memories,
342
- associations,
343
- stats: {
344
- total_memories: memories.length,
345
- total_associations: associations.length,
346
- agents,
347
- },
348
- };
349
-
350
- const json = JSON.stringify(exportData, null, 2);
351
-
352
- if (outputPath) {
353
- writeFileSync(outputPath, json + '\n');
354
- console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath}`);
355
- } else {
356
- process.stdout.write(json + '\n');
357
- }
358
-
359
- db.close();
360
- }
361
-
362
- // ─── IMPORT ──────────────────────────────────────
363
-
364
- async function importMemories() {
365
- let filePath = '';
366
- let dbPath = '';
367
- let remapAgent: string | null = null;
368
- let dedupe = false;
369
- let dryRun = false;
370
- let includeRetracted = false;
371
-
372
- // First non-flag arg after 'import' is the file path
373
- for (let i = 1; i < args.length; i++) {
374
- if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
375
- else if (args[i] === '--remap-agent' && args[i + 1]) remapAgent = args[++i];
376
- else if (args[i] === '--dedupe') dedupe = true;
377
- else if (args[i] === '--dry-run') dryRun = true;
378
- else if (args[i] === '--include-retracted') includeRetracted = true;
379
- else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
380
- }
381
-
382
- if (!filePath) {
383
- console.error('Error: <file> is required');
384
- process.exit(1);
385
- }
386
- if (!dbPath) {
387
- console.error('Error: --db <path> is required');
388
- process.exit(1);
389
- }
390
- if (!existsSync(filePath)) {
391
- console.error(`Error: import file not found: ${filePath}`);
392
- process.exit(1);
393
- }
394
-
395
- const importData = JSON.parse(readFileSync(filePath, 'utf-8'));
396
- if (!importData.memories || !Array.isArray(importData.memories)) {
397
- console.error('Error: invalid export file — missing memories array');
398
- process.exit(1);
399
- }
400
-
401
- const Database = (await import('better-sqlite3')).default;
402
- const db = new Database(dbPath);
403
-
404
- // Ensure tables exist in target
405
- db.exec(`
406
- CREATE TABLE IF NOT EXISTS engrams (
407
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
408
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
409
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
410
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
411
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
412
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]',
413
- episode_id TEXT, task_status TEXT, task_priority TEXT, blocked_by TEXT,
414
- memory_class TEXT NOT NULL DEFAULT 'working', superseded_by TEXT, supersedes TEXT
415
- );
416
- CREATE TABLE IF NOT EXISTS associations (
417
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
418
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
419
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
420
- created_at TEXT NOT NULL, last_activated TEXT
421
- );
422
- `);
423
-
424
- // Build dedup set if needed
425
- const existingHashes = new Set<string>();
426
- if (dedupe) {
427
- const existing = db.prepare('SELECT concept, content FROM engrams').all() as any[];
428
- for (const row of existing) {
429
- const hash = (row.concept ?? '').toLowerCase().trim() + '||' + (row.content ?? '').toLowerCase().trim();
430
- existingHashes.add(hash);
431
- }
432
- }
433
- const idMap = new Map<string, string>();
434
- let imported = 0;
435
- let skippedDupes = 0;
436
- let skippedRetracted = 0;
437
-
438
- const insertMem = db.prepare(`
439
- INSERT INTO engrams (id, agent_id, concept, content, confidence, salience,
440
- access_count, last_accessed, created_at, stage, tags, memory_class,
441
- episode_id, task_status, task_priority, supersedes, superseded_by, retracted)
442
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
443
- `);
444
-
445
- const insertAssoc = db.prepare(`
446
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at)
447
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
448
- `);
449
-
450
- const importTx = db.transaction(() => {
451
- // Import memories
452
- for (const mem of importData.memories) {
453
- // Skip retracted unless --include-retracted
454
- if (mem.retracted && !includeRetracted) {
455
- skippedRetracted++;
456
- continue;
457
- }
458
-
459
- // Dedupe check
460
- if (dedupe) {
461
- const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
462
- if (existingHashes.has(hash)) {
463
- skippedDupes++;
464
- continue;
465
- }
466
- }
467
-
468
- const newId = randomUUID();
469
- idMap.set(mem.id, newId);
470
-
471
- const agentId = remapAgent ?? mem.agent_id;
472
- const tags = Array.isArray(mem.tags) ? JSON.stringify(mem.tags) : (mem.tags ?? '[]');
473
-
474
- if (!dryRun) {
475
- insertMem.run(
476
- newId, agentId, mem.concept, mem.content,
477
- mem.confidence ?? 0.5, mem.salience ?? 0.5,
478
- mem.access_count ?? 0, mem.last_accessed ?? mem.created_at,
479
- mem.created_at, mem.stage ?? 'active', tags,
480
- mem.memory_class ?? 'working', mem.episode_id ?? null,
481
- mem.task_status ?? null, mem.task_priority ?? null,
482
- mem.supersedes ?? null, mem.superseded_by ?? null,
483
- mem.retracted ?? 0
484
- );
485
- }
486
- imported++;
487
- }
488
-
489
- // Import associations (using remapped IDs)
490
- let assocImported = 0;
491
- const associations = importData.associations ?? [];
492
- for (const assoc of associations) {
493
- const fromId = idMap.get(assoc.from_id);
494
- const toId = idMap.get(assoc.to_id);
495
- if (!fromId || !toId) continue; // skip if either memory was skipped
496
-
497
- if (!dryRun) {
498
- insertAssoc.run(
499
- randomUUID(), fromId, toId,
500
- assoc.weight ?? 0.5, assoc.type ?? 'hebbian',
501
- assoc.activation_count ?? 0
502
- );
503
- }
504
- assocImported++;
505
- }
506
-
507
- return assocImported;
508
- });
509
-
510
- const assocCount = importTx();
511
-
512
- const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
513
- console.log(`${prefix} ${imported} memories, ${assocCount} associations` +
514
- (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
515
- (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
516
- (remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
517
-
518
- db.close();
519
- }
520
-
521
- // ─── MERGE ──────────────────────────────────────
522
-
523
- async function mergeMemories() {
524
- const Database = (await import('better-sqlite3')).default;
525
- const { createHash, randomUUID } = await import('node:crypto');
526
-
527
- let target = '';
528
- const sources: string[] = [];
529
- const remapEntries = new Map<string, string>();
530
- let remapAllUuids = '';
531
- let dedupe = false;
532
- let dryRun = false;
533
-
534
- for (let i = 1; i < args.length; i++) {
535
- if (args[i] === '--target' && args[i + 1]) {
536
- target = args[++i];
537
- } else if (args[i] === '--source' && args[i + 1]) {
538
- sources.push(args[++i]);
539
- } else if (args[i] === '--remap' && args[i + 1]) {
540
- const val = args[++i];
541
- const eqIdx = val.indexOf('=');
542
- if (eqIdx > 0) remapEntries.set(val.slice(0, eqIdx), val.slice(eqIdx + 1));
543
- } else if (args[i] === '--remap-all-uuids' && args[i + 1]) {
544
- remapAllUuids = args[++i];
545
- } else if (args[i] === '--dedupe') {
546
- dedupe = true;
547
- } else if (args[i] === '--dry-run') {
548
- dryRun = true;
549
- }
550
- }
551
-
552
- if (!target || sources.length === 0) {
553
- console.error('Usage: awm merge --target <path> --source <path> [--source <path>...] [--remap uuid=name] [--remap-all-uuids name] [--dedupe] [--dry-run]');
554
- process.exit(1);
555
- }
556
-
557
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
558
-
559
- function remapAgentId(agentId: string): string {
560
- if (remapEntries.has(agentId)) return remapEntries.get(agentId)!;
561
- if (remapAllUuids && UUID_RE.test(agentId)) return remapAllUuids;
562
- return agentId;
563
- }
564
-
565
- function contentHash(concept: string, content: string): string {
566
- return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
567
- }
568
-
569
- console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
570
-
571
- const targetDb = new Database(target);
572
- targetDb.pragma('journal_mode = WAL');
573
- targetDb.pragma('foreign_keys = ON');
574
-
575
- // Ensure tables exist in target
576
- targetDb.exec(`
577
- CREATE TABLE IF NOT EXISTS engrams (
578
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
579
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
580
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
581
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
582
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
583
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
584
- );
585
- CREATE TABLE IF NOT EXISTS associations (
586
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
587
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
588
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
589
- created_at TEXT NOT NULL, last_activated TEXT NOT NULL
590
- );
591
- `);
592
-
593
- // Build dedupe hash set from existing target memories
594
- const existingHashes = new Set<string>();
595
- if (dedupe) {
596
- const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
597
- for (const row of rows) existingHashes.add(contentHash(row.concept, row.content));
598
- console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
599
- }
600
-
601
- const insertEngram = targetDb.prepare(`
602
- INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
603
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
604
- retracted, retracted_by, retracted_at, tags)
605
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
606
- `);
607
- const insertAssoc = targetDb.prepare(`
608
- INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
609
- activation_count, created_at, last_activated)
610
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
611
- `);
612
-
613
- let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
614
-
615
- for (const sourcePath of sources) {
616
- if (!existsSync(sourcePath)) {
617
- console.error(` Source not found: ${sourcePath}`);
618
- continue;
619
- }
620
-
621
- const sourceDb = new Database(sourcePath, { readonly: true });
622
- const engrams = sourceDb.prepare(
623
- `SELECT id, agent_id, concept, content, confidence, salience, access_count,
624
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
625
- retracted, retracted_by, retracted_at, tags FROM engrams`
626
- ).all() as any[];
627
- const assocs = sourceDb.prepare(
628
- `SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
629
- activation_count, created_at, last_activated FROM associations`
630
- ).all() as any[];
631
-
632
- const idMap = new Map<string, string>();
633
- const skippedIds = new Set<string>();
634
-
635
- const result = targetDb.transaction(() => {
636
- let imported = 0, skipped = 0;
637
- for (const e of engrams) {
638
- const hash = contentHash(e.concept, e.content);
639
- if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
640
- const newId = randomUUID();
641
- idMap.set(e.id, newId);
642
- existingHashes.add(hash);
643
- if (!dryRun) {
644
- insertEngram.run(newId, remapAgentId(e.agent_id), e.concept, e.content, e.confidence,
645
- e.salience, e.access_count, e.last_accessed, e.created_at, e.salience_features,
646
- e.reason_codes, e.stage, e.ttl, e.retracted, e.retracted_by, e.retracted_at, e.tags);
647
- }
648
- imported++;
649
- }
650
- let assocImported = 0;
651
- for (const a of assocs) {
652
- if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
653
- const fromId = idMap.get(a.from_engram_id);
654
- const toId = idMap.get(a.to_engram_id);
655
- if (!fromId || !toId) continue;
656
- if (!dryRun) {
657
- insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
658
- a.activation_count, a.created_at, a.last_activated);
659
- }
660
- assocImported++;
661
- }
662
- return { imported, skipped, assocImported };
663
- })();
664
-
665
- sourceDb.close();
666
-
667
- const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
668
- console.log(` Source: ${sourcePath}`);
669
- console.log(` Engrams: ${engrams.length} total, ${result.imported} imported, ${result.skipped} skipped`);
670
- console.log(` Associations: ${assocs.length} total, ${result.assocImported} imported`);
671
- console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
672
-
673
- totalMemories += result.imported;
674
- totalAssociations += result.assocImported;
675
- totalSkipped += result.skipped;
676
- }
677
-
678
- targetDb.close();
679
- console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
680
- if (dryRun) console.log('(dry run — no data written)');
681
- }
682
-
683
- // ─── Dispatch ──────────────────────────────────────
684
-
685
- switch (command) {
686
- case 'setup':
687
- await setup();
688
- break;
689
- case 'doctor':
690
- await doctor();
691
- break;
692
- case 'mcp':
693
- mcp();
694
- break;
695
- case 'serve':
696
- serve();
697
- break;
698
- case 'health':
699
- health();
700
- break;
701
- case 'export':
702
- exportMemories();
703
- break;
704
- case 'import':
705
- importMemories();
706
- break;
707
- case 'merge':
708
- mergeMemories();
709
- break;
710
- case '--help':
711
- case '-h':
712
- case undefined:
713
- printUsage();
714
- break;
715
- default:
716
- console.error(`Unknown command: ${command}`);
717
- printUsage();
718
- process.exit(1);
719
- }
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, join, dirname } from 'node:path';
17
+ import { execSync } from 'node:child_process';
18
+ import { randomUUID } from 'node:crypto';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ // Load .env if present
25
+ try {
26
+ const envPath = resolve(process.cwd(), '.env');
27
+ const envContent = readFileSync(envPath, 'utf-8');
28
+ for (const line of envContent.split('\n')) {
29
+ const trimmed = line.trim();
30
+ if (!trimmed || trimmed.startsWith('#')) continue;
31
+ const eqIdx = trimmed.indexOf('=');
32
+ if (eqIdx === -1) continue;
33
+ const key = trimmed.slice(0, eqIdx).trim();
34
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
35
+ if (!process.env[key]) process.env[key] = val;
36
+ }
37
+ } catch { /* No .env file */ }
38
+
39
+ const args = process.argv.slice(2);
40
+ const command = args[0];
41
+
42
+ function printUsage() {
43
+ console.log(`
44
+ AgentWorkingMemory — Cognitive memory for AI agents
45
+
46
+ Usage:
47
+ awm setup [target] [options] Configure AWM for an AI CLI
48
+ awm doctor [target|--all] Validate AWM integrations
49
+ awm mcp Start MCP server (stdio)
50
+ awm serve [--port <port>] Start HTTP API server
51
+ awm health [--port <port>] Check server health
52
+ awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
53
+ Export memories to JSON
54
+ awm import <file> --db <path> [--remap-agent <id>] [--dedupe] [--dry-run]
55
+ Import memories from JSON
56
+ awm merge --target <db> --source <db> [--source ...]
57
+ [--remap uuid=name] [--remap-all-uuids <name>]
58
+ [--dedupe] [--dry-run] Merge multiple memory DBs
59
+
60
+ Setup targets:
61
+ claude-code (default) .mcp.json + CLAUDE.md + hooks
62
+ codex ~/.codex/config.toml + AGENTS.md
63
+ cursor .cursor/mcp.json + .cursorrules
64
+ http Connection info for HTTP API
65
+
66
+ Setup options:
67
+ --global Use global scope (recommended for claude-code)
68
+ --agent-id <id> Agent identifier (default: project name)
69
+ --db-path <path> Database path (default: <awm>/data/memory.db)
70
+ --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
71
+ --no-claude-md Alias for --no-instructions
72
+ --no-hooks Skip hook installation
73
+ --hook-port PORT Sidecar port for hooks (default: 8401)
74
+
75
+ Examples:
76
+ awm setup --global Claude Code, global (recommended)
77
+ awm setup codex Codex CLI
78
+ awm setup cursor Cursor IDE
79
+ awm setup http Generic HTTP integration
80
+ awm doctor --all Check all configured targets
81
+ `.trim());
82
+ }
83
+
84
+ // ─── SETUP ──────────────────────────────────────
85
+
86
+ async function setup() {
87
+ // Parse flags
88
+ let target = 'claude-code';
89
+ let agentId: string | undefined;
90
+ let dbPath: string | null = null;
91
+ let skipInstructions = false;
92
+ let isGlobal = false;
93
+ let skipHooks = false;
94
+ let hookPort = '8401';
95
+
96
+ for (let i = 1; i < args.length; i++) {
97
+ if (args[i] === '--agent-id' && args[i + 1]) {
98
+ agentId = args[++i];
99
+ } else if (args[i] === '--db-path' && args[i + 1]) {
100
+ dbPath = args[++i];
101
+ } else if (args[i] === '--no-claude-md' || args[i] === '--no-instructions') {
102
+ skipInstructions = true;
103
+ } else if (args[i] === '--no-hooks') {
104
+ skipHooks = true;
105
+ } else if (args[i] === '--hook-port' && args[i + 1]) {
106
+ hookPort = args[++i];
107
+ } else if (args[i] === '--global') {
108
+ isGlobal = true;
109
+ } else if (!args[i].startsWith('--')) {
110
+ // Positional arg = target
111
+ target = args[i];
112
+ }
113
+ }
114
+
115
+ // Load adapter
116
+ const { getAdapter } = await import('./adapters/index.js');
117
+ const { buildSetupContext } = await import('./adapters/common.js');
118
+
119
+ let adapter;
120
+ try {
121
+ adapter = await getAdapter(target);
122
+ } catch (e: any) {
123
+ console.error(e.message);
124
+ process.exit(1);
125
+ }
126
+
127
+ // Force global for adapters that don't support project scope
128
+ if (!adapter.supportsProjectScope && !isGlobal) {
129
+ isGlobal = true;
130
+ }
131
+
132
+ // Build context
133
+ const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort });
134
+
135
+ // Run adapter
136
+ const configAction = adapter.writeMcpConfig(ctx);
137
+ const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
138
+ const hooksAction = adapter.writeHooks(ctx, skipHooks);
139
+
140
+ console.log(`
141
+ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
142
+
143
+ Agent ID: ${ctx.agentId}
144
+ DB path: ${ctx.dbPath}
145
+ ${configAction}
146
+ ${instructionsAction}
147
+ ${hooksAction}
148
+
149
+ Next steps:
150
+ 1. Restart ${adapter.name} to pick up the MCP server
151
+ 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
152
+ `.trim());
153
+ }
154
+
155
+ // ─── DOCTOR ──────────────────────────────────────
156
+
157
+ async function doctor() {
158
+ const { getAdapter, listAdapters } = await import('./adapters/index.js');
159
+ const { buildSetupContext } = await import('./adapters/common.js');
160
+
161
+ let targets: string[] = [];
162
+ let checkAll = false;
163
+
164
+ for (let i = 1; i < args.length; i++) {
165
+ if (args[i] === '--all') {
166
+ checkAll = true;
167
+ } else if (!args[i].startsWith('--')) {
168
+ targets.push(args[i]);
169
+ }
170
+ }
171
+
172
+ if (checkAll) {
173
+ targets = listAdapters();
174
+ } else if (targets.length === 0) {
175
+ targets = listAdapters();
176
+ }
177
+
178
+ const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
179
+
180
+ console.log('AWM Doctor\n');
181
+
182
+ for (const targetId of targets) {
183
+ let adapter;
184
+ try {
185
+ adapter = await getAdapter(targetId);
186
+ } catch {
187
+ console.log(` ? ${targetId}: unknown target (skipped)`);
188
+ continue;
189
+ }
190
+
191
+ console.log(` ${adapter.name}:`);
192
+ const results = adapter.diagnose(ctx);
193
+ for (const r of results) {
194
+ const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
195
+ console.log(` [${icon}] ${r.check}: ${r.message}`);
196
+ if (r.fix) {
197
+ console.log(` Fix: ${r.fix}`);
198
+ }
199
+ }
200
+ console.log();
201
+ }
202
+ }
203
+
204
+ // ─── MCP ──────────────────────────────────────
205
+
206
+ async function mcp() {
207
+ // Dynamic import to avoid loading heavy deps for setup/health commands
208
+ await import('./mcp.js');
209
+ }
210
+
211
+ // ─── SERVE ──────────────────────────────────────
212
+
213
+ async function serve() {
214
+ // Parse --port flag
215
+ for (let i = 1; i < args.length; i++) {
216
+ if (args[i] === '--port' && args[i + 1]) {
217
+ process.env.AWM_PORT = args[++i];
218
+ }
219
+ }
220
+ await import('./index.js');
221
+ }
222
+
223
+ // ─── HEALTH ──────────────────────────────────────
224
+
225
+ function health() {
226
+ let port = '8400';
227
+ for (let i = 1; i < args.length; i++) {
228
+ if (args[i] === '--port' && args[i + 1]) {
229
+ port = args[++i];
230
+ }
231
+ }
232
+
233
+ try {
234
+ const result = execSync(`curl -sf http://localhost:${port}/health`, {
235
+ encoding: 'utf8',
236
+ timeout: 5000,
237
+ });
238
+ const data = JSON.parse(result);
239
+ console.log(`OK — v${data.version} (${data.timestamp})`);
240
+ } catch {
241
+ console.error(`Cannot reach AWM server on port ${port}`);
242
+ process.exit(1);
243
+ }
244
+ }
245
+
246
+ // ─── EXPORT ──────────────────────────────────────
247
+
248
+ async function exportMemories() {
249
+ let dbPath = '';
250
+ let agentFilter: string | null = null;
251
+ let outputPath: string | null = null;
252
+ let activeOnly = false;
253
+
254
+ for (let i = 1; i < args.length; i++) {
255
+ if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
256
+ else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
257
+ else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
258
+ else if (args[i] === '--active-only') activeOnly = true;
259
+ }
260
+
261
+ if (!dbPath) {
262
+ console.error('Error: --db <path> is required');
263
+ process.exit(1);
264
+ }
265
+
266
+ if (!existsSync(dbPath)) {
267
+ console.error(`Error: database not found: ${dbPath}`);
268
+ process.exit(1);
269
+ }
270
+
271
+ // Dynamic import to avoid loading better-sqlite3 for other commands
272
+ const Database = (await import('better-sqlite3')).default;
273
+ const db = new Database(dbPath, { readonly: true });
274
+
275
+ // Build memory query
276
+ let memQuery = 'SELECT * FROM engrams';
277
+ const conditions: string[] = [];
278
+ const params: any[] = [];
279
+
280
+ if (agentFilter) {
281
+ conditions.push('agent_id = ?');
282
+ params.push(agentFilter);
283
+ }
284
+ if (activeOnly) {
285
+ conditions.push('retracted = 0');
286
+ }
287
+
288
+ if (conditions.length > 0) {
289
+ memQuery += ' WHERE ' + conditions.join(' AND ');
290
+ }
291
+ memQuery += ' ORDER BY created_at ASC';
292
+
293
+ const rows = db.prepare(memQuery).all(...params) as any[];
294
+
295
+ // Build memory objects (exclude embedding blobs)
296
+ const memories = rows.map((r: any) => ({
297
+ id: r.id,
298
+ agent_id: r.agent_id,
299
+ concept: r.concept,
300
+ content: r.content,
301
+ confidence: r.confidence,
302
+ salience: r.salience,
303
+ access_count: r.access_count,
304
+ last_accessed: r.last_accessed,
305
+ created_at: r.created_at,
306
+ stage: r.stage,
307
+ tags: r.tags ? JSON.parse(r.tags) : [],
308
+ memory_class: r.memory_class ?? 'working',
309
+ episode_id: r.episode_id ?? null,
310
+ task_status: r.task_status ?? null,
311
+ task_priority: r.task_priority ?? null,
312
+ supersedes: r.supersedes ?? null,
313
+ superseded_by: r.superseded_by ?? null,
314
+ retracted: r.retracted ?? 0,
315
+ }));
316
+
317
+ // Get memory IDs for association filtering
318
+ const memIds = new Set(memories.map((m: any) => m.id));
319
+
320
+ // Build associations
321
+ let assocQuery = 'SELECT * FROM associations';
322
+ const allAssocs = db.prepare(assocQuery).all() as any[];
323
+ const associations = allAssocs
324
+ .filter((a: any) => memIds.has(a.from_engram_id) && memIds.has(a.to_engram_id))
325
+ .map((a: any) => ({
326
+ from_id: a.from_engram_id,
327
+ to_id: a.to_engram_id,
328
+ weight: a.weight,
329
+ type: a.type ?? 'hebbian',
330
+ activation_count: a.activation_count ?? 0,
331
+ }));
332
+
333
+ // Collect unique agents
334
+ const agents = [...new Set(memories.map((m: any) => m.agent_id))];
335
+
336
+ const exportData = {
337
+ version: '0.7.2',
338
+ exported_at: new Date().toISOString(),
339
+ source_db: dbPath,
340
+ agent_filter: agentFilter,
341
+ memories,
342
+ associations,
343
+ stats: {
344
+ total_memories: memories.length,
345
+ total_associations: associations.length,
346
+ agents,
347
+ },
348
+ };
349
+
350
+ const json = JSON.stringify(exportData, null, 2);
351
+
352
+ if (outputPath) {
353
+ writeFileSync(outputPath, json + '\n');
354
+ console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath}`);
355
+ } else {
356
+ process.stdout.write(json + '\n');
357
+ }
358
+
359
+ db.close();
360
+ }
361
+
362
+ // ─── IMPORT ──────────────────────────────────────
363
+
364
+ async function importMemories() {
365
+ let filePath = '';
366
+ let dbPath = '';
367
+ let remapAgent: string | null = null;
368
+ let dedupe = false;
369
+ let dryRun = false;
370
+ let includeRetracted = false;
371
+
372
+ // First non-flag arg after 'import' is the file path
373
+ for (let i = 1; i < args.length; i++) {
374
+ if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
375
+ else if (args[i] === '--remap-agent' && args[i + 1]) remapAgent = args[++i];
376
+ else if (args[i] === '--dedupe') dedupe = true;
377
+ else if (args[i] === '--dry-run') dryRun = true;
378
+ else if (args[i] === '--include-retracted') includeRetracted = true;
379
+ else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
380
+ }
381
+
382
+ if (!filePath) {
383
+ console.error('Error: <file> is required');
384
+ process.exit(1);
385
+ }
386
+ if (!dbPath) {
387
+ console.error('Error: --db <path> is required');
388
+ process.exit(1);
389
+ }
390
+ if (!existsSync(filePath)) {
391
+ console.error(`Error: import file not found: ${filePath}`);
392
+ process.exit(1);
393
+ }
394
+
395
+ const importData = JSON.parse(readFileSync(filePath, 'utf-8'));
396
+ if (!importData.memories || !Array.isArray(importData.memories)) {
397
+ console.error('Error: invalid export file — missing memories array');
398
+ process.exit(1);
399
+ }
400
+
401
+ const Database = (await import('better-sqlite3')).default;
402
+ const db = new Database(dbPath);
403
+
404
+ // Ensure tables exist in target
405
+ db.exec(`
406
+ CREATE TABLE IF NOT EXISTS engrams (
407
+ id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
408
+ embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
409
+ access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
410
+ salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
411
+ stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
412
+ retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]',
413
+ episode_id TEXT, task_status TEXT, task_priority TEXT, blocked_by TEXT,
414
+ memory_class TEXT NOT NULL DEFAULT 'working', superseded_by TEXT, supersedes TEXT
415
+ );
416
+ CREATE TABLE IF NOT EXISTS associations (
417
+ id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
418
+ weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
419
+ type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
420
+ created_at TEXT NOT NULL, last_activated TEXT
421
+ );
422
+ `);
423
+
424
+ // Build dedup set if needed
425
+ const existingHashes = new Set<string>();
426
+ if (dedupe) {
427
+ const existing = db.prepare('SELECT concept, content FROM engrams').all() as any[];
428
+ for (const row of existing) {
429
+ const hash = (row.concept ?? '').toLowerCase().trim() + '||' + (row.content ?? '').toLowerCase().trim();
430
+ existingHashes.add(hash);
431
+ }
432
+ }
433
+ const idMap = new Map<string, string>();
434
+ let imported = 0;
435
+ let skippedDupes = 0;
436
+ let skippedRetracted = 0;
437
+
438
+ const insertMem = db.prepare(`
439
+ INSERT INTO engrams (id, agent_id, concept, content, confidence, salience,
440
+ access_count, last_accessed, created_at, stage, tags, memory_class,
441
+ episode_id, task_status, task_priority, supersedes, superseded_by, retracted)
442
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
443
+ `);
444
+
445
+ const insertAssoc = db.prepare(`
446
+ INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at)
447
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
448
+ `);
449
+
450
+ const importTx = db.transaction(() => {
451
+ // Import memories
452
+ for (const mem of importData.memories) {
453
+ // Skip retracted unless --include-retracted
454
+ if (mem.retracted && !includeRetracted) {
455
+ skippedRetracted++;
456
+ continue;
457
+ }
458
+
459
+ // Dedupe check
460
+ if (dedupe) {
461
+ const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
462
+ if (existingHashes.has(hash)) {
463
+ skippedDupes++;
464
+ continue;
465
+ }
466
+ }
467
+
468
+ const newId = randomUUID();
469
+ idMap.set(mem.id, newId);
470
+
471
+ const agentId = remapAgent ?? mem.agent_id;
472
+ const tags = Array.isArray(mem.tags) ? JSON.stringify(mem.tags) : (mem.tags ?? '[]');
473
+
474
+ if (!dryRun) {
475
+ insertMem.run(
476
+ newId, agentId, mem.concept, mem.content,
477
+ mem.confidence ?? 0.5, mem.salience ?? 0.5,
478
+ mem.access_count ?? 0, mem.last_accessed ?? mem.created_at,
479
+ mem.created_at, mem.stage ?? 'active', tags,
480
+ mem.memory_class ?? 'working', mem.episode_id ?? null,
481
+ mem.task_status ?? null, mem.task_priority ?? null,
482
+ mem.supersedes ?? null, mem.superseded_by ?? null,
483
+ mem.retracted ?? 0
484
+ );
485
+ }
486
+ imported++;
487
+ }
488
+
489
+ // Import associations (using remapped IDs)
490
+ let assocImported = 0;
491
+ const associations = importData.associations ?? [];
492
+ for (const assoc of associations) {
493
+ const fromId = idMap.get(assoc.from_id);
494
+ const toId = idMap.get(assoc.to_id);
495
+ if (!fromId || !toId) continue; // skip if either memory was skipped
496
+
497
+ if (!dryRun) {
498
+ insertAssoc.run(
499
+ randomUUID(), fromId, toId,
500
+ assoc.weight ?? 0.5, assoc.type ?? 'hebbian',
501
+ assoc.activation_count ?? 0
502
+ );
503
+ }
504
+ assocImported++;
505
+ }
506
+
507
+ return assocImported;
508
+ });
509
+
510
+ const assocCount = importTx();
511
+
512
+ const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
513
+ console.log(`${prefix} ${imported} memories, ${assocCount} associations` +
514
+ (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
515
+ (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
516
+ (remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
517
+
518
+ db.close();
519
+ }
520
+
521
+ // ─── MERGE ──────────────────────────────────────
522
+
523
+ async function mergeMemories() {
524
+ const Database = (await import('better-sqlite3')).default;
525
+ const { createHash, randomUUID } = await import('node:crypto');
526
+
527
+ let target = '';
528
+ const sources: string[] = [];
529
+ const remapEntries = new Map<string, string>();
530
+ let remapAllUuids = '';
531
+ let dedupe = false;
532
+ let dryRun = false;
533
+
534
+ for (let i = 1; i < args.length; i++) {
535
+ if (args[i] === '--target' && args[i + 1]) {
536
+ target = args[++i];
537
+ } else if (args[i] === '--source' && args[i + 1]) {
538
+ sources.push(args[++i]);
539
+ } else if (args[i] === '--remap' && args[i + 1]) {
540
+ const val = args[++i];
541
+ const eqIdx = val.indexOf('=');
542
+ if (eqIdx > 0) remapEntries.set(val.slice(0, eqIdx), val.slice(eqIdx + 1));
543
+ } else if (args[i] === '--remap-all-uuids' && args[i + 1]) {
544
+ remapAllUuids = args[++i];
545
+ } else if (args[i] === '--dedupe') {
546
+ dedupe = true;
547
+ } else if (args[i] === '--dry-run') {
548
+ dryRun = true;
549
+ }
550
+ }
551
+
552
+ if (!target || sources.length === 0) {
553
+ console.error('Usage: awm merge --target <path> --source <path> [--source <path>...] [--remap uuid=name] [--remap-all-uuids name] [--dedupe] [--dry-run]');
554
+ process.exit(1);
555
+ }
556
+
557
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
558
+
559
+ function remapAgentId(agentId: string): string {
560
+ if (remapEntries.has(agentId)) return remapEntries.get(agentId)!;
561
+ if (remapAllUuids && UUID_RE.test(agentId)) return remapAllUuids;
562
+ return agentId;
563
+ }
564
+
565
+ function contentHash(concept: string, content: string): string {
566
+ return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
567
+ }
568
+
569
+ console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
570
+
571
+ const targetDb = new Database(target);
572
+ targetDb.pragma('journal_mode = WAL');
573
+ targetDb.pragma('foreign_keys = ON');
574
+
575
+ // Ensure tables exist in target
576
+ targetDb.exec(`
577
+ CREATE TABLE IF NOT EXISTS engrams (
578
+ id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
579
+ embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
580
+ access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
581
+ salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
582
+ stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
583
+ retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
584
+ );
585
+ CREATE TABLE IF NOT EXISTS associations (
586
+ id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
587
+ weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
588
+ type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
589
+ created_at TEXT NOT NULL, last_activated TEXT NOT NULL
590
+ );
591
+ `);
592
+
593
+ // Build dedupe hash set from existing target memories
594
+ const existingHashes = new Set<string>();
595
+ if (dedupe) {
596
+ const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
597
+ for (const row of rows) existingHashes.add(contentHash(row.concept, row.content));
598
+ console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
599
+ }
600
+
601
+ const insertEngram = targetDb.prepare(`
602
+ INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
603
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
604
+ retracted, retracted_by, retracted_at, tags)
605
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
606
+ `);
607
+ const insertAssoc = targetDb.prepare(`
608
+ INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
609
+ activation_count, created_at, last_activated)
610
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
611
+ `);
612
+
613
+ let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
614
+
615
+ for (const sourcePath of sources) {
616
+ if (!existsSync(sourcePath)) {
617
+ console.error(` Source not found: ${sourcePath}`);
618
+ continue;
619
+ }
620
+
621
+ const sourceDb = new Database(sourcePath, { readonly: true });
622
+ const engrams = sourceDb.prepare(
623
+ `SELECT id, agent_id, concept, content, confidence, salience, access_count,
624
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
625
+ retracted, retracted_by, retracted_at, tags FROM engrams`
626
+ ).all() as any[];
627
+ const assocs = sourceDb.prepare(
628
+ `SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
629
+ activation_count, created_at, last_activated FROM associations`
630
+ ).all() as any[];
631
+
632
+ const idMap = new Map<string, string>();
633
+ const skippedIds = new Set<string>();
634
+
635
+ const result = targetDb.transaction(() => {
636
+ let imported = 0, skipped = 0;
637
+ for (const e of engrams) {
638
+ const hash = contentHash(e.concept, e.content);
639
+ if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
640
+ const newId = randomUUID();
641
+ idMap.set(e.id, newId);
642
+ existingHashes.add(hash);
643
+ if (!dryRun) {
644
+ insertEngram.run(newId, remapAgentId(e.agent_id), e.concept, e.content, e.confidence,
645
+ e.salience, e.access_count, e.last_accessed, e.created_at, e.salience_features,
646
+ e.reason_codes, e.stage, e.ttl, e.retracted, e.retracted_by, e.retracted_at, e.tags);
647
+ }
648
+ imported++;
649
+ }
650
+ let assocImported = 0;
651
+ for (const a of assocs) {
652
+ if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
653
+ const fromId = idMap.get(a.from_engram_id);
654
+ const toId = idMap.get(a.to_engram_id);
655
+ if (!fromId || !toId) continue;
656
+ if (!dryRun) {
657
+ insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
658
+ a.activation_count, a.created_at, a.last_activated);
659
+ }
660
+ assocImported++;
661
+ }
662
+ return { imported, skipped, assocImported };
663
+ })();
664
+
665
+ sourceDb.close();
666
+
667
+ const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
668
+ console.log(` Source: ${sourcePath}`);
669
+ console.log(` Engrams: ${engrams.length} total, ${result.imported} imported, ${result.skipped} skipped`);
670
+ console.log(` Associations: ${assocs.length} total, ${result.assocImported} imported`);
671
+ console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
672
+
673
+ totalMemories += result.imported;
674
+ totalAssociations += result.assocImported;
675
+ totalSkipped += result.skipped;
676
+ }
677
+
678
+ targetDb.close();
679
+ console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
680
+ if (dryRun) console.log('(dry run — no data written)');
681
+ }
682
+
683
+ // ─── Dispatch ──────────────────────────────────────
684
+
685
+ switch (command) {
686
+ case 'setup':
687
+ await setup();
688
+ break;
689
+ case 'doctor':
690
+ await doctor();
691
+ break;
692
+ case 'mcp':
693
+ mcp();
694
+ break;
695
+ case 'serve':
696
+ serve();
697
+ break;
698
+ case 'health':
699
+ health();
700
+ break;
701
+ case 'export':
702
+ exportMemories();
703
+ break;
704
+ case 'import':
705
+ importMemories();
706
+ break;
707
+ case 'merge':
708
+ mergeMemories();
709
+ break;
710
+ case '--help':
711
+ case '-h':
712
+ case undefined:
713
+ printUsage();
714
+ break;
715
+ default:
716
+ console.error(`Unknown command: ${command}`);
717
+ printUsage();
718
+ process.exit(1);
719
+ }