agent-working-memory 0.15.6 → 0.15.7

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/src/cli.ts CHANGED
@@ -1,161 +1,161 @@
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, basename } from 'node:path';
17
- import { execSync } from 'node:child_process';
18
- import { randomUUID } from 'node:crypto';
19
- import { fileURLToPath } from 'node:url';
20
- import { VERSION } from './version.js';
21
- import { runOnboard, ONBOARD_SKILL } from './onboard/index.js';
22
-
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = dirname(__filename);
25
-
26
- // Load .env if present
27
- try {
28
- const envPath = resolve(process.cwd(), '.env');
29
- const envContent = readFileSync(envPath, 'utf-8');
30
- for (const line of envContent.split('\n')) {
31
- const trimmed = line.trim();
32
- if (!trimmed || trimmed.startsWith('#')) continue;
33
- const eqIdx = trimmed.indexOf('=');
34
- if (eqIdx === -1) continue;
35
- const key = trimmed.slice(0, eqIdx).trim();
36
- const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
37
- if (!process.env[key]) process.env[key] = val;
38
- }
39
- } catch { /* No .env file */ }
40
-
41
- const args = process.argv.slice(2);
42
- const command = args[0];
43
-
44
- function printUsage() {
45
- console.log(`
46
- AgentWorkingMemory — Cognitive memory for AI agents
47
-
48
- Usage:
49
- awm setup [target] [options] Configure AWM for an AI CLI
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, basename } from 'node:path';
17
+ import { execSync } from 'node:child_process';
18
+ import { randomUUID } from 'node:crypto';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { VERSION } from './version.js';
21
+ import { runOnboard, ONBOARD_SKILL } from './onboard/index.js';
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+
26
+ // Load .env if present
27
+ try {
28
+ const envPath = resolve(process.cwd(), '.env');
29
+ const envContent = readFileSync(envPath, 'utf-8');
30
+ for (const line of envContent.split('\n')) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith('#')) continue;
33
+ const eqIdx = trimmed.indexOf('=');
34
+ if (eqIdx === -1) continue;
35
+ const key = trimmed.slice(0, eqIdx).trim();
36
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
37
+ if (!process.env[key]) process.env[key] = val;
38
+ }
39
+ } catch { /* No .env file */ }
40
+
41
+ const args = process.argv.slice(2);
42
+ const command = args[0];
43
+
44
+ function printUsage() {
45
+ console.log(`
46
+ AgentWorkingMemory — Cognitive memory for AI agents
47
+
48
+ Usage:
49
+ awm setup [target] [options] Configure AWM for an AI CLI
50
50
  awm plugin Install as a Claude Code plugin (local, no GitHub)
51
- awm doctor [target|--all] Validate AWM integrations
52
- awm mcp Start MCP server (stdio)
53
- awm serve [--port <port>] Start HTTP API server
54
- awm health [--port <port>] Check server health
55
- awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
56
- Export memories to JSON
57
- awm import <file> --db <path> [--remap-agent <id>] [--dedupe] [--dry-run]
58
- Import memories from JSON
59
- awm merge --target <db> --source <db> [--source ...]
60
- [--remap uuid=name] [--remap-all-uuids <name>]
61
- [--dedupe] [--dry-run] Merge multiple memory DBs
62
- awm migrate --from <sqlite.db> --to <pglite-dir> [--dry-run] [--verbose]
63
- Migrate SQLite DB to PGlite
64
-
65
- Setup targets:
66
- claude-code (default) .mcp.json + CLAUDE.md + hooks
67
- codex ~/.codex/config.toml + AGENTS.md
68
- cursor .cursor/mcp.json + .cursorrules
69
- http Connection info for HTTP API
70
-
71
- Setup options:
72
- --global Use global scope (recommended for claude-code)
73
- --agent-id <id> Agent identifier (default: project name)
74
- --db-path <path> Database path (default: <awm>/data/memory.db)
75
- --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
76
- --no-claude-md Alias for --no-instructions
77
- --no-hooks Skip hook installation
78
- --no-prime Skip the UserPromptSubmit prime hook (auto-inject memories per prompt)
79
- --hook-port PORT Preferred sidecar port (default: 8401; each session walks upward when busy)
80
- --hook-port-range N Ports to try upward from --hook-port (default: 10)
81
- Re-running setup keeps your existing agent id, db path, port and any env you added.
82
-
83
- Examples:
84
- awm setup --global Claude Code, global (recommended)
85
- awm setup codex Codex CLI
86
- awm setup cursor Cursor IDE
87
- awm setup http Generic HTTP integration
88
- awm doctor --all Check all configured targets
89
- `.trim());
90
- }
91
-
92
- // ─── SETUP ──────────────────────────────────────
93
-
94
- async function setup() {
95
- // Parse flags
96
- let target = 'claude-code';
97
- let agentId: string | undefined;
98
- let dbPath: string | null = null;
99
- let skipInstructions = false;
100
- let isGlobal = false;
101
- let skipHooks = false;
102
- let installPrime = true;
103
- let hookPort: string | undefined;
104
- let hookPortRange: string | undefined;
105
-
106
- for (let i = 1; i < args.length; i++) {
107
- if (args[i] === '--agent-id' && args[i + 1]) {
108
- agentId = args[++i];
109
- } else if (args[i] === '--db-path' && args[i + 1]) {
110
- dbPath = args[++i];
111
- } else if (args[i] === '--no-claude-md' || args[i] === '--no-instructions') {
112
- skipInstructions = true;
113
- } else if (args[i] === '--no-hooks') {
114
- skipHooks = true;
115
- } else if (args[i] === '--no-prime') {
116
- installPrime = false;
117
- } else if (args[i] === '--hook-port' && args[i + 1]) {
118
- hookPort = args[++i];
119
- } else if (args[i] === '--hook-port-range' && args[i + 1]) {
120
- hookPortRange = args[++i];
121
- } else if (args[i] === '--global') {
122
- isGlobal = true;
123
- } else if (!args[i].startsWith('--')) {
124
- // Positional arg = target
125
- target = args[i];
126
- }
127
- }
128
-
129
- // Load adapter
130
- const { getAdapter } = await import('./adapters/index.js');
131
- const { buildSetupContext } = await import('./adapters/common.js');
132
-
133
- let adapter;
134
- try {
135
- adapter = await getAdapter(target);
136
- } catch (e: any) {
137
- console.error(e.message);
138
- process.exit(1);
139
- }
140
-
141
- // Force global for adapters that don't support project scope
142
- if (!adapter.supportsProjectScope && !isGlobal) {
143
- isGlobal = true;
144
- }
145
-
146
- // Build context. A previous install's values survive unless a flag overrides them
147
- // (0.14.6) — an upgrade must never repoint the database or rename the agent.
148
- const existingEnv = adapter.readExistingEnv?.(isGlobal, process.cwd()) ?? null;
51
+ awm doctor [target|--all] Validate AWM integrations
52
+ awm mcp Start MCP server (stdio)
53
+ awm serve [--port <port>] Start HTTP API server
54
+ awm health [--port <port>] Check server health
55
+ awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
56
+ Export memories to JSON
57
+ awm import <file> --db <path> [--remap-agent <id>] [--dedupe] [--dry-run]
58
+ Import memories from JSON
59
+ awm merge --target <db> --source <db> [--source ...]
60
+ [--remap uuid=name] [--remap-all-uuids <name>]
61
+ [--dedupe] [--dry-run] Merge multiple memory DBs
62
+ awm migrate --from <sqlite.db> --to <pglite-dir> [--dry-run] [--verbose]
63
+ Migrate SQLite DB to PGlite
64
+
65
+ Setup targets:
66
+ claude-code (default) .mcp.json + CLAUDE.md + hooks
67
+ codex ~/.codex/config.toml + AGENTS.md
68
+ cursor .cursor/mcp.json + .cursorrules
69
+ http Connection info for HTTP API
70
+
71
+ Setup options:
72
+ --global Use global scope (recommended for claude-code)
73
+ --agent-id <id> Agent identifier (default: project name)
74
+ --db-path <path> Database path (default: ~/.awm/memory.db)
75
+ --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
76
+ --no-claude-md Alias for --no-instructions
77
+ --no-hooks Skip hook installation
78
+ --no-prime Skip the UserPromptSubmit prime hook (auto-inject memories per prompt)
79
+ --hook-port PORT Preferred sidecar port (default: 8401; each session walks upward when busy)
80
+ --hook-port-range N Ports to try upward from --hook-port (default: 10)
81
+ Re-running setup keeps your existing agent id, db path, port and any env you added.
82
+
83
+ Examples:
84
+ awm setup --global Claude Code, global (recommended)
85
+ awm setup codex Codex CLI
86
+ awm setup cursor Cursor IDE
87
+ awm setup http Generic HTTP integration
88
+ awm doctor --all Check all configured targets
89
+ `.trim());
90
+ }
91
+
92
+ // ─── SETUP ──────────────────────────────────────
93
+
94
+ async function setup() {
95
+ // Parse flags
96
+ let target = 'claude-code';
97
+ let agentId: string | undefined;
98
+ let dbPath: string | null = null;
99
+ let skipInstructions = false;
100
+ let isGlobal = false;
101
+ let skipHooks = false;
102
+ let installPrime = true;
103
+ let hookPort: string | undefined;
104
+ let hookPortRange: string | undefined;
105
+
106
+ for (let i = 1; i < args.length; i++) {
107
+ if (args[i] === '--agent-id' && args[i + 1]) {
108
+ agentId = args[++i];
109
+ } else if (args[i] === '--db-path' && args[i + 1]) {
110
+ dbPath = args[++i];
111
+ } else if (args[i] === '--no-claude-md' || args[i] === '--no-instructions') {
112
+ skipInstructions = true;
113
+ } else if (args[i] === '--no-hooks') {
114
+ skipHooks = true;
115
+ } else if (args[i] === '--no-prime') {
116
+ installPrime = false;
117
+ } else if (args[i] === '--hook-port' && args[i + 1]) {
118
+ hookPort = args[++i];
119
+ } else if (args[i] === '--hook-port-range' && args[i + 1]) {
120
+ hookPortRange = args[++i];
121
+ } else if (args[i] === '--global') {
122
+ isGlobal = true;
123
+ } else if (!args[i].startsWith('--')) {
124
+ // Positional arg = target
125
+ target = args[i];
126
+ }
127
+ }
128
+
129
+ // Load adapter
130
+ const { getAdapter } = await import('./adapters/index.js');
131
+ const { buildSetupContext } = await import('./adapters/common.js');
132
+
133
+ let adapter;
134
+ try {
135
+ adapter = await getAdapter(target);
136
+ } catch (e: any) {
137
+ console.error(e.message);
138
+ process.exit(1);
139
+ }
140
+
141
+ // Force global for adapters that don't support project scope
142
+ if (!adapter.supportsProjectScope && !isGlobal) {
143
+ isGlobal = true;
144
+ }
145
+
146
+ // Build context. A previous install's values survive unless a flag overrides them
147
+ // (0.14.6) — an upgrade must never repoint the database or rename the agent.
148
+ const existingEnv = adapter.readExistingEnv?.(isGlobal, process.cwd()) ?? null;
149
149
  // `awm setup claude-code` -> client=claude-code, cursor -> client=cursor, and so on.
150
- const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort, hookPortRange, installPrime, existingEnv, client: target });
151
-
152
- // Run adapter
153
- const configAction = adapter.writeMcpConfig(ctx);
154
- const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
155
- const hooksAction = adapter.writeHooks(ctx, skipHooks);
156
-
157
- // Seed the onboarding skill so a cold store can teach the agent how to warm itself.
158
- const skillAction = await seedOnboardSkill(ctx.dbPath, ctx.agentId);
150
+ const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort, hookPortRange, installPrime, existingEnv, client: target });
151
+
152
+ // Run adapter
153
+ const configAction = adapter.writeMcpConfig(ctx);
154
+ const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
155
+ const hooksAction = adapter.writeHooks(ctx, skipHooks);
156
+
157
+ // Seed the onboarding skill so a cold store can teach the agent how to warm itself.
158
+ const skillAction = await seedOnboardSkill(ctx.dbPath, ctx.agentId);
159
159
 
160
160
  // A rescue must be loud. The user did not ask for their store to move, and the reason it
161
161
  // moved is that the old location was about to be deleted by their next npm upgrade.
@@ -173,24 +173,24 @@ async function setup() {
173
173
  The file was COPIED, not moved: the original is still there if you want to check it.
174
174
  Once you are satisfied, the old copy can be deleted.`);
175
175
  }
176
-
177
- console.log(`
178
- AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
179
-
180
- Agent ID: ${ctx.agentId}
181
- DB path: ${ctx.dbPath}
182
- ${configAction}
183
- ${instructionsAction}
184
- ${hooksAction}
185
- ${skillAction}
186
-
187
- Next steps:
188
- 1. Restart ${adapter.name} to pick up the MCP server
189
- 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
190
- 3. \`awm doctor\` checks the install and lists the live sidecars
191
- `.trim());
192
- }
193
-
176
+
177
+ console.log(`
178
+ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
179
+
180
+ Agent ID: ${ctx.agentId}
181
+ DB path: ${ctx.dbPath}
182
+ ${configAction}
183
+ ${instructionsAction}
184
+ ${hooksAction}
185
+ ${skillAction}
186
+
187
+ Next steps:
188
+ 1. Restart ${adapter.name} to pick up the MCP server
189
+ 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
190
+ 3. \`awm doctor\` checks the install and lists the live sidecars
191
+ `.trim());
192
+ }
193
+
194
194
  // ─── PLUGIN ──────────────────────────────────────
195
195
 
