agent-working-memory 0.5.6 → 0.6.1

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 (135) hide show
  1. package/README.md +78 -43
  2. package/dist/adapters/claude-code.d.ts +4 -0
  3. package/dist/adapters/claude-code.d.ts.map +1 -0
  4. package/dist/adapters/claude-code.js +218 -0
  5. package/dist/adapters/claude-code.js.map +1 -0
  6. package/dist/adapters/codex.d.ts +4 -0
  7. package/dist/adapters/codex.d.ts.map +1 -0
  8. package/dist/adapters/codex.js +226 -0
  9. package/dist/adapters/codex.js.map +1 -0
  10. package/dist/adapters/common.d.ts +34 -0
  11. package/dist/adapters/common.d.ts.map +1 -0
  12. package/dist/adapters/common.js +145 -0
  13. package/dist/adapters/common.js.map +1 -0
  14. package/dist/adapters/cursor.d.ts +4 -0
  15. package/dist/adapters/cursor.d.ts.map +1 -0
  16. package/dist/adapters/cursor.js +138 -0
  17. package/dist/adapters/cursor.js.map +1 -0
  18. package/dist/adapters/http.d.ts +4 -0
  19. package/dist/adapters/http.d.ts.map +1 -0
  20. package/dist/adapters/http.js +88 -0
  21. package/dist/adapters/http.js.map +1 -0
  22. package/dist/adapters/index.d.ts +7 -0
  23. package/dist/adapters/index.d.ts.map +1 -0
  24. package/dist/adapters/index.js +21 -0
  25. package/dist/adapters/index.js.map +1 -0
  26. package/dist/adapters/types.d.ts +65 -0
  27. package/dist/adapters/types.d.ts.map +1 -0
  28. package/dist/adapters/types.js +4 -0
  29. package/dist/adapters/types.js.map +1 -0
  30. package/dist/api/routes.d.ts.map +1 -1
  31. package/dist/api/routes.js +40 -1
  32. package/dist/api/routes.js.map +1 -1
  33. package/dist/cli.js +504 -230
  34. package/dist/cli.js.map +1 -1
  35. package/dist/coordination/events.d.ts +59 -0
  36. package/dist/coordination/events.d.ts.map +1 -0
  37. package/dist/coordination/events.js +28 -0
  38. package/dist/coordination/events.js.map +1 -0
  39. package/dist/coordination/index.d.ts +10 -1
  40. package/dist/coordination/index.d.ts.map +1 -1
  41. package/dist/coordination/index.js +87 -3
  42. package/dist/coordination/index.js.map +1 -1
  43. package/dist/coordination/mcp-tools.d.ts.map +1 -1
  44. package/dist/coordination/mcp-tools.js +10 -5
  45. package/dist/coordination/mcp-tools.js.map +1 -1
  46. package/dist/coordination/peer-decisions.d.ts +40 -0
  47. package/dist/coordination/peer-decisions.d.ts.map +1 -0
  48. package/dist/coordination/peer-decisions.js +82 -0
  49. package/dist/coordination/peer-decisions.js.map +1 -0
  50. package/dist/coordination/plugin-loader.d.ts +18 -0
  51. package/dist/coordination/plugin-loader.d.ts.map +1 -0
  52. package/dist/coordination/plugin-loader.js +55 -0
  53. package/dist/coordination/plugin-loader.js.map +1 -0
  54. package/dist/coordination/plugin.d.ts +40 -0
  55. package/dist/coordination/plugin.d.ts.map +1 -0
  56. package/dist/coordination/plugin.js +22 -0
  57. package/dist/coordination/plugin.js.map +1 -0
  58. package/dist/coordination/routes.d.ts +2 -1
  59. package/dist/coordination/routes.d.ts.map +1 -1
  60. package/dist/coordination/routes.js +1027 -65
  61. package/dist/coordination/routes.js.map +1 -1
  62. package/dist/coordination/schema.d.ts.map +1 -1
  63. package/dist/coordination/schema.js +104 -12
  64. package/dist/coordination/schema.js.map +1 -1
  65. package/dist/coordination/schemas.d.ts +105 -5
  66. package/dist/coordination/schemas.d.ts.map +1 -1
  67. package/dist/coordination/schemas.js +87 -1
  68. package/dist/coordination/schemas.js.map +1 -1
  69. package/dist/coordination/stale.d.ts +2 -0
  70. package/dist/coordination/stale.d.ts.map +1 -1
  71. package/dist/coordination/stale.js +7 -1
  72. package/dist/coordination/stale.js.map +1 -1
  73. package/dist/coordination/types.d.ts +252 -0
  74. package/dist/coordination/types.d.ts.map +1 -0
  75. package/dist/coordination/types.js +8 -0
  76. package/dist/coordination/types.js.map +1 -0
  77. package/dist/coordination/write-mutex.d.ts +26 -0
  78. package/dist/coordination/write-mutex.d.ts.map +1 -0
  79. package/dist/coordination/write-mutex.js +63 -0
  80. package/dist/coordination/write-mutex.js.map +1 -0
  81. package/dist/core/embeddings.d.ts +2 -0
  82. package/dist/core/embeddings.d.ts.map +1 -1
  83. package/dist/core/embeddings.js +4 -0
  84. package/dist/core/embeddings.js.map +1 -1
  85. package/dist/engine/activation.d.ts.map +1 -1
  86. package/dist/engine/activation.js +135 -26
  87. package/dist/engine/activation.js.map +1 -1
  88. package/dist/engine/consolidation.d.ts.map +1 -1
  89. package/dist/engine/consolidation.js +42 -12
  90. package/dist/engine/consolidation.js.map +1 -1
  91. package/dist/engine/retraction.d.ts +3 -1
  92. package/dist/engine/retraction.d.ts.map +1 -1
  93. package/dist/engine/retraction.js +19 -6
  94. package/dist/engine/retraction.js.map +1 -1
  95. package/dist/index.js +82 -16
  96. package/dist/index.js.map +1 -1
  97. package/dist/mcp.js +113 -6
  98. package/dist/mcp.js.map +1 -1
  99. package/dist/storage/sqlite.d.ts +24 -1
  100. package/dist/storage/sqlite.d.ts.map +1 -1
  101. package/dist/storage/sqlite.js +88 -7
  102. package/dist/storage/sqlite.js.map +1 -1
  103. package/dist/types/engram.d.ts +24 -0
  104. package/dist/types/engram.d.ts.map +1 -1
  105. package/dist/types/engram.js.map +1 -1
  106. package/package.json +3 -1
  107. package/src/adapters/claude-code.ts +234 -0
  108. package/src/adapters/codex.ts +262 -0
  109. package/src/adapters/common.ts +172 -0
  110. package/src/adapters/cursor.ts +150 -0
  111. package/src/adapters/http.ts +100 -0
  112. package/src/adapters/index.ts +31 -0
  113. package/src/adapters/types.ts +75 -0
  114. package/src/api/routes.ts +50 -1
  115. package/src/cli.ts +561 -239
  116. package/src/coordination/events.ts +90 -0
  117. package/src/coordination/index.ts +102 -3
  118. package/src/coordination/mcp-tools.ts +10 -5
  119. package/src/coordination/peer-decisions.ts +105 -0
  120. package/src/coordination/plugin-loader.ts +60 -0
  121. package/src/coordination/plugin.ts +44 -0
  122. package/src/coordination/routes.ts +1353 -92
  123. package/src/coordination/schema.ts +91 -12
  124. package/src/coordination/schemas.ts +104 -1
  125. package/src/coordination/stale.ts +11 -2
  126. package/src/coordination/types.ts +311 -0
  127. package/src/coordination/write-mutex.ts +69 -0
  128. package/src/core/embeddings.ts +5 -0
  129. package/src/engine/activation.ts +138 -26
  130. package/src/engine/consolidation.ts +44 -12
  131. package/src/engine/retraction.ts +22 -6
  132. package/src/index.ts +76 -14
  133. package/src/mcp.ts +142 -9
  134. package/src/storage/sqlite.ts +92 -7
  135. package/src/types/engram.ts +28 -0
