agent-working-memory 0.15.5 → 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/README.md +5 -4
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +18 -1
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/common.d.ts +17 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +46 -4
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cli.js +90 -74
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -1
- package/plugin/bin/awm-mcp-launcher.cjs +2 -2
- package/plugin/skills/awm-memory/SKILL.md +1 -1
- package/src/adapters/claude-code.ts +18 -1
- package/src/adapters/common.ts +46 -5
- package/src/adapters/types.ts +3 -1
- package/src/cli.ts +859 -842
package/src/cli.ts
CHANGED
|
@@ -1,179 +1,196 @@
|
|
|
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:
|
|
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);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
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
|
+
|
|
160
|
+
// A rescue must be loud. The user did not ask for their store to move, and the reason it
|
|
161
|
+
// moved is that the old location was about to be deleted by their next npm upgrade.
|
|
162
|
+
if (ctx.rescuedDbFrom) {
|
|
163
|
+
console.log(`
|
|
164
|
+
!! YOUR MEMORY STORE WAS MOVED
|
|
165
|
+
|
|
166
|
+
from: ${ctx.rescuedDbFrom}
|
|
167
|
+
to: ${ctx.dbPath}
|
|
168
|
+
|
|
169
|
+
The old path is inside the installed npm package. \`npm install -g\` renames that
|
|
170
|
+
directory aside and deletes it, so the next upgrade would have taken your memories
|
|
171
|
+
with it — and \`npm uninstall -g\` would have done the same without a word.
|
|
172
|
+
|
|
173
|
+
The file was COPIED, not moved: the original is still there if you want to check it.
|
|
174
|
+
Once you are satisfied, the old copy can be deleted.`);
|
|
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
|
+
|
|
177
194
|
// ─── PLUGIN ──────────────────────────────────────
|
|
178
195
|
|
|
179
196
|
/**
|
|
@@ -226,674 +243,674 @@ AWM ${VERSION} — install as a Claude Code plugin
|
|
|
226
243
|
}
|
|
227
244
|
|
|
228
245
|
|
|
229
|
-
// ─── DOCTOR ──────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
async function doctor() {
|
|
232
|
-
const { getAdapter, listAdapters } = await import('./adapters/index.js');
|
|
233
|
-
const { buildSetupContext } = await import('./adapters/common.js');
|
|
234
|
-
|
|
235
|
-
let targets: string[] = [];
|
|
236
|
-
let checkAll = false;
|
|
237
|
-
|
|
238
|
-
for (let i = 1; i < args.length; i++) {
|
|
239
|
-
if (args[i] === '--all') {
|
|
240
|
-
checkAll = true;
|
|
241
|
-
} else if (!args[i].startsWith('--')) {
|
|
242
|
-
targets.push(args[i]);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (checkAll) {
|
|
247
|
-
targets = listAdapters();
|
|
248
|
-
} else if (targets.length === 0) {
|
|
249
|
-
targets = listAdapters();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
|
|
253
|
-
|
|
254
|
-
console.log('AWM Doctor\n');
|
|
255
|
-
|
|
256
|
-
for (const targetId of targets) {
|
|
257
|
-
let adapter;
|
|
258
|
-
try {
|
|
259
|
-
adapter = await getAdapter(targetId);
|
|
260
|
-
} catch {
|
|
261
|
-
console.log(` ? ${targetId}: unknown target (skipped)`);
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
console.log(` ${adapter.name}:`);
|
|
266
|
-
const existingEnv = adapter.readExistingEnv?.(true, process.cwd()) ?? null;
|
|
267
|
-
const ctx = buildSetupContext({ isGlobal: true, existingEnv });
|
|
268
|
-
const results = await adapter.diagnose(ctx);
|
|
269
|
-
for (const r of results) {
|
|
270
|
-
const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
|
|
271
|
-
console.log(` [${icon}] ${r.check}: ${r.message}`);
|
|
272
|
-
if (r.fix) {
|
|
273
|
-
console.log(` Fix: ${r.fix}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
console.log();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ─── MCP ──────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
async function mcp() {
|
|
283
|
-
// Dynamic import to avoid loading heavy deps for setup/health commands
|
|
284
|
-
await import('./mcp.js');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ─── SERVE ──────────────────────────────────────
|
|
288
|
-
|
|
289
|
-
async function serve() {
|
|
290
|
-
// Parse --port flag
|
|
291
|
-
for (let i = 1; i < args.length; i++) {
|
|
292
|
-
if (args[i] === '--port' && args[i + 1]) {
|
|
293
|
-
process.env.AWM_PORT = args[++i];
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
await import('./index.js');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ─── HEALTH ──────────────────────────────────────
|
|
300
|
-
|
|
301
|
-
function health() {
|
|
302
|
-
let port = '8400';
|
|
303
|
-
for (let i = 1; i < args.length; i++) {
|
|
304
|
-
if (args[i] === '--port' && args[i + 1]) {
|
|
305
|
-
port = args[++i];
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
const result = execSync(`curl -sf http://localhost:${port}/health`, {
|
|
311
|
-
encoding: 'utf8',
|
|
312
|
-
timeout: 5000,
|
|
313
|
-
});
|
|
314
|
-
const data = JSON.parse(result);
|
|
315
|
-
console.log(`OK — v${data.version} (${data.timestamp})`);
|
|
316
|
-
} catch {
|
|
317
|
-
console.error(`Cannot reach AWM server on port ${port}`);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
|
|
323
|
-
//
|
|
324
|
-
// export/import route through openStore() so they work on ANY backend (SQLite,
|
|
325
|
-
// PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
|
|
326
|
-
// (a SQLite file or PGlite dir, by shape); for a Postgres target set
|
|
327
|
-
// AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
|
|
328
|
-
// port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
|
|
329
|
-
|
|
330
|
-
function toISOStr(d: Date | string | null | undefined): string | null {
|
|
331
|
-
if (d == null) return null;
|
|
332
|
-
return d instanceof Date ? d.toISOString() : String(d);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function openCliStore(dbPath?: string): Promise<{ store: any; backend: string; close: () => Promise<void> }> {
|
|
336
|
-
// --db sets the path only when the env doesn't already select a backend/path.
|
|
337
|
-
if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
|
|
338
|
-
process.env.AWM_DB_PATH = dbPath;
|
|
339
|
-
}
|
|
340
|
-
const { openStore } = await import('./storage/factory.js');
|
|
341
|
-
const { store, backend } = await openStore();
|
|
342
|
-
return { store, backend, close: async () => { try { await store.close?.(); } catch { /* */ } } };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ─── EXPORT ──────────────────────────────────────
|
|
346
|
-
|
|
347
|
-
async function exportMemories() {
|
|
348
|
-
let dbPath = '';
|
|
349
|
-
let agentFilter: string | null = null;
|
|
350
|
-
let outputPath: string | null = null;
|
|
351
|
-
let activeOnly = false;
|
|
352
|
-
let allStages = false;
|
|
353
|
-
let includeRetracted = false;
|
|
354
|
-
|
|
355
|
-
for (let i = 1; i < args.length; i++) {
|
|
356
|
-
if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
|
|
357
|
-
else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
|
|
358
|
-
else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
|
|
359
|
-
else if (args[i] === '--active-only') activeOnly = true;
|
|
360
|
-
else if (args[i] === '--all-stages') allStages = true;
|
|
361
|
-
else if (args[i] === '--include-retracted') includeRetracted = true;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// --db must exist for a file/dir backend; a Postgres source is selected by env instead.
|
|
365
|
-
const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
|
|
366
|
-
if (!usingPostgres) {
|
|
367
|
-
if (!dbPath) { console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)'); process.exit(1); }
|
|
368
|
-
if (!existsSync(dbPath)) { console.error(`Error: database not found: ${dbPath}`); process.exit(1); }
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const { store, backend, close } = await openCliStore(dbPath);
|
|
372
|
-
try {
|
|
373
|
-
const agentIds: string[] = agentFilter
|
|
374
|
-
? [agentFilter]
|
|
375
|
-
: ((await store.getActiveAgents()) as any[]).map((a) => a.agentId);
|
|
376
|
-
if (agentIds.length === 0) {
|
|
377
|
-
console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
|
|
378
|
-
}
|
|
379
|
-
// Default to the meaningful memory set (active stage, non-retracted). --all-stages
|
|
380
|
-
// widens to every stage; --include-retracted adds retracted (off with --active-only).
|
|
381
|
-
const stage = allStages ? undefined : 'active';
|
|
382
|
-
const wantRetracted = includeRetracted && !activeOnly;
|
|
383
|
-
const engrams: any[] = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
|
|
384
|
-
|
|
385
|
-
const memories = engrams.map((e: any) => ({
|
|
386
|
-
id: e.id,
|
|
387
|
-
agent_id: e.agentId,
|
|
388
|
-
concept: e.concept,
|
|
389
|
-
content: e.content,
|
|
390
|
-
// Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
|
|
391
|
-
// re-embed after import) → a faithful, recall-ready port when source/target embed
|
|
392
|
-
// models match. import skips them with --no-embeddings (then re-embed).
|
|
393
|
-
embedding: Array.isArray(e.embedding) ? e.embedding : null,
|
|
394
|
-
confidence: e.confidence,
|
|
395
|
-
salience: e.salience,
|
|
396
|
-
access_count: e.accessCount ?? 0,
|
|
397
|
-
last_accessed: toISOStr(e.lastAccessed),
|
|
398
|
-
created_at: toISOStr(e.createdAt),
|
|
399
|
-
stage: e.stage ?? 'active',
|
|
400
|
-
tags: Array.isArray(e.tags) ? e.tags : [],
|
|
401
|
-
memory_class: e.memoryClass ?? 'working',
|
|
402
|
-
memory_type: e.memoryType ?? 'unclassified',
|
|
403
|
-
episode_id: e.episodeId ?? null,
|
|
404
|
-
task_status: e.taskStatus ?? null,
|
|
405
|
-
task_priority: e.taskPriority ?? null,
|
|
406
|
-
supersedes: e.supersedes ?? null,
|
|
407
|
-
superseded_by: e.supersededBy ?? null,
|
|
408
|
-
retracted: e.retracted ? 1 : 0,
|
|
409
|
-
}));
|
|
410
|
-
|
|
411
|
-
const memIds = new Set(memories.map((m) => m.id));
|
|
412
|
-
const seen = new Set<string>();
|
|
413
|
-
const associations: any[] = [];
|
|
414
|
-
for (const aid of agentIds) {
|
|
415
|
-
for (const a of ((await store.getAllAssociations(aid)) as any[]) ?? []) {
|
|
416
|
-
if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId)) continue;
|
|
417
|
-
const k = `${a.fromEngramId}>${a.toEngramId}`;
|
|
418
|
-
if (seen.has(k)) continue;
|
|
419
|
-
seen.add(k);
|
|
420
|
-
associations.push({
|
|
421
|
-
from_id: a.fromEngramId, to_id: a.toEngramId,
|
|
422
|
-
weight: a.weight, type: a.type ?? 'hebbian',
|
|
423
|
-
activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const exportData = {
|
|
429
|
-
version: VERSION,
|
|
430
|
-
exported_at: new Date().toISOString(),
|
|
431
|
-
source_backend: backend,
|
|
432
|
-
agent_filter: agentFilter,
|
|
433
|
-
embedding_model: process.env.AWM_EMBED_MODEL ?? null,
|
|
434
|
-
memories,
|
|
435
|
-
associations,
|
|
436
|
-
stats: {
|
|
437
|
-
total_memories: memories.length,
|
|
438
|
-
total_associations: associations.length,
|
|
439
|
-
agents: [...new Set(memories.map((m) => m.agent_id))],
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
const json = JSON.stringify(exportData, null, 2);
|
|
444
|
-
if (outputPath) {
|
|
445
|
-
writeFileSync(outputPath, json + '\n');
|
|
446
|
-
console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
|
|
447
|
-
} else {
|
|
448
|
-
process.stdout.write(json + '\n');
|
|
449
|
-
}
|
|
450
|
-
} finally {
|
|
451
|
-
await close();
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// ─── IMPORT ──────────────────────────────────────
|
|
456
|
-
|
|
457
|
-
async function importMemories() {
|
|
458
|
-
let filePath = '';
|
|
459
|
-
let dbPath = '';
|
|
460
|
-
let remapAgent: string | null = null;
|
|
461
|
-
let dedupe = false;
|
|
462
|
-
let dryRun = false;
|
|
463
|
-
let includeRetracted = false;
|
|
464
|
-
|
|
465
|
-
// First non-flag arg after 'import' is the file path
|
|
466
|
-
for (let i = 1; i < args.length; i++) {
|
|
467
|
-
if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
|
|
468
|
-
else if (args[i] === '--remap-agent' && args[i + 1]) remapAgent = args[++i];
|
|
469
|
-
else if (args[i] === '--dedupe') dedupe = true;
|
|
470
|
-
else if (args[i] === '--dry-run') dryRun = true;
|
|
471
|
-
else if (args[i] === '--include-retracted') includeRetracted = true;
|
|
472
|
-
else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// --no-embeddings: skip importing embedding vectors (use when source/target embed models
|
|
476
|
-
// differ → import without, then re-embed). Parsed alongside the existing flags above.
|
|
477
|
-
const noEmbeddings = args.includes('--no-embeddings');
|
|
478
|
-
|
|
479
|
-
if (!filePath) {
|
|
480
|
-
console.error('Error: <file> is required');
|
|
481
|
-
process.exit(1);
|
|
482
|
-
}
|
|
483
|
-
const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
|
|
484
|
-
if (!dbPath && !usingPostgres) {
|
|
485
|
-
console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
|
|
486
|
-
process.exit(1);
|
|
487
|
-
}
|
|
488
|
-
if (!existsSync(filePath)) {
|
|
489
|
-
console.error(`Error: import file not found: ${filePath}`);
|
|
490
|
-
process.exit(1);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const importData = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
494
|
-
if (!importData.memories || !Array.isArray(importData.memories)) {
|
|
495
|
-
console.error('Error: invalid export file — missing memories array');
|
|
496
|
-
process.exit(1);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const { store, backend, close } = await openCliStore(dbPath);
|
|
500
|
-
try {
|
|
501
|
-
// Dedupe against existing memories for the TARGET agent(s).
|
|
502
|
-
const existingHashes = new Set<string>();
|
|
503
|
-
if (dedupe) {
|
|
504
|
-
const targetAgents = remapAgent
|
|
505
|
-
? [remapAgent]
|
|
506
|
-
: [...new Set(importData.memories.map((m: any) => m.agent_id))] as string[];
|
|
507
|
-
for (const aid of targetAgents) {
|
|
508
|
-
for (const e of ((await store.getEngramsByAgent(aid, undefined, true)) as any[]) ?? []) {
|
|
509
|
-
existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const idMap = new Map<string, string>(); // old export id → new store id
|
|
515
|
-
let imported = 0, skippedDupes = 0, skippedRetracted = 0;
|
|
516
|
-
|
|
517
|
-
// Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
|
|
518
|
-
// createdAt/accessCount normalize to import time (the contract's createEngram stamps
|
|
519
|
-
// them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
|
|
520
|
-
// embedding) is preserved, so recall is faithful.
|
|
521
|
-
for (const mem of importData.memories) {
|
|
522
|
-
if (mem.retracted && !includeRetracted) { skippedRetracted++; continue; }
|
|
523
|
-
if (dedupe) {
|
|
524
|
-
const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
|
|
525
|
-
if (existingHashes.has(h)) { skippedDupes++; continue; }
|
|
526
|
-
existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
|
|
527
|
-
}
|
|
528
|
-
// dry-run: still map the id so the association-count preview isn't always 0
|
|
529
|
-
if (dryRun) { idMap.set(mem.id, mem.id); imported++; continue; }
|
|
530
|
-
const created = await store.createEngram({
|
|
531
|
-
agentId: remapAgent ?? mem.agent_id,
|
|
532
|
-
concept: mem.concept,
|
|
533
|
-
content: mem.content,
|
|
534
|
-
tags: Array.isArray(mem.tags) ? mem.tags : [],
|
|
535
|
-
embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
|
|
536
|
-
confidence: mem.confidence ?? 0.5,
|
|
537
|
-
salience: mem.salience ?? 0.5,
|
|
538
|
-
memoryClass: mem.memory_class ?? 'working',
|
|
539
|
-
memoryType: mem.memory_type ?? undefined,
|
|
540
|
-
episodeId: mem.episode_id ?? undefined,
|
|
541
|
-
taskStatus: mem.task_status ?? undefined,
|
|
542
|
-
taskPriority: mem.task_priority ?? undefined,
|
|
543
|
-
});
|
|
544
|
-
idMap.set(mem.id, created.id);
|
|
545
|
-
// Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
|
|
546
|
-
// without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
|
|
547
|
-
// non-active stage (staging/consolidated/archived/fading) silently flattens to active.
|
|
548
|
-
if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
|
|
549
|
-
try { await store.updateStage(created.id, mem.stage); } catch { /* best-effort */ }
|
|
550
|
-
}
|
|
551
|
-
if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
|
|
552
|
-
try { await store.retractEngram(created.id, (mem as { retracted_by?: string }).retracted_by ?? null); } catch { /* best-effort */ }
|
|
553
|
-
}
|
|
554
|
-
imported++;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Pass 2 — re-link supersession with remapped ids (supersedeEngram sets both sides:
|
|
558
|
-
// old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
|
|
559
|
-
if (!dryRun) {
|
|
560
|
-
for (const mem of importData.memories) {
|
|
561
|
-
const newId = idMap.get(mem.id);
|
|
562
|
-
if (!newId || !mem.supersedes) continue;
|
|
563
|
-
const supersededNew = idMap.get(mem.supersedes);
|
|
564
|
-
if (supersededNew) { try { await store.supersedeEngram(supersededNew, newId); } catch { /* best-effort */ } }
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
|
|
569
|
-
let assocImported = 0;
|
|
570
|
-
for (const a of (importData.associations ?? [])) {
|
|
571
|
-
const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
|
|
572
|
-
if (!fromId || !toId) continue;
|
|
573
|
-
if (!dryRun) { try { await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5); } catch { /* best-effort */ } }
|
|
574
|
-
assocImported++;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
|
|
578
|
-
console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
|
|
579
|
-
(skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
|
|
580
|
-
(skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
|
|
581
|
-
(remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
|
|
582
|
-
` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
|
|
583
|
-
} finally {
|
|
584
|
-
await close();
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// ─── MERGE ──────────────────────────────────────
|
|
589
|
-
|
|
590
|
-
async function mergeMemories() {
|
|
591
|
-
const Database = (await import('better-sqlite3')).default;
|
|
592
|
-
const { EngramStore } = await import('./storage/sqlite.js');
|
|
593
|
-
const { createHash, randomUUID } = await import('node:crypto');
|
|
594
|
-
|
|
595
|
-
let target = '';
|
|
596
|
-
const sources: string[] = [];
|
|
597
|
-
const remapEntries = new Map<string, string>();
|
|
598
|
-
let remapAllUuids = '';
|
|
599
|
-
let dedupe = false;
|
|
600
|
-
let dryRun = false;
|
|
601
|
-
|
|
602
|
-
for (let i = 1; i < args.length; i++) {
|
|
603
|
-
if (args[i] === '--target' && args[i + 1]) {
|
|
604
|
-
target = args[++i];
|
|
605
|
-
} else if (args[i] === '--source' && args[i + 1]) {
|
|
606
|
-
sources.push(args[++i]);
|
|
607
|
-
} else if (args[i] === '--remap' && args[i + 1]) {
|
|
608
|
-
const val = args[++i];
|
|
609
|
-
const eqIdx = val.indexOf('=');
|
|
610
|
-
if (eqIdx > 0) remapEntries.set(val.slice(0, eqIdx), val.slice(eqIdx + 1));
|
|
611
|
-
} else if (args[i] === '--remap-all-uuids' && args[i + 1]) {
|
|
612
|
-
remapAllUuids = args[++i];
|
|
613
|
-
} else if (args[i] === '--dedupe') {
|
|
614
|
-
dedupe = true;
|
|
615
|
-
} else if (args[i] === '--dry-run') {
|
|
616
|
-
dryRun = true;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (!target || sources.length === 0) {
|
|
621
|
-
console.error('Usage: awm merge --target <path> --source <path> [--source <path>...] [--remap uuid=name] [--remap-all-uuids name] [--dedupe] [--dry-run]');
|
|
622
|
-
process.exit(1);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
626
|
-
|
|
627
|
-
function remapAgentId(agentId: string): string {
|
|
628
|
-
if (remapEntries.has(agentId)) return remapEntries.get(agentId)!;
|
|
629
|
-
if (remapAllUuids && UUID_RE.test(agentId)) return remapAllUuids;
|
|
630
|
-
return agentId;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function contentHash(concept: string, content: string): string {
|
|
634
|
-
return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
|
|
638
|
-
|
|
639
|
-
// Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
|
|
640
|
-
// triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
|
|
641
|
-
// task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
|
|
642
|
-
const store: any = new EngramStore(target);
|
|
643
|
-
const targetDb = store.db as import('better-sqlite3').Database; // the store's better-sqlite3 handle
|
|
644
|
-
|
|
645
|
-
const blobToArr = (b: unknown): number[] | undefined => {
|
|
646
|
-
const buf = b as Buffer | null | undefined;
|
|
647
|
-
return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
|
|
648
|
-
};
|
|
649
|
-
const parseJson = <T>(s: unknown, fallback: T): T => { try { return s ? JSON.parse(String(s)) as T : fallback; } catch { return fallback; } };
|
|
650
|
-
|
|
651
|
-
// Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
|
|
652
|
-
const existingHashes = new Set<string>();
|
|
653
|
-
if (dedupe) {
|
|
654
|
-
const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
|
|
655
|
-
for (const row of rows) existingHashes.add(contentHash(row.concept, row.content));
|
|
656
|
-
console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const insertAssoc = targetDb.prepare(`
|
|
660
|
-
INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
|
|
661
|
-
activation_count, created_at, last_activated)
|
|
662
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
663
|
-
`);
|
|
664
|
-
|
|
665
|
-
let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
|
|
666
|
-
|
|
667
|
-
try {
|
|
668
|
-
for (const sourcePath of sources) {
|
|
669
|
-
if (!existsSync(sourcePath)) {
|
|
670
|
-
console.error(` Source not found: ${sourcePath}`);
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const sourceDb = new Database(sourcePath, { readonly: true });
|
|
675
|
-
// SELECT * is robust to older source schemas — a column a source predates just reads back undefined
|
|
676
|
-
// and createEngram fills the default.
|
|
677
|
-
const engrams = sourceDb.prepare('SELECT * FROM engrams').all() as any[];
|
|
678
|
-
const assocs = sourceDb.prepare('SELECT * FROM associations').all() as any[];
|
|
679
|
-
sourceDb.close(); // reads done — release the source handle before any (throwing) write work
|
|
680
|
-
|
|
681
|
-
const idMap = new Map<string, string>();
|
|
682
|
-
const skippedIds = new Set<string>();
|
|
683
|
-
let imported = 0, skipped = 0, assocImported = 0;
|
|
684
|
-
|
|
685
|
-
for (const e of engrams) {
|
|
686
|
-
const hash = contentHash(e.concept, e.content);
|
|
687
|
-
if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
|
|
688
|
-
existingHashes.add(hash);
|
|
689
|
-
if (dryRun) { idMap.set(e.id, e.id); imported++; continue; }
|
|
690
|
-
// Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
|
|
691
|
-
// memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
|
|
692
|
-
const created = store.createEngram({
|
|
693
|
-
agentId: remapAgentId(e.agent_id),
|
|
694
|
-
concept: e.concept, content: e.content,
|
|
695
|
-
embedding: blobToArr(e.embedding),
|
|
696
|
-
confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
|
|
697
|
-
salienceFeatures: parseJson(e.salience_features, undefined),
|
|
698
|
-
reasonCodes: parseJson(e.reason_codes, undefined),
|
|
699
|
-
tags: parseJson<string[]>(e.tags, []),
|
|
700
|
-
memoryClass: e.memory_class ?? 'working',
|
|
701
|
-
memoryType: e.memory_type ?? undefined,
|
|
702
|
-
episodeId: e.episode_id ?? undefined,
|
|
703
|
-
taskStatus: e.task_status ?? undefined,
|
|
704
|
-
taskPriority: e.task_priority ?? undefined,
|
|
705
|
-
blockedBy: e.blocked_by ?? undefined,
|
|
706
|
-
ttl: e.ttl ?? undefined,
|
|
707
|
-
sequence: e.sequence ?? undefined,
|
|
708
|
-
references: parseJson(e.references_json, undefined),
|
|
709
|
-
});
|
|
710
|
-
idMap.set(e.id, created.id);
|
|
711
|
-
// preserve stage + retracted (createEngram always mints active/non-retracted)
|
|
712
|
-
if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') { try { store.updateStage(created.id, e.stage); } catch { /* */ } }
|
|
713
|
-
if (e.retracted) { try { store.retractEngram(created.id, e.retracted_by ?? null); } catch { /* */ } }
|
|
714
|
-
imported++;
|
|
715
|
-
}
|
|
716
|
-
// second pass — re-link supersession with remapped ids
|
|
717
|
-
if (!dryRun) {
|
|
718
|
-
for (const e of engrams) {
|
|
719
|
-
const newId = idMap.get(e.id);
|
|
720
|
-
if (!newId || !e.supersedes) continue;
|
|
721
|
-
const supNew = idMap.get(e.supersedes);
|
|
722
|
-
if (supNew) { try { store.supersedeEngram(supNew, newId); } catch { /* */ } }
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
for (const a of assocs) {
|
|
726
|
-
if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
|
|
727
|
-
const fromId = idMap.get(a.from_engram_id);
|
|
728
|
-
const toId = idMap.get(a.to_engram_id);
|
|
729
|
-
if (!fromId || !toId) continue;
|
|
730
|
-
if (!dryRun) {
|
|
731
|
-
try {
|
|
732
|
-
insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
|
|
733
|
-
a.activation_count, a.created_at, a.last_activated);
|
|
734
|
-
} catch { /* skip an association whose source row has an unbindable/undefined column */ }
|
|
735
|
-
}
|
|
736
|
-
assocImported++;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
|
|
740
|
-
console.log(` Source: ${sourcePath}`);
|
|
741
|
-
console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
|
|
742
|
-
console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
|
|
743
|
-
console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
|
|
744
|
-
|
|
745
|
-
totalMemories += imported;
|
|
746
|
-
totalAssociations += assocImported;
|
|
747
|
-
totalSkipped += skipped;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
} finally {
|
|
751
|
-
try { store.close(); } catch { /* */ }
|
|
752
|
-
}
|
|
753
|
-
console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
|
|
754
|
-
if (dryRun) console.log('(dry run — no data written)');
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
async function migrateCmd() {
|
|
758
|
-
let from = '';
|
|
759
|
-
let to = '';
|
|
760
|
-
let dryRun = false;
|
|
761
|
-
let verbose = false;
|
|
762
|
-
|
|
763
|
-
for (let i = 1; i < args.length; i++) {
|
|
764
|
-
const a = args[i];
|
|
765
|
-
if (a === '--from') { from = args[++i]; }
|
|
766
|
-
else if (a === '--to') { to = args[++i]; }
|
|
767
|
-
else if (a === '--dry-run') { dryRun = true; }
|
|
768
|
-
else if (a === '--verbose' || a === '-v') { verbose = true; }
|
|
769
|
-
else { console.error(`Unknown argument: ${a}`); process.exit(1); }
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (!from || !to) {
|
|
773
|
-
console.error('Usage: awm migrate --from <sqlite.db> --to <pglite-dir> [--dry-run] [--verbose]');
|
|
774
|
-
process.exit(1);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const { migrate, printStats } = await import('./cli/migrate.js');
|
|
778
|
-
try {
|
|
779
|
-
console.log(`Migrating ${from} → ${to}${dryRun ? ' (dry run)' : ''}`);
|
|
780
|
-
const stats = await migrate({ from, to, dryRun, verbose });
|
|
781
|
-
printStats(stats, dryRun);
|
|
782
|
-
} catch (err) {
|
|
783
|
-
console.error(`Migration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
784
|
-
process.exit(1);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// ─── ONBOARD ──────────────────────────────────────
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* Seed the onboarding skill as a canonical memory (idempotent). This is what lets
|
|
792
|
-
* a cold store teach the host agent how to warm-start itself — the agent recalls
|
|
793
|
-
* the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
|
|
794
|
-
*/
|
|
795
|
-
async function seedOnboardSkill(dbPath: string, agentId: string): Promise<string> {
|
|
796
|
-
try {
|
|
797
|
-
const { store, close } = await openCliStore(dbPath);
|
|
798
|
-
try {
|
|
799
|
-
const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
|
|
800
|
-
if (existing) return 'Onboarding skill: already present';
|
|
801
|
-
await store.createEngram({
|
|
802
|
-
agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
|
|
803
|
-
tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
|
|
804
|
-
});
|
|
805
|
-
return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
|
|
806
|
-
} finally {
|
|
807
|
-
await close();
|
|
808
|
-
}
|
|
809
|
-
} catch (e: any) {
|
|
810
|
-
return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function onboardCmd() {
|
|
815
|
-
const docs: string[] = [];
|
|
816
|
-
let repo: string | undefined;
|
|
817
|
-
let project = '';
|
|
818
|
-
let agentId = '';
|
|
819
|
-
let purpose: string | undefined;
|
|
820
|
-
let outDir = resolve(process.cwd(), '.awm');
|
|
821
|
-
|
|
822
|
-
for (let i = 1; i < args.length; i++) {
|
|
823
|
-
const a = args[i];
|
|
824
|
-
if (a === '--repo' && args[i + 1]) repo = args[++i];
|
|
825
|
-
else if (a === '--project' && args[i + 1]) project = args[++i];
|
|
826
|
-
else if (a === '--agent' && args[i + 1]) agentId = args[++i];
|
|
827
|
-
else if (a === '--purpose' && args[i + 1]) purpose = args[++i];
|
|
828
|
-
else if (a === '--out' && args[i + 1]) outDir = resolve(args[++i]);
|
|
829
|
-
else if (!a.startsWith('--')) docs.push(a);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
|
|
833
|
-
if (docs.length === 0) docs.push(repo ?? process.cwd());
|
|
834
|
-
if (!project) project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
|
|
835
|
-
if (!agentId) agentId = project;
|
|
836
|
-
|
|
837
|
-
const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
|
|
838
|
-
console.log(`
|
|
839
|
-
AWM onboard — warm-start pack for "${project}"
|
|
840
|
-
|
|
841
|
-
Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
|
|
842
|
-
Extracted: ${count} candidate memories (agent: ${agentId})
|
|
843
|
-
|
|
844
|
-
Review: ${reviewPath}
|
|
845
|
-
Pack: ${packPath}
|
|
846
|
-
|
|
847
|
-
Next:
|
|
848
|
-
1. Edit the review file / pack as needed (delete noise, answer the interview questions).
|
|
849
|
-
2. Load it: awm import ${packPath} --db <path> --dedupe
|
|
850
|
-
(embeddings backfill on the first consolidation — recall is warm immediately after)
|
|
851
|
-
`.trimEnd());
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// ─── Dispatch ──────────────────────────────────────
|
|
855
|
-
|
|
856
|
-
switch (command) {
|
|
857
|
-
case 'setup':
|
|
858
|
-
await setup();
|
|
859
|
-
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;
|
|
860
877
|
case 'plugin':
|
|
861
878
|
pluginInfo();
|
|
862
879
|
break;
|
|
863
|
-
case 'doctor':
|
|
864
|
-
await doctor();
|
|
865
|
-
break;
|
|
866
|
-
case 'mcp':
|
|
867
|
-
mcp();
|
|
868
|
-
break;
|
|
869
|
-
case 'serve':
|
|
870
|
-
serve();
|
|
871
|
-
break;
|
|
872
|
-
case 'health':
|
|
873
|
-
health();
|
|
874
|
-
break;
|
|
875
|
-
case 'export':
|
|
876
|
-
exportMemories();
|
|
877
|
-
break;
|
|
878
|
-
case 'import':
|
|
879
|
-
importMemories();
|
|
880
|
-
break;
|
|
881
|
-
case 'merge':
|
|
882
|
-
mergeMemories();
|
|
883
|
-
break;
|
|
884
|
-
case 'migrate':
|
|
885
|
-
await migrateCmd();
|
|
886
|
-
break;
|
|
887
|
-
case 'onboard':
|
|
888
|
-
onboardCmd();
|
|
889
|
-
break;
|
|
890
|
-
case '--help':
|
|
891
|
-
case '-h':
|
|
892
|
-
case undefined:
|
|
893
|
-
printUsage();
|
|
894
|
-
break;
|
|
895
|
-
default:
|
|
896
|
-
console.error(`Unknown command: ${command}`);
|
|
897
|
-
printUsage();
|
|
898
|
-
process.exit(1);
|
|
899
|
-
}
|
|
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
|
+
}
|