196
196
  /**
@@ -243,674 +243,674 @@ AWM ${VERSION} — install as a Claude Code plugin
243
243
  }
244
244
 
245
245
 
246
- // ─── DOCTOR ──────────────────────────────────────
247
-
248
- async function doctor() {
249
- const { getAdapter, listAdapters } = await import('./adapters/index.js');
250
- const { buildSetupContext } = await import('./adapters/common.js');
251
-
252
- let targets: string[] = [];
253
- let checkAll = false;
254
-
255
- for (let i = 1; i < args.length; i++) {
256
- if (args[i] === '--all') {
257
- checkAll = true;
258
- } else if (!args[i].startsWith('--')) {
259
- targets.push(args[i]);
260
- }
261
- }
262
-
263
- if (checkAll) {
264
- targets = listAdapters();
265
- } else if (targets.length === 0) {
266
- targets = listAdapters();
267
- }
268
-
269
- const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
270
-
271
- console.log('AWM Doctor\n');
272
-
273
- for (const targetId of targets) {
274
- let adapter;
275
- try {
276
- adapter = await getAdapter(targetId);
277
- } catch {
278
- console.log(` ? ${targetId}: unknown target (skipped)`);
279
- continue;
280
- }
281
-
282
- console.log(` ${adapter.name}:`);
283
- const existingEnv = adapter.readExistingEnv?.(true, process.cwd()) ?? null;
284
- const ctx = buildSetupContext({ isGlobal: true, existingEnv });
285
- const results = await adapter.diagnose(ctx);
286
- for (const r of results) {
287
- const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
288
- console.log(` [${icon}] ${r.check}: ${r.message}`);
289
- if (r.fix) {
290
- console.log(` Fix: ${r.fix}`);
291
- }
292
- }
293
- console.log();
294
- }
295
- }
296
-
297
- // ─── MCP ──────────────────────────────────────
298
-
299
- async function mcp() {
300
- // Dynamic import to avoid loading heavy deps for setup/health commands
301
- await import('./mcp.js');
302
- }
303
-
304
- // ─── SERVE ──────────────────────────────────────
305
-
306
- async function serve() {
307
- // Parse --port flag
308
- for (let i = 1; i < args.length; i++) {
309
- if (args[i] === '--port' && args[i + 1]) {
310
- process.env.AWM_PORT = args[++i];
311
- }
312
- }
313
- await import('./index.js');
314
- }
315
-
316
- // ─── HEALTH ──────────────────────────────────────
317
-
318
- function health() {
319
- let port = '8400';
320
- for (let i = 1; i < args.length; i++) {
321
- if (args[i] === '--port' && args[i + 1]) {
322
- port = args[++i];
323
- }
324
- }
325
-
326
- try {
327
- const result = execSync(`curl -sf http://localhost:${port}/health`, {
328
- encoding: 'utf8',
329
- timeout: 5000,
330
- });
331
- const data = JSON.parse(result);
332
- console.log(`OK — v${data.version} (${data.timestamp})`);
333
- } catch {
334
- console.error(`Cannot reach AWM server on port ${port}`);
335
- process.exit(1);
336
- }
337
- }
338
-
339
- // ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
340
- //
341
- // export/import route through openStore() so they work on ANY backend (SQLite,
342
- // PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
343
- // (a SQLite file or PGlite dir, by shape); for a Postgres target set
344
- // AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
345
- // port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
346
-
347
- function toISOStr(d: Date | string | null | undefined): string | null {
348
- if (d == null) return null;
349
- return d instanceof Date ? d.toISOString() : String(d);
350
- }
351
-
352
- async function openCliStore(dbPath?: string): Promise<{ store: any; backend: string; close: () => Promise<void> }> {
353
- // --db sets the path only when the env doesn't already select a backend/path.
354
- if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
355
- process.env.AWM_DB_PATH = dbPath;
356
- }
357
- const { openStore } = await import('./storage/factory.js');
358
- const { store, backend } = await openStore();
359
- return { store, backend, close: async () => { try { await store.close?.(); } catch { /* */ } } };
360
- }
361
-
362
- // ─── EXPORT ──────────────────────────────────────
363
-
364
- async function exportMemories() {
365
- let dbPath = '';
366
- let agentFilter: string | null = null;
367
- let outputPath: string | null = null;
368
- let activeOnly = false;
369
- let allStages = false;
370
- let includeRetracted = false;
371
-
372
- for (let i = 1; i < args.length; i++) {
373
- if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
374
- else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
375
- else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
376
- else if (args[i] === '--active-only') activeOnly = true;
377
- else if (args[i] === '--all-stages') allStages = true;
378
- else if (args[i] === '--include-retracted') includeRetracted = true;
379
- }
380
-
381
- // --db must exist for a file/dir backend; a Postgres source is selected by env instead.
382
- const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
383
- if (!usingPostgres) {
384
- if (!dbPath) { console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)'); process.exit(1); }
385
- if (!existsSync(dbPath)) { console.error(`Error: database not found: ${dbPath}`); process.exit(1); }
386
- }
387
-
388
- const { store, backend, close } = await openCliStore(dbPath);
389
- try {
390
- const agentIds: string[] = agentFilter
391
- ? [agentFilter]
392
- : ((await store.getActiveAgents()) as any[]).map((a) => a.agentId);
393
- if (agentIds.length === 0) {
394
- console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
395
- }
396
- // Default to the meaningful memory set (active stage, non-retracted). --all-stages
397
- // widens to every stage; --include-retracted adds retracted (off with --active-only).
398
- const stage = allStages ? undefined : 'active';
399
- const wantRetracted = includeRetracted && !activeOnly;
400
- const engrams: any[] = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
401
-
402
- const memories = engrams.map((e: any) => ({
403
- id: e.id,
404
- agent_id: e.agentId,
405
- concept: e.concept,
406
- content: e.content,
407
- // Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
408
- // re-embed after import) → a faithful, recall-ready port when source/target embed
409
- // models match. import skips them with --no-embeddings (then re-embed).
410
- embedding: Array.isArray(e.embedding) ? e.embedding : null,
411
- confidence: e.confidence,
412
- salience: e.salience,
413
- access_count: e.accessCount ?? 0,
414
- last_accessed: toISOStr(e.lastAccessed),
415
- created_at: toISOStr(e.createdAt),
416
- stage: e.stage ?? 'active',
417
- tags: Array.isArray(e.tags) ? e.tags : [],
418
- memory_class: e.memoryClass ?? 'working',
419
- memory_type: e.memoryType ?? 'unclassified',
420
- episode_id: e.episodeId ?? null,
421
- task_status: e.taskStatus ?? null,
422
- task_priority: e.taskPriority ?? null,
423
- supersedes: e.supersedes ?? null,
424
- superseded_by: e.supersededBy ?? null,
425
- retracted: e.retracted ? 1 : 0,
426
- }));
427
-
428
- const memIds = new Set(memories.map((m) => m.id));
429
- const seen = new Set<string>();
430
- const associations: any[] = [];
431
- for (const aid of agentIds) {
432
- for (const a of ((await store.getAllAssociations(aid)) as any[]) ?? []) {
433
- if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId)) continue;
434
- const k = `${a.fromEngramId}>${a.toEngramId}`;
435
- if (seen.has(k)) continue;
436
- seen.add(k);
437
- associations.push({
438
- from_id: a.fromEngramId, to_id: a.toEngramId,
439
- weight: a.weight, type: a.type ?? 'hebbian',
440
- activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
441
- });
442
- }
443
- }
444
-
445
- const exportData = {
446
- version: VERSION,
447
- exported_at: new Date().toISOString(),
448
- source_backend: backend,
449
- agent_filter: agentFilter,
450
- embedding_model: process.env.AWM_EMBED_MODEL ?? null,
451
- memories,
452
- associations,
453
- stats: {
454
- total_memories: memories.length,
455
- total_associations: associations.length,
456
- agents: [...new Set(memories.map((m) => m.agent_id))],
457
- },
458
- };
459
-
460
- const json = JSON.stringify(exportData, null, 2);
461
- if (outputPath) {
462
- writeFileSync(outputPath, json + '\n');
463
- console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
464
- } else {
465
- process.stdout.write(json + '\n');
466
- }
467
- } finally {
468
- await close();
469
- }
470
- }
471
-
472
- // ─── IMPORT ──────────────────────────────────────
473
-
474
- async function importMemories() {
475
- let filePath = '';
476
- let dbPath = '';
477
- let remapAgent: string | null = null;
478
- let dedupe = false;
479
- let dryRun = false;
480
- let includeRetracted = false;
481
-
482
- // First non-flag arg after 'import' is the file path
483
- for (let i = 1; i < args.length; i++) {
484
- if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
485
- else if (args[i] === '--remap-agent' && args[i + 1]) remapAgent = args[++i];
486
- else if (args[i] === '--dedupe') dedupe = true;
487
- else if (args[i] === '--dry-run') dryRun = true;
488
- else if (args[i] === '--include-retracted') includeRetracted = true;
489
- else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
490
- }
491
-
492
- // --no-embeddings: skip importing embedding vectors (use when source/target embed models
493
- // differ → import without, then re-embed). Parsed alongside the existing flags above.
494
- const noEmbeddings = args.includes('--no-embeddings');
495
-
496
- if (!filePath) {
497
- console.error('Error: <file> is required');
498
- process.exit(1);
499
- }
500
- const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
501
- if (!dbPath && !usingPostgres) {
502
- console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
503
- process.exit(1);
504
- }
505
- if (!existsSync(filePath)) {
506
- console.error(`Error: import file not found: ${filePath}`);
507
- process.exit(1);
508
- }
509
-
510
- const importData = JSON.parse(readFileSync(filePath, 'utf-8'));
511
- if (!importData.memories || !Array.isArray(importData.memories)) {
512
- console.error('Error: invalid export file — missing memories array');
513
- process.exit(1);
514
- }
515
-
516
- const { store, backend, close } = await openCliStore(dbPath);
517
- try {
518
- // Dedupe against existing memories for the TARGET agent(s).
519
- const existingHashes = new Set<string>();
520
- if (dedupe) {
521
- const targetAgents = remapAgent
522
- ? [remapAgent]
523
- : [...new Set(importData.memories.map((m: any) => m.agent_id))] as string[];
524
- for (const aid of targetAgents) {
525
- for (const e of ((await store.getEngramsByAgent(aid, undefined, true)) as any[]) ?? []) {
526
- existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
527
- }
528
- }
529
- }
530
-
531
- const idMap = new Map<string, string>(); // old export id → new store id
532
- let imported = 0, skippedDupes = 0, skippedRetracted = 0;
533
-
534
- // Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
535
- // createdAt/accessCount normalize to import time (the contract's createEngram stamps
536
- // them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
537
- // embedding) is preserved, so recall is faithful.
538
- for (const mem of importData.memories) {
539
- if (mem.retracted && !includeRetracted) { skippedRetracted++; continue; }
540
- if (dedupe) {
541
- const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
542
- if (existingHashes.has(h)) { skippedDupes++; continue; }
543
- existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
544
- }
545
- // dry-run: still map the id so the association-count preview isn't always 0
546
- if (dryRun) { idMap.set(mem.id, mem.id); imported++; continue; }
547
- const created = await store.createEngram({
548
- agentId: remapAgent ?? mem.agent_id,
549
- concept: mem.concept,
550
- content: mem.content,
551
- tags: Array.isArray(mem.tags) ? mem.tags : [],
552
- embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
553
- confidence: mem.confidence ?? 0.5,
554
- salience: mem.salience ?? 0.5,
555
- memoryClass: mem.memory_class ?? 'working',
556
- memoryType: mem.memory_type ?? undefined,
557
- episodeId: mem.episode_id ?? undefined,
558
- taskStatus: mem.task_status ?? undefined,
559
- taskPriority: mem.task_priority ?? undefined,
560
- });
561
- idMap.set(mem.id, created.id);
562
- // Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
563
- // without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
564
- // non-active stage (staging/consolidated/archived/fading) silently flattens to active.
565
- if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
566
- try { await store.updateStage(created.id, mem.stage); } catch { /* best-effort */ }
567
- }
568
- if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
569
- try { await store.retractEngram(created.id, (mem as { retracted_by?: string }).retracted_by ?? null); } catch { /* best-effort */ }
570
- }
571
- imported++;
572
- }
573
-
574
- // Pass 2 — re-link supersession with remapped ids (supersedeEngram sets both sides:
575
- // old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
576
- if (!dryRun) {
577
- for (const mem of importData.memories) {
578
- const newId = idMap.get(mem.id);
579
- if (!newId || !mem.supersedes) continue;
580
- const supersededNew = idMap.get(mem.supersedes);
581
- if (supersededNew) { try { await store.supersedeEngram(supersededNew, newId); } catch { /* best-effort */ } }
582
- }
583
- }
584
-
585
- // Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
586
- let assocImported = 0;
587
- for (const a of (importData.associations ?? [])) {
588
- const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
589
- if (!fromId || !toId) continue;
590
- if (!dryRun) { try { await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5); } catch { /* best-effort */ } }
591
- assocImported++;
592
- }
593
-
594
- const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
595
- console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
596
- (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
597
- (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
598
- (remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
599
- ` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
600
- } finally {
601
- await close();
602
- }
603
- }
604
-
605
- // ─── MERGE ──────────────────────────────────────
606
-
607
- async function mergeMemories() {
608
- const Database = (await import('better-sqlite3')).default;
609
- const { EngramStore } = await import('./storage/sqlite.js');
610
- const { createHash, randomUUID } = await import('node:crypto');
611
-
612
- let target = '';
613
- const sources: string[] = [];
614
- const remapEntries = new Map<string, string>();
615
- let remapAllUuids = '';
616
- let dedupe = false;
617
- let dryRun = false;
618
-
619
- for (let i = 1; i < args.length; i++) {
620
- if (args[i] === '--target' && args[i + 1]) {
621
- target = args[++i];
622
- } else if (args[i] === '--source' && args[i + 1]) {
623
- sources.push(args[++i]);
624
- } else if (args[i] === '--remap' && args[i + 1]) {
625
- const val = args[++i];
626
- const eqIdx = val.indexOf('=');
627
- if (eqIdx > 0) remapEntries.set(val.slice(0, eqIdx), val.slice(eqIdx + 1));
628
- } else if (args[i] === '--remap-all-uuids' && args[i + 1]) {
629
- remapAllUuids = args[++i];
630
- } else if (args[i] === '--dedupe') {
631
- dedupe = true;
632
- } else if (args[i] === '--dry-run') {
633
- dryRun = true;
634
- }
635
- }
636
-
637
- if (!target || sources.length === 0) {
638
- console.error('Usage: awm merge --target <path> --source <path> [--source <path>...] [--remap uuid=name] [--remap-all-uuids name] [--dedupe] [--dry-run]');
639
- process.exit(1);
640
- }
641
-
642
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
643
-
644
- function remapAgentId(agentId: string): string {
645
- if (remapEntries.has(agentId)) return remapEntries.get(agentId)!;
646
- if (remapAllUuids && UUID_RE.test(agentId)) return remapAllUuids;
647
- return agentId;
648
- }
649
-
650
- function contentHash(concept: string, content: string): string {
651
- return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
652
- }
653
-
654
- console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
655
-
656
- // Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
657
- // triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
658
- // task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
659
- const store: any = new EngramStore(target);
660
- const targetDb = store.db as import('better-sqlite3').Database; // the store's better-sqlite3 handle
661
-
662
- const blobToArr = (b: unknown): number[] | undefined => {
663
- const buf = b as Buffer | null | undefined;
664
- return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
665
- };
666
- const parseJson = <T>(s: unknown, fallback: T): T => { try { return s ? JSON.parse(String(s)) as T : fallback; } catch { return fallback; } };
667
-
668
- // Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
669
- const existingHashes = new Set<string>();
670
- if (dedupe) {
671
- const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
672
- for (const row of rows) existingHashes.add(contentHash(row.concept, row.content));
673
- console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
674
- }
675
-
676
- const insertAssoc = targetDb.prepare(`
677
- INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
678
- activation_count, created_at, last_activated)
679
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
680
- `);
681
-
682
- let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
683
-
684
- try {
685
- for (const sourcePath of sources) {
686
- if (!existsSync(sourcePath)) {
687
- console.error(` Source not found: ${sourcePath}`);
688
- continue;
689
- }
690
-
691
- const sourceDb = new Database(sourcePath, { readonly: true });
692
- // SELECT * is robust to older source schemas — a column a source predates just reads back undefined
693
- // and createEngram fills the default.
694
- const engrams = sourceDb.prepare('SELECT * FROM engrams').all() as any[];
695
- const assocs = sourceDb.prepare('SELECT * FROM associations').all() as any[];
696
- sourceDb.close(); // reads done — release the source handle before any (throwing) write work
697
-
698
- const idMap = new Map<string, string>();
699
- const skippedIds = new Set<string>();
700
- let imported = 0, skipped = 0, assocImported = 0;
701
-
702
- for (const e of engrams) {
703
- const hash = contentHash(e.concept, e.content);
704
- if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
705
- existingHashes.add(hash);
706
- if (dryRun) { idMap.set(e.id, e.id); imported++; continue; }
707
- // Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
708
- // memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
709
- const created = store.createEngram({
710
- agentId: remapAgentId(e.agent_id),
711
- concept: e.concept, content: e.content,
712
- embedding: blobToArr(e.embedding),
713
- confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
714
- salienceFeatures: parseJson(e.salience_features, undefined),
715
- reasonCodes: parseJson(e.reason_codes, undefined),
716
- tags: parseJson<string[]>(e.tags, []),
717
- memoryClass: e.memory_class ?? 'working',
718
- memoryType: e.memory_type ?? undefined,
719
- episodeId: e.episode_id ?? undefined,
720
- taskStatus: e.task_status ?? undefined,
721
- taskPriority: e.task_priority ?? undefined,
722
- blockedBy: e.blocked_by ?? undefined,
723
- ttl: e.ttl ?? undefined,
724
- sequence: e.sequence ?? undefined,
725
- references: parseJson(e.references_json, undefined),
726
- });
727
- idMap.set(e.id, created.id);
728
- // preserve stage + retracted (createEngram always mints active/non-retracted)
729
- if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') { try { store.updateStage(created.id, e.stage); } catch { /* */ } }
730
- if (e.retracted) { try { store.retractEngram(created.id, e.retracted_by ?? null); } catch { /* */ } }
731
- imported++;
732
- }
733
- // second pass — re-link supersession with remapped ids
734
- if (!dryRun) {
735
- for (const e of engrams) {
736
- const newId = idMap.get(e.id);
737
- if (!newId || !e.supersedes) continue;
738
- const supNew = idMap.get(e.supersedes);
739
- if (supNew) { try { store.supersedeEngram(supNew, newId); } catch { /* */ } }
740
- }
741
- }
742
- for (const a of assocs) {
743
- if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
744
- const fromId = idMap.get(a.from_engram_id);
745
- const toId = idMap.get(a.to_engram_id);
746
- if (!fromId || !toId) continue;
747
- if (!dryRun) {
748
- try {
749
- insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
750
- a.activation_count, a.created_at, a.last_activated);
751
- } catch { /* skip an association whose source row has an unbindable/undefined column */ }
752
- }
753
- assocImported++;
754
- }
755
-
756
- const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
757
- console.log(` Source: ${sourcePath}`);
758
- console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
759
- console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
760
- console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
761
-
762
- totalMemories += imported;
763
- totalAssociations += assocImported;
764
- totalSkipped += skipped;
765
- }
766
-
767
- } finally {
768
- try { store.close(); } catch { /* */ }
769
- }
770
- console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
771
- if (dryRun) console.log('(dry run — no data written)');
772
- }
773
-
774
- async function migrateCmd() {
775
- let from = '';
776
- let to = '';
777
- let dryRun = false;
778
- let verbose = false;
779
-
780
- for (let i = 1; i < args.length; i++) {
781
- const a = args[i];
782
- if (a === '--from') { from = args[++i]; }
783
- else if (a === '--to') { to = args[++i]; }
784
- else if (a === '--dry-run') { dryRun = true; }
785
- else if (a === '--verbose' || a === '-v') { verbose = true; }
786
- else { console.error(`Unknown argument: ${a}`); process.exit(1); }
787
- }
788
-
789
- if (!from || !to) {
790
- console.error('Usage: awm migrate --from <sqlite.db> --to <pglite-dir> [--dry-run] [--verbose]');
791
- process.exit(1);
792
- }
793
-
794
- const { migrate, printStats } = await import('./cli/migrate.js');
795
- try {
796
- console.log(`Migrating ${from} → ${to}${dryRun ? ' (dry run)' : ''}`);
797
- const stats = await migrate({ from, to, dryRun, verbose });
798
- printStats(stats, dryRun);
799
- } catch (err) {
800
- console.error(`Migration failed: ${err instanceof Error ? err.message : String(err)}`);
801
- process.exit(1);
802
- }
803
- }
804
-
805
- // ─── ONBOARD ──────────────────────────────────────
806
-
807
- /**
808
- * Seed the onboarding skill as a canonical memory (idempotent). This is what lets
809
- * a cold store teach the host agent how to warm-start itself — the agent recalls
810
- * the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
811
- */
812
- async function seedOnboardSkill(dbPath: string, agentId: string): Promise<string> {
813
- try {
814
- const { store, close } = await openCliStore(dbPath);
815
- try {
816
- const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
817
- if (existing) return 'Onboarding skill: already present';
818
- await store.createEngram({
819
- agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
820
- tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
821
- });
822
- return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
823
- } finally {
824
- await close();
825
- }
826
- } catch (e: any) {
827
- return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
828
- }
829
- }
830
-
831
- function onboardCmd() {
832
- const docs: string[] = [];
833
- let repo: string | undefined;
834
- let project = '';
835
- let agentId = '';
836
- let purpose: string | undefined;
837
- let outDir = resolve(process.cwd(), '.awm');
838
-
839
- for (let i = 1; i < args.length; i++) {
840
- const a = args[i];
841
- if (a === '--repo' && args[i + 1]) repo = args[++i];
842
- else if (a === '--project' && args[i + 1]) project = args[++i];
843
- else if (a === '--agent' && args[i + 1]) agentId = args[++i];
844
- else if (a === '--purpose' && args[i + 1]) purpose = args[++i];
845
- else if (a === '--out' && args[i + 1]) outDir = resolve(args[++i]);
846
- else if (!a.startsWith('--')) docs.push(a);
847
- }
848
-
849
- // Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
850
- if (docs.length === 0) docs.push(repo ?? process.cwd());
851
- if (!project) project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
852
- if (!agentId) agentId = project;
853
-
854
- const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
855
- console.log(`
856
- AWM onboard — warm-start pack for "${project}"
857
-
858
- Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
859
- Extracted: ${count} candidate memories (agent: ${agentId})
860
-
861
- Review: ${reviewPath}
862
- Pack: ${packPath}
863
-
864
- Next:
865
- 1. Edit the review file / pack as needed (delete noise, answer the interview questions).
866
- 2. Load it: awm import ${packPath} --db <path> --dedupe
867
- (embeddings backfill on the first consolidation — recall is warm immediately after)
868
- `.trimEnd());
869
- }
870
-
871
- // ─── Dispatch ──────────────────────────────────────
872
-
873
- switch (command) {
874
- case 'setup':
875
- await setup();
876
- break;
246
+ // ─── DOCTOR ──────────────────────────────────────
247
+
248
+ async function doctor() {
249
+ const { getAdapter, listAdapters } = await import('./adapters/index.js');
250
+ const { buildSetupContext } = await import('./adapters/common.js');
251
+
252
+ let targets: string[] = [];
253
+ let checkAll = false;
254
+
255
+ for (let i = 1; i < args.length; i++) {
256
+ if (args[i] === '--all') {
257
+ checkAll = true;
258
+ } else if (!args[i].startsWith('--')) {
259
+ targets.push(args[i]);
260
+ }
261
+ }
262
+
263
+ if (checkAll) {
264
+ targets = listAdapters();
265
+ } else if (targets.length === 0) {
266
+ targets = listAdapters();
267
+ }
268
+
269
+ const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
270
+
271
+ console.log('AWM Doctor\n');
272
+
273
+ for (const targetId of targets) {
274
+ let adapter;
275
+ try {
276
+ adapter = await getAdapter(targetId);
277
+ } catch {
278
+ console.log(` ? ${targetId}: unknown target (skipped)`);
279
+ continue;
280
+ }
281
+
282
+ console.log(` ${adapter.name}:`);
283
+ const existingEnv = adapter.readExistingEnv?.(true, process.cwd()) ?? null;
284
+ const ctx = buildSetupContext({ isGlobal: true, existingEnv });
285
+ const results = await adapter.diagnose(ctx);
286
+ for (const r of results) {
287
+ const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
288
+ console.log(` [${icon}] ${r.check}: ${r.message}`);
289
+ if (r.fix) {
290
+ console.log(` Fix: ${r.fix}`);
291
+ }
292
+ }
293
+ console.log();
294
+ }
295
+ }
296
+
297
+ // ─── MCP ──────────────────────────────────────
298
+
299
+ async function mcp() {
300
+ // Dynamic import to avoid loading heavy deps for setup/health commands
301
+ await import('./mcp.js');
302
+ }
303
+
304
+ // ─── SERVE ──────────────────────────────────────
305
+
306
+ async function serve() {
307
+ // Parse --port flag
308
+ for (let i = 1; i < args.length; i++) {
309
+ if (args[i] === '--port' && args[i + 1]) {
310
+ process.env.AWM_PORT = args[++i];
311
+ }
312
+ }
313
+ await import('./index.js');
314
+ }
315
+
316
+ // ─── HEALTH ──────────────────────────────────────
317
+
318
+ function health() {
319
+ let port = '8400';
320
+ for (let i = 1; i < args.length; i++) {
321
+ if (args[i] === '--port' && args[i + 1]) {
322
+ port = args[++i];
323
+ }
324
+ }
325
+
326
+ try {
327
+ const result = execSync(`curl -sf http://localhost:${port}/health`, {
328
+ encoding: 'utf8',
329
+ timeout: 5000,
330
+ });
331
+ const data = JSON.parse(result);
332
+ console.log(`OK — v${data.version} (${data.timestamp})`);
333
+ } catch {
334
+ console.error(`Cannot reach AWM server on port ${port}`);
335
+ process.exit(1);
336
+ }
337
+ }
338
+
339
+ // ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
340
+ //
341
+ // export/import route through openStore() so they work on ANY backend (SQLite,
342
+ // PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
343
+ // (a SQLite file or PGlite dir, by shape); for a Postgres target set
344
+ // AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
345
+ // port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
346
+
347
+ function toISOStr(d: Date | string | null | undefined): string | null {
348
+ if (d == null) return null;
349
+ return d instanceof Date ? d.toISOString() : String(d);
350
+ }
351
+
352
+ async function openCliStore(dbPath?: string): Promise<{ store: any; backend: string; close: () => Promise<void> }> {
353
+ // --db sets the path only when the env doesn't already select a backend/path.
354
+ if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
355
+ process.env.AWM_DB_PATH = dbPath;
356
+ }
357
+ const { openStore } = await import('./storage/factory.js');
358
+ const { store, backend } = await openStore();
359
+ return { store, backend, close: async () => { try { await store.close?.(); } catch { /* */ } } };
360
+ }
361
+
362
+ // ─── EXPORT ──────────────────────────────────────
363
+
364
+ async function exportMemories() {
365
+ let dbPath = '';
366
+ let agentFilter: string | null = null;
367
+ let outputPath: string | null = null;
368
+ let activeOnly = false;
369
+ let allStages = false;
370
+ let includeRetracted = false;
371
+
372
+ for (let i = 1; i < args.length; i++) {
373
+ if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
374
+ else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
375
+ else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
376
+ else if (args[i] === '--active-only') activeOnly = true;
377
+ else if (args[i] === '--all-stages') allStages = true;
378
+ else if (args[i] === '--include-retracted') includeRetracted = true;
379
+ }
380
+
381
+ // --db must exist for a file/dir backend; a Postgres source is selected by env instead.
382
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
383
+ if (!usingPostgres) {
384
+ if (!dbPath) { console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)'); process.exit(1); }
385
+ if (!existsSync(dbPath)) { console.error(`Error: database not found: ${dbPath}`); process.exit(1); }
386
+ }
387
+
388
+ const { store, backend, close } = await openCliStore(dbPath);
389
+ try {
390
+ const agentIds: string[] = agentFilter
391
+ ? [agentFilter]
392
+ : ((await store.getActiveAgents()) as any[]).map((a) => a.agentId);
393
+ if (agentIds.length === 0) {
394
+ console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
395
+ }
396
+ // Default to the meaningful memory set (active stage, non-retracted). --all-stages
397
+ // widens to every stage; --include-retracted adds retracted (off with --active-only).
398
+ const stage = allStages ? undefined : 'active';
399
+ const wantRetracted = includeRetracted && !activeOnly;
400
+ const engrams: any[] = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
401
+
402
+ const memories = engrams.map((e: any) => ({
403
+ id: e.id,
404
+ agent_id: e.agentId,
405
+ concept: e.concept,
406
+ content: e.content,
407
+ // Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
408
+ // re-embed after import) → a faithful, recall-ready port when source/target embed
409
+ // models match. import skips them with --no-embeddings (then re-embed).
410
+ embedding: Array.isArray(e.embedding) ? e.embedding : null,
411
+ confidence: e.confidence,
412
+ salience: e.salience,
413
+ access_count: e.accessCount ?? 0,
414
+ last_accessed: toISOStr(e.lastAccessed),
415
+ created_at: toISOStr(e.createdAt),
416
+ stage: e.stage ?? 'active',
417
+ tags: Array.isArray(e.tags) ? e.tags : [],
418
+ memory_class: e.memoryClass ?? 'working',
419
+ memory_type: e.memoryType ?? 'unclassified',
420
+ episode_id: e.episodeId ?? null,
421
+ task_status: e.taskStatus ?? null,
422
+ task_priority: e.taskPriority ?? null,
423
+ supersedes: e.supersedes ?? null,
424
+ superseded_by: e.supersededBy ?? null,
425
+ retracted: e.retracted ? 1 : 0,
426
+ }));
427
+
428
+ const memIds = new Set(memories.map((m) => m.id));
429
+ const seen = new Set<string>();
430
+ const associations: any[] = [];
431
+ for (const aid of agentIds) {
432
+ for (const a of ((await store.getAllAssociations(aid)) as any[]) ?? []) {
433
+ if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId)) continue;
434
+ const k = `${a.fromEngramId}>${a.toEngramId}`;
435
+ if (seen.has(k)) continue;
436
+ seen.add(k);
437
+ associations.push({
438
+ from_id: a.fromEngramId, to_id: a.toEngramId,
439
+ weight: a.weight, type: a.type ?? 'hebbian',
440
+ activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
441
+ });
442
+ }
443
+ }
444
+
445
+ const exportData = {
446
+ version: VERSION,
447
+ exported_at: new Date().toISOString(),
448
+ source_backend: backend,
449
+ agent_filter: agentFilter,
450
+ embedding_model: process.env.AWM_EMBED_MODEL ?? null,
451
+ memories,
452
+ associations,
453
+ stats: {
454
+ total_memories: memories.length,
455
+ total_associations: associations.length,
456
+ agents: [...new Set(memories.map((m) => m.agent_id))],
457
+ },
458
+ };
459
+
460
+ const json = JSON.stringify(exportData, null, 2);
461
+ if (outputPath) {
462
+ writeFileSync(outputPath, json + '\n');
463
+ console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
464
+ } else {
465
+ process.stdout.write(json + '\n');
466
+ }
467
+ } finally {
468
+ await close();
469
+ }
470
+ }
471
+
472
+ // ─── IMPORT ──────────────────────────────────────
473
+
474
+ async function importMemories() {
475
+ let filePath = '';
476
+ let dbPath = '';
477
+ let remapAgent: string | null = null;
478
+ let dedupe = false;
479
+ let dryRun = false;
480
+ let includeRetracted = false;
481
+
482
+ // First non-flag arg after 'import' is the file path
483
+ for (let i = 1; i < args.length; i++) {
484
+ if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
485
+ else if (args[i] === '--remap-agent' && args[i + 1]) remapAgent = args[++i];
486
+ else if (args[i] === '--dedupe') dedupe = true;
487
+ else if (args[i] === '--dry-run') dryRun = true;
488
+ else if (args[i] === '--include-retracted') includeRetracted = true;
489
+ else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
490
+ }
491
+
492
+ // --no-embeddings: skip importing embedding vectors (use when source/target embed models
493
+ // differ → import without, then re-embed). Parsed alongside the existing flags above.
494
+ const noEmbeddings = args.includes('--no-embeddings');
495
+
496
+ if (!filePath) {
497
+ console.error('Error: <file> is required');
498
+ process.exit(1);
499
+ }
500
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
501
+ if (!dbPath && !usingPostgres) {
502
+ console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
503
+ process.exit(1);
504
+ }
505
+ if (!existsSync(filePath)) {
506
+ console.error(`Error: import file not found: ${filePath}`);
507
+ process.exit(1);
508
+ }
509
+
510
+ const importData = JSON.parse(readFileSync(filePath, 'utf-8'));
511
+ if (!importData.memories || !Array.isArray(importData.memories)) {
512
+ console.error('Error: invalid export file — missing memories array');
513
+ process.exit(1);
514
+ }
515
+
516
+ const { store, backend, close } = await openCliStore(dbPath);
517
+ try {
518
+ // Dedupe against existing memories for the TARGET agent(s).
519
+ const existingHashes = new Set<string>();
520
+ if (dedupe) {
521
+ const targetAgents = remapAgent
522
+ ? [remapAgent]
523
+ : [...new Set(importData.memories.map((m: any) => m.agent_id))] as string[];
524
+ for (const aid of targetAgents) {
525
+ for (const e of ((await store.getEngramsByAgent(aid, undefined, true)) as any[]) ?? []) {
526
+ existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
527
+ }
528
+ }
529
+ }
530
+
531
+ const idMap = new Map<string, string>(); // old export id → new store id
532
+ let imported = 0, skippedDupes = 0, skippedRetracted = 0;
533
+
534
+ // Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
535
+ // createdAt/accessCount normalize to import time (the contract's createEngram stamps
536
+ // them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
537
+ // embedding) is preserved, so recall is faithful.
538
+ for (const mem of importData.memories) {
539
+ if (mem.retracted && !includeRetracted) { skippedRetracted++; continue; }
540
+ if (dedupe) {
541
+ const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
542
+ if (existingHashes.has(h)) { skippedDupes++; continue; }
543
+ existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
544
+ }
545
+ // dry-run: still map the id so the association-count preview isn't always 0
546
+ if (dryRun) { idMap.set(mem.id, mem.id); imported++; continue; }
547
+ const created = await store.createEngram({
548
+ agentId: remapAgent ?? mem.agent_id,
549
+ concept: mem.concept,
550
+ content: mem.content,
551
+ tags: Array.isArray(mem.tags) ? mem.tags : [],
552
+ embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
553
+ confidence: mem.confidence ?? 0.5,
554
+ salience: mem.salience ?? 0.5,
555
+ memoryClass: mem.memory_class ?? 'working',
556
+ memoryType: mem.memory_type ?? undefined,
557
+ episodeId: mem.episode_id ?? undefined,
558
+ taskStatus: mem.task_status ?? undefined,
559
+ taskPriority: mem.task_priority ?? undefined,
560
+ });
561
+ idMap.set(mem.id, created.id);
562
+ // Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
563
+ // without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
564
+ // non-active stage (staging/consolidated/archived/fading) silently flattens to active.
565
+ if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
566
+ try { await store.updateStage(created.id, mem.stage); } catch { /* best-effort */ }
567
+ }
568
+ if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
569
+ try { await store.retractEngram(created.id, (mem as { retracted_by?: string }).retracted_by ?? null); } catch { /* best-effort */ }
570
+ }
571
+ imported++;
572
+ }
573
+
574
+ // Pass 2 — re-link supersession with remapped ids (supersedeEngram sets both sides:
575
+ // old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
576
+ if (!dryRun) {
577
+ for (const mem of importData.memories) {
578
+ const newId = idMap.get(mem.id);
579
+ if (!newId || !mem.supersedes) continue;
580
+ const supersededNew = idMap.get(mem.supersedes);
581
+ if (supersededNew) { try { await store.supersedeEngram(supersededNew, newId); } catch { /* best-effort */ } }
582
+ }
583
+ }
584
+
585
+ // Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
586
+ let assocImported = 0;
587
+ for (const a of (importData.associations ?? [])) {
588
+ const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
589
+ if (!fromId || !toId) continue;
590
+ if (!dryRun) { try { await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5); } catch { /* best-effort */ } }
591
+ assocImported++;
592
+ }
593
+
594
+ const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
595
+ console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
596
+ (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
597
+ (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
598
+ (remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
599
+ ` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
600
+ } finally {
601
+ await close();
602
+ }
603
+ }
604
+
605
+ // ─── MERGE ──────────────────────────────────────
606
+
607
+ async function mergeMemories() {
608
+ const Database = (await import('better-sqlite3')).default;
609
+ const { EngramStore } = await import('./storage/sqlite.js');
610
+ const { createHash, randomUUID } = await import('node:crypto');
611
+
612
+ let target = '';
613
+ const sources: string[] = [];
614
+ const remapEntries = new Map<string, string>();
615
+ let remapAllUuids = '';
616
+ let dedupe = false;
617
+ let dryRun = false;
618
+
619
+ for (let i = 1; i < args.length; i++) {
620
+ if (args[i] === '--target' && args[i + 1]) {
621
+ target = args[++i];
622
+ } else if (args[i] === '--source' && args[i + 1]) {
623
+ sources.push(args[++i]);
624
+ } else if (args[i] === '--remap' && args[i + 1]) {
625
+ const val = args[++i];
626
+ const eqIdx = val.indexOf('=');
627
+ if (eqIdx > 0) remapEntries.set(val.slice(0, eqIdx), val.slice(eqIdx + 1));
628
+ } else if (args[i] === '--remap-all-uuids' && args[i + 1]) {
629
+ remapAllUuids = args[++i];
630
+ } else if (args[i] === '--dedupe') {
631
+ dedupe = true;
632
+ } else if (args[i] === '--dry-run') {
633
+ dryRun = true;
634
+ }
635
+ }
636
+
637
+ if (!target || sources.length === 0) {
638
+ console.error('Usage: awm merge --target <path> --source <path> [--source <path>...] [--remap uuid=name] [--remap-all-uuids name] [--dedupe] [--dry-run]');
639
+ process.exit(1);
640
+ }
641
+
642
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
643
+
644
+ function remapAgentId(agentId: string): string {
645
+ if (remapEntries.has(agentId)) return remapEntries.get(agentId)!;
646
+ if (remapAllUuids && UUID_RE.test(agentId)) return remapAllUuids;
647
+ return agentId;
648
+ }
649
+
650
+ function contentHash(concept: string, content: string): string {
651
+ return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
652
+ }
653
+
654
+ console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
655
+
656
+ // Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
657
+ // triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
658
+ // task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
659
+ const store: any = new EngramStore(target);
660
+ const targetDb = store.db as import('better-sqlite3').Database; // the store's better-sqlite3 handle
661
+
662
+ const blobToArr = (b: unknown): number[] | undefined => {
663
+ const buf = b as Buffer | null | undefined;
664
+ return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
665
+ };
666
+ const parseJson = <T>(s: unknown, fallback: T): T => { try { return s ? JSON.parse(String(s)) as T : fallback; } catch { return fallback; } };
667
+
668
+ // Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
669
+ const existingHashes = new Set<string>();
670
+ if (dedupe) {
671
+ const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
672
+ for (const row of rows) existingHashes.add(contentHash(row.concept, row.content));
673
+ console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
674
+ }
675
+
676
+ const insertAssoc = targetDb.prepare(`
677
+ INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
678
+ activation_count, created_at, last_activated)
679
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
680
+ `);
681
+
682
+ let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
683
+
684
+ try {
685
+ for (const sourcePath of sources) {
686
+ if (!existsSync(sourcePath)) {
687
+ console.error(` Source not found: ${sourcePath}`);
688
+ continue;
689
+ }
690
+
691
+ const sourceDb = new Database(sourcePath, { readonly: true });
692
+ // SELECT * is robust to older source schemas — a column a source predates just reads back undefined
693
+ // and createEngram fills the default.
694
+ const engrams = sourceDb.prepare('SELECT * FROM engrams').all() as any[];
695
+ const assocs = sourceDb.prepare('SELECT * FROM associations').all() as any[];
696
+ sourceDb.close(); // reads done — release the source handle before any (throwing) write work
697
+
698
+ const idMap = new Map<string, string>();
699
+ const skippedIds = new Set<string>();
700
+ let imported = 0, skipped = 0, assocImported = 0;
701
+
702
+ for (const e of engrams) {
703
+ const hash = contentHash(e.concept, e.content);
704
+ if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
705
+ existingHashes.add(hash);
706
+ if (dryRun) { idMap.set(e.id, e.id); imported++; continue; }
707
+ // Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
708
+ // memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
709
+ const created = store.createEngram({
710
+ agentId: remapAgentId(e.agent_id),
711
+ concept: e.concept, content: e.content,
712
+ embedding: blobToArr(e.embedding),
713
+ confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
714
+ salienceFeatures: parseJson(e.salience_features, undefined),
715
+ reasonCodes: parseJson(e.reason_codes, undefined),
716
+ tags: parseJson<string[]>(e.tags, []),
717
+ memoryClass: e.memory_class ?? 'working',
718
+ memoryType: e.memory_type ?? undefined,
719
+ episodeId: e.episode_id ?? undefined,
720
+ taskStatus: e.task_status ?? undefined,
721
+ taskPriority: e.task_priority ?? undefined,
722
+ blockedBy: e.blocked_by ?? undefined,
723
+ ttl: e.ttl ?? undefined,
724
+ sequence: e.sequence ?? undefined,
725
+ references: parseJson(e.references_json, undefined),
726
+ });
727
+ idMap.set(e.id, created.id);
728
+ // preserve stage + retracted (createEngram always mints active/non-retracted)
729
+ if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') { try { store.updateStage(created.id, e.stage); } catch { /* */ } }
730
+ if (e.retracted) { try { store.retractEngram(created.id, e.retracted_by ?? null); } catch { /* */ } }
731
+ imported++;
732
+ }
733
+ // second pass — re-link supersession with remapped ids
734
+ if (!dryRun) {
735
+ for (const e of engrams) {
736
+ const newId = idMap.get(e.id);
737
+ if (!newId || !e.supersedes) continue;
738
+ const supNew = idMap.get(e.supersedes);
739
+ if (supNew) { try { store.supersedeEngram(supNew, newId); } catch { /* */ } }
740
+ }
741
+ }
742
+ for (const a of assocs) {
743
+ if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
744
+ const fromId = idMap.get(a.from_engram_id);
745
+ const toId = idMap.get(a.to_engram_id);
746
+ if (!fromId || !toId) continue;
747
+ if (!dryRun) {
748
+ try {
749
+ insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
750
+ a.activation_count, a.created_at, a.last_activated);
751
+ } catch { /* skip an association whose source row has an unbindable/undefined column */ }
752
+ }
753
+ assocImported++;
754
+ }
755
+
756
+ const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
757
+ console.log(` Source: ${sourcePath}`);
758
+ console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
759
+ console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
760
+ console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
761
+
762
+ totalMemories += imported;
763
+ totalAssociations += assocImported;
764
+ totalSkipped += skipped;
765
+ }
766
+
767
+ } finally {
768
+ try { store.close(); } catch { /* */ }
769
+ }
770
+ console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
771
+ if (dryRun) console.log('(dry run — no data written)');
772
+ }
773
+
774
+ async function migrateCmd() {
775
+ let from = '';
776
+ let to = '';
777
+ let dryRun = false;
778
+ let verbose = false;
779
+
780
+ for (let i = 1; i < args.length; i++) {
781
+ const a = args[i];
782
+ if (a === '--from') { from = args[++i]; }
783
+ else if (a === '--to') { to = args[++i]; }
784
+ else if (a === '--dry-run') { dryRun = true; }
785
+ else if (a === '--verbose' || a === '-v') { verbose = true; }
786
+ else { console.error(`Unknown argument: ${a}`); process.exit(1); }
787
+ }
788
+
789
+ if (!from || !to) {
790
+ console.error('Usage: awm migrate --from <sqlite.db> --to <pglite-dir> [--dry-run] [--verbose]');
791
+ process.exit(1);
792
+ }
793
+
794
+ const { migrate, printStats } = await import('./cli/migrate.js');
795
+ try {
796
+ console.log(`Migrating ${from} → ${to}${dryRun ? ' (dry run)' : ''}`);
797
+ const stats = await migrate({ from, to, dryRun, verbose });
798
+ printStats(stats, dryRun);
799
+ } catch (err) {
800
+ console.error(`Migration failed: ${err instanceof Error ? err.message : String(err)}`);
801
+ process.exit(1);
802
+ }
803
+ }
804
+
805
+ // ─── ONBOARD ──────────────────────────────────────
806
+
807
+ /**
808
+ * Seed the onboarding skill as a canonical memory (idempotent). This is what lets
809
+ * a cold store teach the host agent how to warm-start itself — the agent recalls
810
+ * the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
811
+ */
812
+ async function seedOnboardSkill(dbPath: string, agentId: string): Promise<string> {
813
+ try {
814
+ const { store, close } = await openCliStore(dbPath);
815
+ try {
816
+ const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
817
+ if (existing) return 'Onboarding skill: already present';
818
+ await store.createEngram({
819
+ agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
820
+ tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
821
+ });
822
+ return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
823
+ } finally {
824
+ await close();
825
+ }
826
+ } catch (e: any) {
827
+ return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
828
+ }
829
+ }
830
+
831
+ function onboardCmd() {
832
+ const docs: string[] = [];
833
+ let repo: string | undefined;
834
+ let project = '';
835
+ let agentId = '';
836
+ let purpose: string | undefined;
837
+ let outDir = resolve(process.cwd(), '.awm');
838
+
839
+ for (let i = 1; i < args.length; i++) {
840
+ const a = args[i];
841
+ if (a === '--repo' && args[i + 1]) repo = args[++i];
842
+ else if (a === '--project' && args[i + 1]) project = args[++i];
843
+ else if (a === '--agent' && args[i + 1]) agentId = args[++i];
844
+ else if (a === '--purpose' && args[i + 1]) purpose = args[++i];
845
+ else if (a === '--out' && args[i + 1]) outDir = resolve(args[++i]);
846
+ else if (!a.startsWith('--')) docs.push(a);
847
+ }
848
+
849
+ // Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
850
+ if (docs.length === 0) docs.push(repo ?? process.cwd());
851
+ if (!project) project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
852
+ if (!agentId) agentId = project;
853
+
854
+ const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
855
+ console.log(`
856
+ AWM onboard — warm-start pack for "${project}"
857
+
858
+ Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
859
+ Extracted: ${count} candidate memories (agent: ${agentId})
860
+
861
+ Review: ${reviewPath}
862
+ Pack: ${packPath}
863
+
864
+ Next:
865
+ 1. Edit the review file / pack as needed (delete noise, answer the interview questions).
866
+ 2. Load it: awm import ${packPath} --db <path> --dedupe
867
+ (embeddings backfill on the first consolidation — recall is warm immediately after)
868
+ `.trimEnd());
869
+ }
870
+
871
+ // ─── Dispatch ──────────────────────────────────────
872
+
873
+ switch (command) {
874
+ case 'setup':
875
+ await setup();
876
+ break;
877
877
  case 'plugin':
878
878
  pluginInfo();
879
879
  break;
880
- case 'doctor':
881
- await doctor();
882
- break;
883
- case 'mcp':
884
- mcp();
885
- break;
886
- case 'serve':
887
- serve();
888
- break;
889
- case 'health':
890
- health();
891
- break;
892
- case 'export':
893
- exportMemories();
894
- break;
895
- case 'import':
896
- importMemories();
897
- break;
898
- case 'merge':
899
- mergeMemories();
900
- break;
901
- case 'migrate':
902
- await migrateCmd();
903
- break;
904
- case 'onboard':
905
- onboardCmd();
906
- break;
907
- case '--help':
908
- case '-h':
909
- case undefined:
910
- printUsage();
911
- break;
912
- default:
913
- console.error(`Unknown command: ${command}`);
914
- printUsage();
915
- process.exit(1);
916
- }
880
+ case 'doctor':
881
+ await doctor();
882
+ break;
883
+ case 'mcp':
884
+ mcp();
885
+ break;
886
+ case 'serve':
887
+ serve();
888
+ break;
889
+ case 'health':
890
+ health();
891
+ break;
892
+ case 'export':
893
+ exportMemories();
894
+ break;
895
+ case 'import':
896
+ importMemories();
897
+ break;
898
+ case 'merge':
899
+ mergeMemories();
900
+ break;
901
+ case 'migrate':
902
+ await migrateCmd();
903
+ break;
904
+ case 'onboard':
905
+ onboardCmd();
906
+ break;
907
+ case '--help':
908
+ case '-h':
909
+ case undefined:
910
+ printUsage();
911
+ break;
912
+ default:
913
+ console.error(`Unknown command: ${command}`);
914
+ printUsage();
915
+ process.exit(1);
916
+ }