package/dist/cli.js CHANGED
@@ -10,12 +10,11 @@
10
10
  * awm serve — start the HTTP API server
11
11
  * awm health — check if a running server is healthy
12
12
  */
13
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
14
- import { resolve, basename, join, dirname } from 'node:path';
13
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
14
+ import { resolve, dirname } from 'node:path';
15
15
  import { execSync } from 'node:child_process';
16
- import { randomBytes } from 'node:crypto';
16
+ import { randomUUID } from 'node:crypto';
17
17
  import { fileURLToPath } from 'node:url';
18
- import { homedir as osHomedir } from 'node:os';
19
18
  const __filename = fileURLToPath(import.meta.url);
20
19
  const __dirname = dirname(__filename);
21
20
  // Load .env if present
@@ -43,34 +42,49 @@ function printUsage() {
43
42
  AgentWorkingMemory — Cognitive memory for AI agents
44
43
 
45
44
  Usage:
46
- awm setup [--global] [--agent-id <id>] [--db-path <path>] [--no-claude-md]
47
- [--no-hooks] [--hook-port <port>] Configure MCP for Claude Code
48
- awm mcp Start MCP server (used by Claude Code)
45
+ awm setup [target] [options] Configure AWM for an AI CLI
46
+ awm doctor [target|--all] Validate AWM integrations
47
+ awm mcp Start MCP server (stdio)
49
48
  awm serve [--port <port>] Start HTTP API server
50
49
  awm health [--port <port>] Check server health
50
+ awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
51
+ Export memories to JSON
52
+ awm import <file> --db <path> [--remap-agent <id>] [--dedupe] [--dry-run]
53
+ Import memories from JSON
54
+ awm merge --target <db> --source <db> [--source ...]
55
+ [--remap uuid=name] [--remap-all-uuids <name>]
56
+ [--dedupe] [--dry-run] Merge multiple memory DBs
51
57
 
52
- Setup:
53
- awm setup --global Recommended. Writes ~/.mcp.json so AWM is available
54
- in every project — one brain across all your work.
58
+ Setup targets:
59
+ claude-code (default) .mcp.json + CLAUDE.md + hooks
60
+ codex ~/.codex/config.toml + AGENTS.md
61
+ cursor .cursor/mcp.json + .cursorrules
62
+ http Connection info for HTTP API
55
63
 
56
- awm setup Project-level. Writes .mcp.json in the current directory
57
- and appends workflow instructions to CLAUDE.md.
64
+ Setup options:
65
+ --global Use global scope (recommended for claude-code)
66
+ --agent-id <id> Agent identifier (default: project name)
67
+ --db-path <path> Database path (default: <awm>/data/memory.db)
68
+ --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
69
+ --no-claude-md Alias for --no-instructions
70
+ --no-hooks Skip hook installation
71
+ --hook-port PORT Sidecar port for hooks (default: 8401)
58
72
 
59
- --no-claude-md Skip CLAUDE.md modification
60
- --no-hooks Skip hook installation (no auto-checkpoint)
61
- --hook-port PORT Sidecar port for hooks (default: 8401)
62
-
63
- Restart Claude Code after setup to pick up the new MCP server.
73
+ Examples:
74
+ awm setup --global Claude Code, global (recommended)
75
+ awm setup codex Codex CLI
76
+ awm setup cursor Cursor IDE
77
+ awm setup http Generic HTTP integration
78
+ awm doctor --all Check all configured targets
64
79
  `.trim());
65
80
  }
66
81
  // ─── SETUP ──────────────────────────────────────
67
- function setup() {
68
- const cwd = process.cwd();
69
- const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
82
+ async function setup() {
70
83
  // Parse flags
71
- let agentId = projectName;
84
+ let target = 'claude-code';
85
+ let agentId;
72
86
  let dbPath = null;
73
- let skipClaudeMd = false;
87
+ let skipInstructions = false;
74
88
  let isGlobal = false;
75
89
  let skipHooks = false;
76
90
  let hookPort = '8401';
@@ -81,8 +95,8 @@ function setup() {
81
95
  else if (args[i] === '--db-path' && args[i + 1]) {
82
96
  dbPath = args[++i];
83
97
  }
84
- else if (args[i] === '--no-claude-md') {
85
- skipClaudeMd = true;
98
+ else if (args[i] === '--no-claude-md' || args[i] === '--no-instructions') {
99
+ skipInstructions = true;
86
100
  }
87
101
  else if (args[i] === '--no-hooks') {
88
102
  skipHooks = true;
@@ -92,225 +106,89 @@ function setup() {
92
106
  }
93
107
  else if (args[i] === '--global') {
94
108
  isGlobal = true;
95
- agentId = 'claude'; // unified agent ID for global setup
109
+ }
110
+ else if (!args[i].startsWith('--')) {
111
+ // Positional arg = target
112
+ target = args[i];
96
113
  }
97
114
  }
98
- // Find the package root (where src/mcp.ts lives)
99
- const packageRoot = resolve(__dirname, '..');
100
- const mcpScript = join(packageRoot, 'src', 'mcp.ts');
101
- const mcpDist = join(packageRoot, 'dist', 'mcp.js');
102
- // Determine DB path — default to <awm-root>/data/memory.db (shared across projects)
103
- if (!dbPath) {
104
- dbPath = join(packageRoot, 'data', 'memory.db');
105
- }
106
- const dbDir = dirname(dbPath);
107
- // Ensure data directory exists
108
- if (!existsSync(dbDir)) {
109
- mkdirSync(dbDir, { recursive: true });
110
- console.log(`Created data directory: ${dbDir}`);
111
- }
112
- // Generate hook secret (or reuse existing one)
113
- let hookSecret = '';
114
- const secretPath = join(dirname(dbPath), '.awm-hook-secret');
115
- if (existsSync(secretPath)) {
116
- hookSecret = readFileSync(secretPath, 'utf-8').trim();
117
- }
118
- if (!hookSecret) {
119
- hookSecret = randomBytes(32).toString('hex');
120
- mkdirSync(dirname(secretPath), { recursive: true });
121
- writeFileSync(secretPath, hookSecret + '\n');
122
- }
123
- // Determine command based on platform and whether dist exists
124
- const isWindows = process.platform === 'win32';
125
- const hasDist = existsSync(mcpDist);
126
- const envVars = {
127
- AWM_DB_PATH: (isWindows ? dbPath.replace(/\\/g, '/') : dbPath),
128
- AWM_AGENT_ID: agentId,
129
- AWM_HOOK_PORT: hookPort,
130
- AWM_HOOK_SECRET: hookSecret,
131
- };
132
- let mcpConfig;
133
- if (hasDist) {
134
- mcpConfig = {
135
- command: 'node',
136
- args: [mcpDist.replace(/\\/g, '/')],
137
- env: envVars,
138
- };
139
- }
140
- else if (isWindows) {
141
- mcpConfig = {
142
- command: 'cmd',
143
- args: ['/c', 'npx', 'tsx', mcpScript.replace(/\\/g, '/')],
144
- env: envVars,
145
- };
115
+ // Load adapter
116
+ const { getAdapter } = await import('./adapters/index.js');
117
+ const { buildSetupContext } = await import('./adapters/common.js');
118
+ let adapter;
119
+ try {
120
+ adapter = await getAdapter(target);
146
121
  }
147
- else {
148
- mcpConfig = {
149
- command: 'npx',
150
- args: ['tsx', mcpScript],
151
- env: envVars,
152
- };
153
- }
154
- // Read or create .mcp.json
155
- const mcpJsonPath = isGlobal ? join(osHomedir(), '.mcp.json') : join(cwd, '.mcp.json');
156
- let existing = { mcpServers: {} };
157
- if (existsSync(mcpJsonPath)) {
158
- try {
159
- existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
160
- if (!existing.mcpServers)
161
- existing.mcpServers = {};
162
- }
163
- catch {
164
- existing = { mcpServers: {} };
165
- }
122
+ catch (e) {
123
+ console.error(e.message);
124
+ process.exit(1);
166
125
  }
167
- existing.mcpServers['agent-working-memory'] = mcpConfig;
168
- writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
169
- // Auto-append CLAUDE.md snippet unless --no-claude-md
170
- let claudeMdAction = '';
171
- const claudeMdSnippet = `
172
-
173
- ## Memory (AWM)
174
- You have persistent memory via the agent-working-memory MCP server.
175
-
176
- ### Lifecycle (always do these)
177
- - Session start: call memory_restore to recover previous context
178
- - Starting a task: call memory_task_begin (checkpoints + recalls relevant memories)
179
- - Finishing a task: call memory_task_end with a summary
180
- - Auto-checkpoint: hooks handle compaction, session end, and 15-min timer (no action needed)
181
-
182
- ### Write memory when:
183
- - A project decision is made or changed
184
- - A root cause is discovered after debugging
185
- - A reusable implementation pattern is established
186
- - A user preference, constraint, or requirement is clarified
187
- - A prior assumption is found to be wrong
188
- - A significant piece of work is completed
126
+ // Force global for adapters that don't support project scope
127
+ if (!adapter.supportsProjectScope && !isGlobal) {
128
+ isGlobal = true;
129
+ }
130
+ // Build context
131
+ const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort });
132
+ // Run adapter
133
+ const configAction = adapter.writeMcpConfig(ctx);
134
+ const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
135
+ const hooksAction = adapter.writeHooks(ctx, skipHooks);
136
+ console.log(`
137
+ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
189
138
 
190
- ### Recall memory when:
191
- - Starting work on a new task or subsystem
192
- - Re-entering code you haven't touched recently
193
- - After a failed attempt — check if there's prior knowledge
194
- - Before refactoring or making architectural changes
195
- - When a topic comes up that you might have prior context on
139
+ Agent ID: ${ctx.agentId}
140
+ DB path: ${ctx.dbPath}
141
+ ${configAction}
142
+ ${instructionsAction}
143
+ ${hooksAction}
196
144
 
197
- ### Also:
198
- - After using a recalled memory: call memory_feedback (useful/not-useful)
199
- - To correct wrong info: call memory_retract
200
- - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
201
- `;
202
- // For global: write to ~/.claude/CLAUDE.md (loaded by Claude Code in every session)
203
- // For project: write to ./CLAUDE.md in the current directory
204
- const claudeMdPath = isGlobal
205
- ? join(osHomedir(), '.claude', 'CLAUDE.md')
206
- : join(cwd, 'CLAUDE.md');
207
- // Ensure parent directory exists (for ~/.claude/CLAUDE.md)
208
- const claudeMdDir = dirname(claudeMdPath);
209
- if (!existsSync(claudeMdDir)) {
210
- mkdirSync(claudeMdDir, { recursive: true });
211
- }
212
- if (skipClaudeMd) {
213
- claudeMdAction = ' CLAUDE.md: skipped (--no-claude-md)';
214
- }
215
- else if (existsSync(claudeMdPath)) {
216
- const content = readFileSync(claudeMdPath, 'utf-8');
217
- if (content.includes('## Memory (AWM)')) {
218
- claudeMdAction = ' CLAUDE.md: already has AWM section (skipped)';
145
+ Next steps:
146
+ 1. Restart ${adapter.name} to pick up the MCP server
147
+ 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
148
+ `.trim());
149
+ }
150
+ // ─── DOCTOR ──────────────────────────────────────
151
+ async function doctor() {
152
+ const { getAdapter, listAdapters } = await import('./adapters/index.js');
153
+ const { buildSetupContext } = await import('./adapters/common.js');
154
+ let targets = [];
155
+ let checkAll = false;
156
+ for (let i = 1; i < args.length; i++) {
157
+ if (args[i] === '--all') {
158
+ checkAll = true;
219
159
  }
220
- else {
221
- writeFileSync(claudeMdPath, content.trimEnd() + '\n' + claudeMdSnippet);
222
- claudeMdAction = ' CLAUDE.md: appended AWM workflow section';
160
+ else if (!args[i].startsWith('--')) {
161
+ targets.push(args[i]);
223
162
  }
224
163
  }
225
- else {
226
- const title = isGlobal ? '# Global Instructions' : `# ${basename(cwd)}`;
227
- writeFileSync(claudeMdPath, `${title}\n${claudeMdSnippet}`);
228
- claudeMdAction = ' CLAUDE.md: created with AWM workflow section';
164
+ if (checkAll) {
165
+ targets = listAdapters();
229
166
  }
230
- // --- Hook configuration ---
231
- let hookAction = '';
232
- if (skipHooks) {
233
- hookAction = ' Hooks: skipped (--no-hooks)';
167
+ else if (targets.length === 0) {
168
+ targets = listAdapters();
234
169
  }
235
- else {
236
- // Write hooks to Claude Code settings (~/.claude/settings.json)
237
- const settingsPath = join(osHomedir(), '.claude', 'settings.json');
238
- let settings = {};
239
- if (existsSync(settingsPath)) {
240
- try {
241
- settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
242
- }
243
- catch {
244
- settings = {};
245
- }
170
+ const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
171
+ console.log('AWM Doctor\n');
172
+ for (const targetId of targets) {
173
+ let adapter;
174
+ try {
175
+ adapter = await getAdapter(targetId);
176
+ }
177
+ catch {
178
+ console.log(` ? ${targetId}: unknown target (skipped)`);
179
+ continue;
246
180
  }
247
- if (!settings.hooks)
248
- settings.hooks = {};
249
- const hookUrl = `http://127.0.0.1:${hookPort}/hooks/checkpoint`;
250
- // Stop remind Claude to write/recall/switch tasks after each response
251
- settings.hooks.Stop = [{
252
- matcher: '',
253
- hooks: [{
254
- type: 'command',
255
- command: 'echo "MEMORY: (1) Did you learn anything new? Call memory_write. (2) Are you about to work on a topic you might have prior knowledge about? Call memory_recall. (3) Switching tasks? Call memory_task_begin."',
256
- timeout: 5,
257
- async: true,
258
- }],
259
- }];
260
- // Build hook command with multi-port fallback for separate memory pools.
261
- // When users have work (port 8401) and personal (port 8402) pools via
262
- // per-folder .mcp.json, the hook needs to try both ports since the global
263
- // settings.json can't know which pool is active in the current session.
264
- const altPort = hookPort === '8401' ? '8402' : '8401';
265
- const hookUrlAlt = `http://127.0.0.1:${altPort}/hooks/checkpoint`;
266
- const buildHookCmd = (event, maxTime) => {
267
- const primary = `curl -sf -X POST ${hookUrl} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
268
- const fallback = `curl -sf -X POST ${hookUrlAlt} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
269
- return `${primary} || ${fallback}`;
270
- };
271
- // PreCompact — auto-checkpoint before context compaction
272
- settings.hooks.PreCompact = [{
273
- matcher: '',
274
- hooks: [{
275
- type: 'command',
276
- command: buildHookCmd('PreCompact', 5),
277
- timeout: 10,
278
- }],
279
- }];
280
- // SessionEnd — auto-checkpoint on session close (fast timeout to avoid cancellation)
281
- settings.hooks.SessionEnd = [{
282
- matcher: '',
283
- hooks: [{
284
- type: 'command',
285
- command: buildHookCmd('SessionEnd', 2),
286
- timeout: 5,
287
- }],
288
- }];
289
- // Ensure settings directory exists
290
- const settingsDir = dirname(settingsPath);
291
- if (!existsSync(settingsDir)) {
292
- mkdirSync(settingsDir, { recursive: true });
181
+ console.log(` ${adapter.name}:`);
182
+ const results = adapter.diagnose(ctx);
183
+ for (const r of results) {
184
+ const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
185
+ console.log(` [${icon}] ${r.check}: ${r.message}`);
186
+ if (r.fix) {
187
+ console.log(` Fix: ${r.fix}`);
188
+ }
293
189
  }
294
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
295
- hookAction = ` Hooks: Stop (memory reminder) + PreCompact + SessionEnd → auto-checkpoint (port ${hookPort})`;
190
+ console.log();
296
191
  }
297
- const scope = isGlobal ? 'globally (all projects)' : cwd;
298
- console.log(`
299
- AWM configured ${isGlobal ? 'globally' : 'for: ' + cwd}
300
-
301
- Agent ID: ${agentId}
302
- DB path: ${dbPath}
303
- MCP config: ${mcpJsonPath}
304
- Hook port: ${hookPort}
305
- Hook secret: ${hookSecret.slice(0, 8)}...
306
- ${claudeMdAction}
307
- ${hookAction}
308
-
309
- Next steps:
310
- 1. Restart Claude Code to pick up the MCP server
311
- 2. The memory tools will appear automatically
312
- 3. Hooks auto-checkpoint on context compaction and session end${isGlobal ? '\n 4. One brain across all your projects — no per-project setup needed' : ''}
313
- `.trim());
314
192
  }
315
193
  // ─── MCP ──────────────────────────────────────
316
194
  async function mcp() {
@@ -348,10 +226,397 @@ function health() {
348
226
  process.exit(1);
349
227
  }
350
228
  }
229
+ // ─── EXPORT ──────────────────────────────────────
230
+ async function exportMemories() {
231
+ let dbPath = '';
232
+ let agentFilter = null;
233
+ let outputPath = null;
234
+ let activeOnly = false;
235
+ for (let i = 1; i < args.length; i++) {
236
+ if (args[i] === '--db' && args[i + 1])
237
+ dbPath = args[++i];
238
+ else if (args[i] === '--agent' && args[i + 1])
239
+ agentFilter = args[++i];
240
+ else if (args[i] === '--output' && args[i + 1])
241
+ outputPath = args[++i];
242
+ else if (args[i] === '--active-only')
243
+ activeOnly = true;
244
+ }
245
+ if (!dbPath) {
246
+ console.error('Error: --db <path> is required');
247
+ process.exit(1);
248
+ }
249
+ if (!existsSync(dbPath)) {
250
+ console.error(`Error: database not found: ${dbPath}`);
251
+ process.exit(1);
252
+ }
253
+ // Dynamic import to avoid loading better-sqlite3 for other commands
254
+ const Database = (await import('better-sqlite3')).default;
255
+ const db = new Database(dbPath, { readonly: true });
256
+ // Build memory query
257
+ let memQuery = 'SELECT * FROM engrams';
258
+ const conditions = [];
259
+ const params = [];
260
+ if (agentFilter) {
261
+ conditions.push('agent_id = ?');
262
+ params.push(agentFilter);
263
+ }
264
+ if (activeOnly) {
265
+ conditions.push('retracted = 0');
266
+ }
267
+ if (conditions.length > 0) {
268
+ memQuery += ' WHERE ' + conditions.join(' AND ');
269
+ }
270
+ memQuery += ' ORDER BY created_at ASC';
271
+ const rows = db.prepare(memQuery).all(...params);
272
+ // Build memory objects (exclude embedding blobs)
273
+ const memories = rows.map((r) => ({
274
+ id: r.id,
275
+ agent_id: r.agent_id,
276
+ concept: r.concept,
277
+ content: r.content,
278
+ confidence: r.confidence,
279
+ salience: r.salience,
280
+ access_count: r.access_count,
281
+ last_accessed: r.last_accessed,
282
+ created_at: r.created_at,
283
+ stage: r.stage,
284
+ tags: r.tags ? JSON.parse(r.tags) : [],
285
+ memory_class: r.memory_class ?? 'working',
286
+ episode_id: r.episode_id ?? null,
287
+ task_status: r.task_status ?? null,
288
+ task_priority: r.task_priority ?? null,
289
+ supersedes: r.supersedes ?? null,
290
+ superseded_by: r.superseded_by ?? null,
291
+ retracted: r.retracted ?? 0,
292
+ }));
293
+ // Get memory IDs for association filtering
294
+ const memIds = new Set(memories.map((m) => m.id));
295
+ // Build associations
296
+ let assocQuery = 'SELECT * FROM associations';
297
+ const allAssocs = db.prepare(assocQuery).all();
298
+ const associations = allAssocs
299
+ .filter((a) => memIds.has(a.from_engram_id) && memIds.has(a.to_engram_id))
300
+ .map((a) => ({
301
+ from_id: a.from_engram_id,
302
+ to_id: a.to_engram_id,
303
+ weight: a.weight,
304
+ type: a.type ?? 'hebbian',
305
+ activation_count: a.activation_count ?? 0,
306
+ }));
307
+ // Collect unique agents
308
+ const agents = [...new Set(memories.map((m) => m.agent_id))];
309
+ const exportData = {
310
+ version: '0.6.0',
311
+ exported_at: new Date().toISOString(),
312
+ source_db: dbPath,
313
+ agent_filter: agentFilter,
314
+ memories,
315
+ associations,
316
+ stats: {
317
+ total_memories: memories.length,
318
+ total_associations: associations.length,
319
+ agents,
320
+ },
321
+ };
322
+ const json = JSON.stringify(exportData, null, 2);
323
+ if (outputPath) {
324
+ writeFileSync(outputPath, json + '\n');
325
+ console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath}`);
326
+ }
327
+ else {
328
+ process.stdout.write(json + '\n');
329
+ }
330
+ db.close();
331
+ }
332
+ // ─── IMPORT ──────────────────────────────────────
333
+ async function importMemories() {
334
+ let filePath = '';
335
+ let dbPath = '';
336
+ let remapAgent = null;
337
+ let dedupe = false;
338
+ let dryRun = false;
339
+ let includeRetracted = false;
340
+ // First non-flag arg after 'import' is the file path
341
+ for (let i = 1; i < args.length; i++) {
342
+ if (args[i] === '--db' && args[i + 1])
343
+ dbPath = args[++i];
344
+ else if (args[i] === '--remap-agent' && args[i + 1])
345
+ remapAgent = args[++i];
346
+ else if (args[i] === '--dedupe')
347
+ dedupe = true;
348
+ else if (args[i] === '--dry-run')
349
+ dryRun = true;
350
+ else if (args[i] === '--include-retracted')
351
+ includeRetracted = true;
352
+ else if (!args[i].startsWith('--') && !filePath)
353
+ filePath = args[i];
354
+ }
355
+ if (!filePath) {
356
+ console.error('Error: <file> is required');
357
+ process.exit(1);
358
+ }
359
+ if (!dbPath) {
360
+ console.error('Error: --db <path> is required');
361
+ process.exit(1);
362
+ }
363
+ if (!existsSync(filePath)) {
364
+ console.error(`Error: import file not found: ${filePath}`);
365
+ process.exit(1);
366
+ }
367
+ const importData = JSON.parse(readFileSync(filePath, 'utf-8'));
368
+ if (!importData.memories || !Array.isArray(importData.memories)) {
369
+ console.error('Error: invalid export file — missing memories array');
370
+ process.exit(1);
371
+ }
372
+ const Database = (await import('better-sqlite3')).default;
373
+ const db = new Database(dbPath);
374
+ // Ensure tables exist in target
375
+ db.exec(`
376
+ CREATE TABLE IF NOT EXISTS engrams (
377
+ id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
378
+ embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
379
+ access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
380
+ salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
381
+ stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
382
+ retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]',
383
+ episode_id TEXT, task_status TEXT, task_priority TEXT, blocked_by TEXT,
384
+ memory_class TEXT NOT NULL DEFAULT 'working', superseded_by TEXT, supersedes TEXT
385
+ );
386
+ CREATE TABLE IF NOT EXISTS associations (
387
+ id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
388
+ weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
389
+ type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
390
+ created_at TEXT NOT NULL, last_activated TEXT
391
+ );
392
+ `);
393
+ // Build dedup set if needed
394
+ const existingHashes = new Set();
395
+ if (dedupe) {
396
+ const existing = db.prepare('SELECT concept, content FROM engrams').all();
397
+ for (const row of existing) {
398
+ const hash = (row.concept ?? '').toLowerCase().trim() + '||' + (row.content ?? '').toLowerCase().trim();
399
+ existingHashes.add(hash);
400
+ }
401
+ }
402
+ const idMap = new Map();
403
+ let imported = 0;
404
+ let skippedDupes = 0;
405
+ let skippedRetracted = 0;
406
+ const insertMem = db.prepare(`
407
+ INSERT INTO engrams (id, agent_id, concept, content, confidence, salience,
408
+ access_count, last_accessed, created_at, stage, tags, memory_class,
409
+ episode_id, task_status, task_priority, supersedes, superseded_by, retracted)
410
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
411
+ `);
412
+ const insertAssoc = db.prepare(`
413
+ INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at)
414
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
415
+ `);
416
+ const importTx = db.transaction(() => {
417
+ // Import memories
418
+ for (const mem of importData.memories) {
419
+ // Skip retracted unless --include-retracted
420
+ if (mem.retracted && !includeRetracted) {
421
+ skippedRetracted++;
422
+ continue;
423
+ }
424
+ // Dedupe check
425
+ if (dedupe) {
426
+ const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
427
+ if (existingHashes.has(hash)) {
428
+ skippedDupes++;
429
+ continue;
430
+ }
431
+ }
432
+ const newId = randomUUID();
433
+ idMap.set(mem.id, newId);
434
+ const agentId = remapAgent ?? mem.agent_id;
435
+ const tags = Array.isArray(mem.tags) ? JSON.stringify(mem.tags) : (mem.tags ?? '[]');
436
+ if (!dryRun) {
437
+ insertMem.run(newId, agentId, mem.concept, mem.content, mem.confidence ?? 0.5, mem.salience ?? 0.5, mem.access_count ?? 0, mem.last_accessed ?? mem.created_at, mem.created_at, mem.stage ?? 'active', tags, mem.memory_class ?? 'working', mem.episode_id ?? null, mem.task_status ?? null, mem.task_priority ?? null, mem.supersedes ?? null, mem.superseded_by ?? null, mem.retracted ?? 0);
438
+ }
439
+ imported++;
440
+ }
441
+ // Import associations (using remapped IDs)
442
+ let assocImported = 0;
443
+ const associations = importData.associations ?? [];
444
+ for (const assoc of associations) {
445
+ const fromId = idMap.get(assoc.from_id);
446
+ const toId = idMap.get(assoc.to_id);
447
+ if (!fromId || !toId)
448
+ continue; // skip if either memory was skipped
449
+ if (!dryRun) {
450
+ insertAssoc.run(randomUUID(), fromId, toId, assoc.weight ?? 0.5, assoc.type ?? 'hebbian', assoc.activation_count ?? 0);
451
+ }
452
+ assocImported++;
453
+ }
454
+ return assocImported;
455
+ });
456
+ const assocCount = importTx();
457
+ const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
458
+ console.log(`${prefix} ${imported} memories, ${assocCount} associations` +
459
+ (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
460
+ (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
461
+ (remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
462
+ db.close();
463
+ }
464
+ // ─── MERGE ──────────────────────────────────────
465
+ async function mergeMemories() {
466
+ const Database = (await import('better-sqlite3')).default;
467
+ const { createHash, randomUUID } = await import('node:crypto');
468
+ let target = '';
469
+ const sources = [];
470
+ const remapEntries = new Map();
471
+ let remapAllUuids = '';
472
+ let dedupe = false;
473
+ let dryRun = false;
474
+ for (let i = 1; i < args.length; i++) {
475
+ if (args[i] === '--target' && args[i + 1]) {
476
+ target = args[++i];
477
+ }
478
+ else if (args[i] === '--source' && args[i + 1]) {
479
+ sources.push(args[++i]);
480
+ }
481
+ else if (args[i] === '--remap' && args[i + 1]) {
482
+ const val = args[++i];
483
+ const eqIdx = val.indexOf('=');
484
+ if (eqIdx > 0)
485
+ remapEntries.set(val.slice(0, eqIdx), val.slice(eqIdx + 1));
486
+ }
487
+ else if (args[i] === '--remap-all-uuids' && args[i + 1]) {
488
+ remapAllUuids = args[++i];
489
+ }
490
+ else if (args[i] === '--dedupe') {
491
+ dedupe = true;
492
+ }
493
+ else if (args[i] === '--dry-run') {
494
+ dryRun = true;
495
+ }
496
+ }
497
+ if (!target || sources.length === 0) {
498
+ console.error('Usage: awm merge --target <path> --source <path> [--source <path>...] [--remap uuid=name] [--remap-all-uuids name] [--dedupe] [--dry-run]');
499
+ process.exit(1);
500
+ }
501
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
502
+ function remapAgentId(agentId) {
503
+ if (remapEntries.has(agentId))
504
+ return remapEntries.get(agentId);
505
+ if (remapAllUuids && UUID_RE.test(agentId))
506
+ return remapAllUuids;
507
+ return agentId;
508
+ }
509
+ function contentHash(concept, content) {
510
+ return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
511
+ }
512
+ console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
513
+ const targetDb = new Database(target);
514
+ targetDb.pragma('journal_mode = WAL');
515
+ targetDb.pragma('foreign_keys = ON');
516
+ // Ensure tables exist in target
517
+ targetDb.exec(`
518
+ CREATE TABLE IF NOT EXISTS engrams (
519
+ id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
520
+ embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
521
+ access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
522
+ salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
523
+ stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
524
+ retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
525
+ );
526
+ CREATE TABLE IF NOT EXISTS associations (
527
+ id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
528
+ weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
529
+ type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
530
+ created_at TEXT NOT NULL, last_activated TEXT NOT NULL
531
+ );
532
+ `);
533
+ // Build dedupe hash set from existing target memories
534
+ const existingHashes = new Set();
535
+ if (dedupe) {
536
+ const rows = targetDb.prepare('SELECT concept, content FROM engrams').all();
537
+ for (const row of rows)
538
+ existingHashes.add(contentHash(row.concept, row.content));
539
+ console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
540
+ }
541
+ const insertEngram = targetDb.prepare(`
542
+ INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
543
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
544
+ retracted, retracted_by, retracted_at, tags)
545
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
546
+ `);
547
+ const insertAssoc = targetDb.prepare(`
548
+ INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
549
+ activation_count, created_at, last_activated)
550
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
551
+ `);
552
+ let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
553
+ for (const sourcePath of sources) {
554
+ if (!existsSync(sourcePath)) {
555
+ console.error(` Source not found: ${sourcePath}`);
556
+ continue;
557
+ }
558
+ const sourceDb = new Database(sourcePath, { readonly: true });
559
+ const engrams = sourceDb.prepare(`SELECT id, agent_id, concept, content, confidence, salience, access_count,
560
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
561
+ retracted, retracted_by, retracted_at, tags FROM engrams`).all();
562
+ const assocs = sourceDb.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
563
+ activation_count, created_at, last_activated FROM associations`).all();
564
+ const idMap = new Map();
565
+ const skippedIds = new Set();
566
+ const result = targetDb.transaction(() => {
567
+ let imported = 0, skipped = 0;
568
+ for (const e of engrams) {
569
+ const hash = contentHash(e.concept, e.content);
570
+ if (dedupe && existingHashes.has(hash)) {
571
+ skippedIds.add(e.id);
572
+ skipped++;
573
+ continue;
574
+ }
575
+ const newId = randomUUID();
576
+ idMap.set(e.id, newId);
577
+ existingHashes.add(hash);
578
+ if (!dryRun) {
579
+ insertEngram.run(newId, remapAgentId(e.agent_id), e.concept, e.content, e.confidence, e.salience, e.access_count, e.last_accessed, e.created_at, e.salience_features, e.reason_codes, e.stage, e.ttl, e.retracted, e.retracted_by, e.retracted_at, e.tags);
580
+ }
581
+ imported++;
582
+ }
583
+ let assocImported = 0;
584
+ for (const a of assocs) {
585
+ if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id))
586
+ continue;
587
+ const fromId = idMap.get(a.from_engram_id);
588
+ const toId = idMap.get(a.to_engram_id);
589
+ if (!fromId || !toId)
590
+ continue;
591
+ if (!dryRun) {
592
+ insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type, a.activation_count, a.created_at, a.last_activated);
593
+ }
594
+ assocImported++;
595
+ }
596
+ return { imported, skipped, assocImported };
597
+ })();
598
+ sourceDb.close();
599
+ const agentSet = new Set(engrams.map((e) => remapAgentId(e.agent_id)));
600
+ console.log(` Source: ${sourcePath}`);
601
+ console.log(` Engrams: ${engrams.length} total, ${result.imported} imported, ${result.skipped} skipped`);
602
+ console.log(` Associations: ${assocs.length} total, ${result.assocImported} imported`);
603
+ console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
604
+ totalMemories += result.imported;
605
+ totalAssociations += result.assocImported;
606
+ totalSkipped += result.skipped;
607
+ }
608
+ targetDb.close();
609
+ console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
610
+ if (dryRun)
611
+ console.log('(dry run — no data written)');
612
+ }
351
613
  // ─── Dispatch ──────────────────────────────────────
352
614
  switch (command) {
353
615
  case 'setup':
354
- setup();
616
+ await setup();
617
+ break;
618
+ case 'doctor':
619
+ await doctor();
355
620
  break;
356
621
  case 'mcp':
357
622
  mcp();
@@ -362,6 +627,15 @@ switch (command) {
362
627
  case 'health':
363
628
  health();
364
629
  break;
630
+ case 'export':
631
+ exportMemories();
632
+ break;
633
+ case 'import':
634
+ importMemories();
635
+ break;
636
+ case 'merge':
637
+ mergeMemories();
638
+ break;
365
639
  case '--help':
366
640
  case '-h':
367
641
  case undefined: