@iamem/amem 0.1.0

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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/activity.d.ts +27 -0
  4. package/dist/activity.js +202 -0
  5. package/dist/api/routes.d.ts +31 -0
  6. package/dist/api/routes.js +1345 -0
  7. package/dist/attest.d.ts +52 -0
  8. package/dist/attest.js +192 -0
  9. package/dist/backup-schedule.d.ts +25 -0
  10. package/dist/backup-schedule.js +216 -0
  11. package/dist/capture.d.ts +34 -0
  12. package/dist/capture.js +257 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +1007 -0
  15. package/dist/context.d.ts +46 -0
  16. package/dist/context.js +334 -0
  17. package/dist/crypto.d.ts +39 -0
  18. package/dist/crypto.js +166 -0
  19. package/dist/db.d.ts +192 -0
  20. package/dist/db.js +666 -0
  21. package/dist/draft-quality.d.ts +19 -0
  22. package/dist/draft-quality.js +85 -0
  23. package/dist/embed.d.ts +76 -0
  24. package/dist/embed.js +331 -0
  25. package/dist/estimate.d.ts +32 -0
  26. package/dist/estimate.js +69 -0
  27. package/dist/freshness.d.ts +15 -0
  28. package/dist/freshness.js +93 -0
  29. package/dist/hook.d.ts +16 -0
  30. package/dist/hook.js +177 -0
  31. package/dist/hygiene-schedule.d.ts +28 -0
  32. package/dist/hygiene-schedule.js +221 -0
  33. package/dist/hygiene.d.ts +61 -0
  34. package/dist/hygiene.js +196 -0
  35. package/dist/install/claude.d.ts +6 -0
  36. package/dist/install/claude.js +69 -0
  37. package/dist/install/cursor.d.ts +7 -0
  38. package/dist/install/cursor.js +80 -0
  39. package/dist/install/hosts.d.ts +21 -0
  40. package/dist/install/hosts.js +186 -0
  41. package/dist/install/skills.d.ts +8 -0
  42. package/dist/install/skills.js +67 -0
  43. package/dist/it-pack.d.ts +20 -0
  44. package/dist/it-pack.js +84 -0
  45. package/dist/kinds.d.ts +18 -0
  46. package/dist/kinds.js +106 -0
  47. package/dist/license.d.ts +48 -0
  48. package/dist/license.js +172 -0
  49. package/dist/mcp.d.ts +40 -0
  50. package/dist/mcp.js +435 -0
  51. package/dist/paths.d.ts +11 -0
  52. package/dist/paths.js +55 -0
  53. package/dist/personal.d.ts +7 -0
  54. package/dist/personal.js +44 -0
  55. package/dist/platforms.d.ts +11 -0
  56. package/dist/platforms.js +32 -0
  57. package/dist/policy.d.ts +46 -0
  58. package/dist/policy.js +254 -0
  59. package/dist/prefs.d.ts +6 -0
  60. package/dist/prefs.js +11 -0
  61. package/dist/proposal.d.ts +90 -0
  62. package/dist/proposal.js +376 -0
  63. package/dist/publish.d.ts +28 -0
  64. package/dist/publish.js +57 -0
  65. package/dist/remember-contract.d.ts +23 -0
  66. package/dist/remember-contract.js +117 -0
  67. package/dist/repo-identity.d.ts +15 -0
  68. package/dist/repo-identity.js +82 -0
  69. package/dist/rules-sync.d.ts +7 -0
  70. package/dist/rules-sync.js +47 -0
  71. package/dist/savings-export.d.ts +49 -0
  72. package/dist/savings-export.js +141 -0
  73. package/dist/scan.d.ts +16 -0
  74. package/dist/scan.js +109 -0
  75. package/dist/search.d.ts +25 -0
  76. package/dist/search.js +150 -0
  77. package/dist/service.d.ts +20 -0
  78. package/dist/service.js +254 -0
  79. package/dist/shop.d.ts +9 -0
  80. package/dist/shop.js +15 -0
  81. package/dist/ui/server.d.ts +21 -0
  82. package/dist/ui/server.js +268 -0
  83. package/dist/vault.d.ts +25 -0
  84. package/dist/vault.js +42 -0
  85. package/dist/workspace-setup.d.ts +8 -0
  86. package/dist/workspace-setup.js +55 -0
  87. package/docs/agent-install-prompt.md +41 -0
  88. package/docs/backlog.md +59 -0
  89. package/docs/enterprise-endpoint.md +98 -0
  90. package/docs/license.md +54 -0
  91. package/docs/npm-release.md +38 -0
  92. package/docs/remember-contract.md +42 -0
  93. package/package.json +64 -0
  94. package/scripts/mdm-offboard.sh +14 -0
  95. package/skills/amem-bootstrap/SKILL.md +83 -0
  96. package/skills/amem-update-working-memory/SKILL.md +54 -0
  97. package/templates/cursor-rule.mdc +23 -0
  98. package/templates/mdm/co.amem.managed.plist +33 -0
  99. package/templates/policy.deny-default.toml +20 -0
  100. package/templates/policy.example.toml +22 -0
  101. package/ui-static/app.js +3966 -0
  102. package/ui-static/index.html +178 -0
  103. package/ui-static/orbit.js +389 -0
  104. package/ui-static/styles.css +2411 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1007 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { buildAttestReport, formatAttestHuman } from "./attest.js";
5
+ import { handleApi, logContextUsage } from "./api/routes.js";
6
+ import { closeDb, getRepoByCwd, getRepoByName, renameWorkspace, listClaims, listComponents, listFlows, requireRepo, setReportedOnLatest, setReportedTokensSaved, touchSession, upsertRepo, upsertSetupState, wipeAllRepos, wipeRepo, openDb, } from "./db.js";
7
+ import { handleHookPayload } from "./hook.js";
8
+ import { installClaude, claudeInstallHealth } from "./install/claude.js";
9
+ import { installCursor, cursorInstallHealth } from "./install/cursor.js";
10
+ import { installHost } from "./install/hosts.js";
11
+ import { amemHome, dbPath, tryEnsureDir } from "./paths.js";
12
+ import { assertExportAllowed, assertPlatformAllowed, assertRemoteAllowed, assertUiAllowed, loadPolicy, } from "./policy.js";
13
+ import { applyProposal, diffProposal, exportRepoMemory, formatProposalDiff, loadProposalFile, validateProposal, } from "./proposal.js";
14
+ import { detectRepoIdentity, parseWorkspaceSlug, workspaceIdentity } from "./repo-identity.js";
15
+ import { startUiServer, buildUiLandingUrl, openUiInBrowser, isAddrInUse, probeUiHealth, } from "./ui/server.js";
16
+ import { installLoginService, isServiceInstalled, isServiceSupported, uninstallLoginService } from "./service.js";
17
+ import { mcpClientConfig, runMcpServer } from "./mcp.js";
18
+ import { provisionWorkspace } from "./workspace-setup.js";
19
+ import { HOST_INSTALL_IDS } from "./platforms.js";
20
+ import { ensurePersonalWorkspace, PERSONAL_SLUG } from "./personal.js";
21
+ import { createBackup, defaultBackupDir, encryptedDbPath, isDbEncryptedAtRest, lockDatabase, resolvePassphrase, restoreBackup, unlockDatabase, } from "./crypto.js";
22
+ import { installBackupSchedule, isBackupScheduleInstalled, uninstallBackupSchedule, writeBackupHelperScript, backupSchedulePath, } from "./backup-schedule.js";
23
+ import { rememberContract } from "./remember-contract.js";
24
+ import { vaultStatus } from "./vault.js";
25
+ import { applyLicenseFile, clearLicense, generateLicenseKeys, licenseStatus, signLicense, } from "./license.js";
26
+ import { embedIndexIssues, embedStatus, reindexAllEmbeds, setEmbedBackend, } from "./embed.js";
27
+ import { acceptSafeCleanups, decayStaleClaims, hygieneReport, mergeDuplicate, runScheduledHygiene } from "./hygiene.js";
28
+ import { hygieneSchedulePath, installHygieneSchedule, isHygieneScheduleInstalled, uninstallHygieneSchedule, writeHygieneHelperScript, } from "./hygiene-schedule.js";
29
+ import { syncPinnedRules } from "./rules-sync.js";
30
+ import { buildSbom, writeItPack } from "./it-pack.js";
31
+ function usage() {
32
+ console.log(`amem — local personal agent memory
33
+
34
+ Usage:
35
+ amem setup [--personal] [--platform <host>]
36
+ amem init --platform cursor|claude|windsurf|continue|aider|zed
37
+ amem init --workspace <name> [--path <dir>] [--platform …]
38
+ amem init --personal
39
+ amem rename "<display name>" --workspace <slug>
40
+ amem status [--workspace <name>]
41
+ amem doctor [--attest] [--json]
42
+ amem context "<query>" [--workspace <name>] [--platform cursor|claude|luna]
43
+ amem remember "<text>" [--workspace <name>] [--kind session] [--anchor <path>]
44
+ amem recipe [--json]
45
+ amem propose validate <file.json>
46
+ amem propose diff <file.json>
47
+ amem propose apply <file.json>
48
+ amem export [--out <file.json>]
49
+ amem wipe --yes
50
+ amem wipe --all --yes
51
+ amem lock --passphrase <secret>
52
+ amem unlock --passphrase <secret>
53
+ amem backup [--out <dir>] [--passphrase <secret>] [--label <name>]
54
+ amem backup schedule [--out <dir>] [--hour <0-23>]
55
+ amem backup unschedule
56
+ amem restore --file <backup.db|backup.db.enc> [--passphrase <secret>]
57
+ amem hygiene [--days 90] [--decay] [--accept-safe] [--merge <keepId> <dropId>]
58
+ amem hygiene --scheduled
59
+ amem hygiene schedule [--hour <0-23>]
60
+ amem hygiene unschedule
61
+ amem rules sync
62
+ amem it-pack [--out <dir>]
63
+ amem doctor [--attest] [--sbom] [--json]
64
+ amem session touch --platform cursor|claude [--session-id <id>]
65
+ amem hook
66
+ amem usage report --saved <n> [--platform cursor|claude] [--event-id <id>]
67
+ amem usage export [--format json|md|pdf] [--days 30] [--scope current|all] [--out <file>]
68
+ amem ui [--port 7843] [--no-open]
69
+ amem service install|uninstall|status
70
+ amem mcp [--print-config] [--workspace <name>]
71
+ amem license status|apply|clear|issue|keys
72
+ amem embed status|use hash|use ngram|use external|reindex
73
+
74
+ Install:
75
+ npx @iamem/amem setup
76
+ npm i -g @iamem/amem && amem setup
77
+ # or from a clone: npm install && npm link && amem setup
78
+
79
+ Privacy:
80
+ All memory stays in ${amemHome()} on this machine.
81
+ Nothing is uploaded or written into the product git history.
82
+
83
+ Enterprise:
84
+ Policy file: /etc/amem/policy.toml (system) or ~/.amem/policy.toml (user)
85
+ Override: AMEM_POLICY_PATH=/path/to/policy.toml
86
+ Attest: amem doctor --attest
87
+ `);
88
+ process.exit(1);
89
+ }
90
+ function parseArgs(argv) {
91
+ const args = argv.slice(2);
92
+ const flags = new Map();
93
+ const positional = [];
94
+ for (let i = 0; i < args.length; i++) {
95
+ const a = args[i];
96
+ if (a.startsWith("--")) {
97
+ const key = a.slice(2);
98
+ const next = args[i + 1];
99
+ if (!next || next.startsWith("--")) {
100
+ flags.set(key, true);
101
+ }
102
+ else {
103
+ flags.set(key, next);
104
+ i += 1;
105
+ }
106
+ }
107
+ else {
108
+ positional.push(a);
109
+ }
110
+ }
111
+ return { positional, flags };
112
+ }
113
+ function flagString(flags, name) {
114
+ const v = flags.get(name);
115
+ return typeof v === "string" ? v : undefined;
116
+ }
117
+ function resolveBinding(flags) {
118
+ const workspace = flagString(flags, "workspace") || process.env.AMEM_WORKSPACE;
119
+ if (workspace) {
120
+ const repo = getRepoByName(workspace);
121
+ if (!repo) {
122
+ throw new Error(`No workspace "${workspace}". Run: amem init --workspace ${workspace}`);
123
+ }
124
+ return repo;
125
+ }
126
+ return requireRepo();
127
+ }
128
+ async function readStdin() {
129
+ const chunks = [];
130
+ for await (const chunk of process.stdin) {
131
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
132
+ }
133
+ return Buffer.concat(chunks).toString("utf8");
134
+ }
135
+ async function main() {
136
+ const { positional, flags } = parseArgs(process.argv);
137
+ const cmd = positional[0];
138
+ if (!cmd) {
139
+ console.log("amem — local personal agent memory\n");
140
+ console.log("Quick start: amem setup");
141
+ console.log("Help: amem help\n");
142
+ console.log(`Home: ${amemHome()}`);
143
+ process.exit(0);
144
+ }
145
+ try {
146
+ switch (cmd) {
147
+ case "setup": {
148
+ const personal = Boolean(flags.get("personal")) || !flagString(flags, "platform");
149
+ if (personal || flags.get("personal")) {
150
+ const repo = ensurePersonalWorkspace(flagString(flags, "platform") ?? "app");
151
+ console.log(`Personal prefs workspace ready: ${repo.repo_name} (${repo.id})`);
152
+ console.log(` remember: amem remember "I prefer …" --workspace ${PERSONAL_SLUG}`);
153
+ }
154
+ const platform = flagString(flags, "platform");
155
+ if (platform && HOST_INSTALL_IDS.has(platform)) {
156
+ const result = installHost(platform, {
157
+ repoRoot: process.cwd(),
158
+ workspace: PERSONAL_SLUG,
159
+ });
160
+ console.log(`Installed host hints for ${result.host}:`);
161
+ for (const p of result.paths)
162
+ console.log(` ${p}`);
163
+ for (const n of result.notes)
164
+ console.log(` note: ${n}`);
165
+ }
166
+ else if (platform === "cursor") {
167
+ const identity = detectRepoIdentity();
168
+ const info = installCursor(identity.rootPath);
169
+ console.log(`Cursor install under ${identity.rootPath}`);
170
+ for (const s of info.skills)
171
+ console.log(` skill: ${s}`);
172
+ }
173
+ else if (platform === "claude") {
174
+ const info = installClaude(detectRepoIdentity().rootPath);
175
+ console.log("Claude Code hooks/skills installed");
176
+ for (const s of info.skills)
177
+ console.log(` skill: ${s}`);
178
+ }
179
+ console.log(`Memory DB: ${dbPath()}`);
180
+ console.log("Next: amem ui or amem context \"What should I know?\"");
181
+ console.log("Host recipe (any MCP client): amem recipe");
182
+ console.log("Install: npx @iamem/amem setup (or npm i -g @iamem/amem)");
183
+ break;
184
+ }
185
+ case "init": {
186
+ if (flags.get("personal")) {
187
+ const repo = ensurePersonalWorkspace(flagString(flags, "platform") ?? "app");
188
+ upsertSetupState(repo.id, [repo.platform ?? "app"], true);
189
+ console.log(`Personal prefs workspace ${repo.repo_name} (${repo.id})`);
190
+ console.log(`Memory DB: ${dbPath()}`);
191
+ console.log(`Root: ${repo.root_path}`);
192
+ console.log(`Use: amem remember "…" --workspace ${PERSONAL_SLUG}`);
193
+ break;
194
+ }
195
+ const workspace = flagString(flags, "workspace");
196
+ if (workspace) {
197
+ const platform = flagString(flags, "platform") ?? "app";
198
+ const slugPath = join(amemHome(), "workspaces", workspace.toLowerCase());
199
+ const root = resolve(flagString(flags, "path") ?? slugPath);
200
+ tryEnsureDir(root);
201
+ const identity = workspaceIdentity(workspace, root);
202
+ const repo = upsertRepo(identity, platform);
203
+ upsertSetupState(repo.id, [platform], true);
204
+ if (platform === "cursor")
205
+ installCursor(identity.rootPath);
206
+ else if (platform === "claude")
207
+ installClaude(identity.rootPath);
208
+ else if (HOST_INSTALL_IDS.has(platform)) {
209
+ installHost(platform, { repoRoot: identity.rootPath, workspace });
210
+ }
211
+ const ready = provisionWorkspace(repo, platform);
212
+ console.log(`Workspace ${repo.repo_name} (${repo.id})`);
213
+ console.log(`Memory DB: ${dbPath()}`);
214
+ console.log(`Root: ${repo.root_path}`);
215
+ console.log(`Platform: ${platform}`);
216
+ for (const line of ready.checks)
217
+ console.log(`Ready: ${line}`);
218
+ break;
219
+ }
220
+ const platform = flagString(flags, "platform");
221
+ if (!platform) {
222
+ throw new Error("amem init requires --platform cursor|claude|windsurf|continue|aider|zed, --workspace <name>, or --personal");
223
+ }
224
+ const policy = loadPolicy().policy;
225
+ assertPlatformAllowed(platform, policy);
226
+ const identity = detectRepoIdentity();
227
+ assertRemoteAllowed(identity.remoteUrl, policy);
228
+ const repo = upsertRepo(identity, platform);
229
+ let installInfo = { skills: [] };
230
+ if (platform === "cursor") {
231
+ installInfo = installCursor(identity.rootPath);
232
+ }
233
+ else if (platform === "claude") {
234
+ installInfo = installClaude(identity.rootPath);
235
+ }
236
+ else if (HOST_INSTALL_IDS.has(platform)) {
237
+ installInfo = installHost(platform, {
238
+ repoRoot: identity.rootPath,
239
+ workspace: identity.repoName,
240
+ });
241
+ }
242
+ else {
243
+ throw new Error(`Unknown platform "${platform}". Use cursor|claude|windsurf|continue|aider|zed`);
244
+ }
245
+ upsertSetupState(repo.id, [platform], true);
246
+ console.log(`Bound repo ${repo.repo_name} (${repo.repo_key})`);
247
+ console.log(`Memory DB: ${dbPath()}`);
248
+ console.log(`Platform: ${platform}`);
249
+ if ("rulePath" in installInfo && installInfo.rulePath) {
250
+ console.log(`Cursor rule: ${installInfo.rulePath}`);
251
+ }
252
+ if ("hooksPath" in installInfo && installInfo.hooksPath) {
253
+ console.log(`Cursor hooks: ${installInfo.hooksPath}`);
254
+ }
255
+ if ("settingsPath" in installInfo && installInfo.settingsPath) {
256
+ console.log(`Claude settings: ${installInfo.settingsPath}`);
257
+ }
258
+ if (installInfo.skills) {
259
+ for (const s of installInfo.skills)
260
+ console.log(`Skill: ${s}`);
261
+ }
262
+ if (installInfo.paths) {
263
+ for (const p of installInfo.paths)
264
+ console.log(`Config: ${p}`);
265
+ }
266
+ if (installInfo.notes) {
267
+ for (const n of installInfo.notes)
268
+ console.log(`Note: ${n}`);
269
+ }
270
+ console.log('Next: amem ui or amem context "What should I know?"');
271
+ break;
272
+ }
273
+ case "status": {
274
+ const workspaceFlag = flagString(flags, "workspace") || process.env.AMEM_WORKSPACE;
275
+ const identity = workspaceFlag
276
+ ? (() => {
277
+ const bound = getRepoByName(workspaceFlag);
278
+ if (!bound)
279
+ throw new Error(`No workspace "${workspaceFlag}"`);
280
+ return { rootPath: bound.root_path, repoKey: bound.repo_key, remoteUrl: bound.remote_url, repoName: bound.repo_name };
281
+ })()
282
+ : detectRepoIdentity();
283
+ const repo = workspaceFlag ? getRepoByName(workspaceFlag) : getRepoByCwd();
284
+ const policy = loadPolicy().policy;
285
+ console.log(`cwd root: ${identity.rootPath}`);
286
+ console.log(`repo key: ${identity.repoKey}`);
287
+ console.log(`remote: ${identity.remoteUrl ?? "(none)"}`);
288
+ console.log(`amem home:${amemHome()}`);
289
+ console.log(`db: ${dbPath()}`);
290
+ console.log(`encrypted:${isDbEncryptedAtRest() ? `yes (${encryptedDbPath()})` : existsSync(encryptedDbPath()) ? "unlocked (enc copy present)" : "no"}`);
291
+ console.log(`export: ${policy.allow_export ? "allowed" : "blocked by policy"}`);
292
+ console.log(`ui: ${policy.ui_enabled ? `enabled (${policy.ui_bind})` : "disabled by policy"}`);
293
+ console.log(`auto-apply kinds: ${(policy.auto_apply_kinds ?? []).join(", ") || "(none)"}`);
294
+ const vault = vaultStatus();
295
+ console.log(`backup schedule: ${isBackupScheduleInstalled() ? backupSchedulePath() : "not installed"}`);
296
+ console.log(`last backup: ${vault.backup.last ? `${vault.backup.last.name} (${vault.backup.last.mtime})` : "none"}`);
297
+ const lic = licenseStatus();
298
+ console.log(`license: ${lic.tier} (${lic.kind}${lic.valid ? "" : ", invalid"})`);
299
+ const emb = embedStatus();
300
+ console.log(`embed: ${emb.backend} dim=${emb.dim}${emb.requested !== emb.backend ? ` (requested ${emb.requested})` : ""}`);
301
+ if (!repo) {
302
+ console.log("binding: not initialized");
303
+ }
304
+ else {
305
+ const slug = parseWorkspaceSlug(repo.remote_url);
306
+ console.log(`kind: ${slug ? "workspace" : "git repo"}`);
307
+ console.log(`binding: ${repo.repo_name} (${repo.id})`);
308
+ if (slug && slug !== repo.repo_name) {
309
+ console.log(`mcp id: ${slug}`);
310
+ }
311
+ console.log(`platform: ${repo.platform ?? "(unset)"}`);
312
+ console.log(`claims: ${listClaims(repo.id).length}`);
313
+ console.log(`flows: ${listFlows(repo.id).length}`);
314
+ console.log(`components: ${listComponents(repo.id).length}`);
315
+ }
316
+ break;
317
+ }
318
+ case "rename": {
319
+ const workspace = flagString(flags, "workspace") || process.env.AMEM_WORKSPACE;
320
+ const name = positional.slice(1).join(" ").trim();
321
+ if (!workspace || !name) {
322
+ throw new Error('Usage: amem rename "<display name>" --workspace <slug>');
323
+ }
324
+ const repo = getRepoByName(workspace);
325
+ if (!repo)
326
+ throw new Error(`No workspace "${workspace}". Run: amem init --workspace ${workspace}`);
327
+ const updated = renameWorkspace(repo.id, name);
328
+ const slug = parseWorkspaceSlug(updated.remote_url);
329
+ console.log(`Renamed to "${updated.repo_name}"`);
330
+ if (slug)
331
+ console.log(`MCP id unchanged: ${slug} (claims stay on ${updated.id})`);
332
+ break;
333
+ }
334
+ case "doctor": {
335
+ if (flags.get("sbom")) {
336
+ const sbom = buildSbom();
337
+ const out = flagString(flags, "out");
338
+ const json = `${JSON.stringify(sbom, null, 2)}\n`;
339
+ if (out) {
340
+ writeFileSync(resolve(out), json, "utf8");
341
+ console.log(`Wrote SBOM to ${resolve(out)}`);
342
+ }
343
+ else {
344
+ process.stdout.write(json);
345
+ }
346
+ break;
347
+ }
348
+ if (flags.get("attest")) {
349
+ const report = buildAttestReport();
350
+ if (flags.get("json")) {
351
+ console.log(JSON.stringify(report, null, 2));
352
+ }
353
+ else {
354
+ console.log(formatAttestHuman(report));
355
+ console.log("");
356
+ console.log("--- json ---");
357
+ console.log(JSON.stringify(report, null, 2));
358
+ }
359
+ if (!report.ok)
360
+ process.exitCode = 1;
361
+ break;
362
+ }
363
+ const identity = detectRepoIdentity();
364
+ const repo = getRepoByCwd();
365
+ const issues = [];
366
+ const loaded = loadPolicy();
367
+ for (const src of loaded.sources) {
368
+ if (src.error)
369
+ issues.push(`Policy ${src.role}: ${src.error}`);
370
+ }
371
+ if (!repo)
372
+ issues.push("Repo not initialized — run amem init");
373
+ try {
374
+ issues.push(...embedIndexIssues(openDb()));
375
+ }
376
+ catch {
377
+ // A locked or absent vault is reported elsewhere; don't fail doctor on it.
378
+ }
379
+ const platform = repo?.platform ?? flagString(flags, "platform");
380
+ if (platform === "cursor") {
381
+ issues.push(...cursorInstallHealth(identity.rootPath));
382
+ }
383
+ else if (platform === "claude") {
384
+ issues.push(...claudeInstallHealth());
385
+ }
386
+ else if (repo && repo.platform !== "luna" && !(repo.remote_url || "").startsWith("amem://workspace/")) {
387
+ issues.push(...cursorInstallHealth(identity.rootPath));
388
+ issues.push(...claudeInstallHealth());
389
+ }
390
+ if (issues.length === 0) {
391
+ console.log("amem doctor: ok");
392
+ }
393
+ else {
394
+ console.log("amem doctor: issues found");
395
+ for (const issue of issues)
396
+ console.log(`- ${issue}`);
397
+ process.exitCode = 1;
398
+ }
399
+ break;
400
+ }
401
+ case "context": {
402
+ const query = positional.slice(1).join(" ").trim();
403
+ if (!query)
404
+ throw new Error('Usage: amem context "<query>"');
405
+ const repo = resolveBinding(flags);
406
+ const platform = flagString(flags, "platform") ??
407
+ repo.platform ??
408
+ "unknown";
409
+ if (platform === "cursor" || platform === "claude") {
410
+ assertPlatformAllowed(platform);
411
+ }
412
+ const sessionId = flagString(flags, "session-id") ??
413
+ process.env.AMEM_SESSION_ID ??
414
+ process.env.CURSOR_SESSION_ID ??
415
+ process.env.CLAUDE_SESSION_ID;
416
+ const { markdown, event } = logContextUsage({
417
+ repoId: repo.id,
418
+ platform,
419
+ sessionId,
420
+ query,
421
+ });
422
+ console.log(markdown);
423
+ console.log("");
424
+ console.log(`Usage logged: ${event.id} · estimated tokens saved ~${event.estimated_tokens_saved}`);
425
+ break;
426
+ }
427
+ case "remember": {
428
+ const text = positional.slice(1).join(" ").trim();
429
+ if (!text)
430
+ throw new Error('Usage: amem remember "<text>" [--workspace <name>] [--kind session] [--anchor <path>]');
431
+ const repo = resolveBinding(flags);
432
+ const anchor = flagString(flags, "anchor");
433
+ const result = handleApi({
434
+ method: "POST",
435
+ pathname: "/api/remember",
436
+ searchParams: new URLSearchParams(),
437
+ body: {
438
+ text,
439
+ repoId: repo.id,
440
+ kind: flagString(flags, "kind") ?? "session",
441
+ anchors: anchor ? [anchor] : undefined,
442
+ source: "cli",
443
+ },
444
+ cwd: process.cwd(),
445
+ });
446
+ if (result.status >= 400) {
447
+ throw new Error(result.body.error || "remember failed");
448
+ }
449
+ const remembered = result.body;
450
+ console.log(`Remembered ${remembered.claimId} in ${remembered.workspace || repo.repo_name}`);
451
+ break;
452
+ }
453
+ case "recipe": {
454
+ const contract = rememberContract();
455
+ if (flags.get("json")) {
456
+ console.log(JSON.stringify(contract, null, 2));
457
+ }
458
+ else {
459
+ console.log(contract.paste);
460
+ }
461
+ break;
462
+ }
463
+ case "propose": {
464
+ const sub = positional[1];
465
+ const file = positional[2];
466
+ if ((sub !== "validate" && sub !== "apply" && sub !== "diff") || !file) {
467
+ throw new Error("Usage: amem propose validate|diff|apply <file.json>");
468
+ }
469
+ const policy = loadPolicy().policy;
470
+ const proposal = loadProposalFile(resolve(file));
471
+ let existingClaims = undefined;
472
+ let repoId = null;
473
+ try {
474
+ const bound = resolveBinding(flags);
475
+ repoId = bound.id;
476
+ existingClaims = listClaims(bound.id, { includeSuperseded: true });
477
+ }
478
+ catch {
479
+ // validate/diff can run without a binding; conflict checks need one
480
+ }
481
+ const validated = validateProposal(proposal, policy, { existingClaims });
482
+ if (!validated.ok) {
483
+ console.error("Invalid proposal:");
484
+ for (const e of validated.errors)
485
+ console.error(`- ${e}`);
486
+ process.exitCode = 1;
487
+ break;
488
+ }
489
+ if (validated.warnings.length > 0) {
490
+ console.log("Warnings:");
491
+ for (const w of validated.warnings)
492
+ console.log(`- ${w}`);
493
+ }
494
+ if (repoId) {
495
+ console.log(formatProposalDiff(diffProposal(repoId, proposal)));
496
+ }
497
+ if (sub === "validate" || sub === "diff") {
498
+ console.log(sub === "diff" ? "Diff complete." : "Proposal is valid.");
499
+ break;
500
+ }
501
+ const repo = resolveBinding(flags);
502
+ assertRemoteAllowed(repo.remote_url, policy);
503
+ const result = applyProposal(repo.id, proposal, policy);
504
+ const supersededBit = result.superseded > 0 ? `, ${result.superseded} superseded` : "";
505
+ console.log(`Applied: ${result.claims} claims, ${result.flows} flows, ${result.components} components, ${result.edges} edges${supersededBit}`);
506
+ break;
507
+ }
508
+ case "export": {
509
+ assertExportAllowed();
510
+ const repo = resolveBinding(flags);
511
+ const data = {
512
+ exported_at: new Date().toISOString(),
513
+ repo: {
514
+ id: repo.id,
515
+ repo_key: repo.repo_key,
516
+ repo_name: repo.repo_name,
517
+ remote_url: repo.remote_url,
518
+ },
519
+ ...exportRepoMemory(repo.id),
520
+ };
521
+ const out = flagString(flags, "out");
522
+ const json = `${JSON.stringify(data, null, 2)}\n`;
523
+ if (out) {
524
+ const path = resolve(out);
525
+ mkdirSync(dirname(path), { recursive: true });
526
+ writeFileSync(path, json, "utf8");
527
+ console.log(`Wrote personal export to ${path}`);
528
+ console.log("Do not commit this file to a shared repository.");
529
+ }
530
+ else {
531
+ process.stdout.write(json);
532
+ }
533
+ break;
534
+ }
535
+ case "lock": {
536
+ const passphrase = resolvePassphrase(flagString(flags, "passphrase"));
537
+ closeDb();
538
+ const result = lockDatabase(passphrase);
539
+ console.log(`Locked database → ${result.encPath}`);
540
+ console.log("Plaintext graph.db removed. Unlock with: amem unlock --passphrase …");
541
+ break;
542
+ }
543
+ case "unlock": {
544
+ const passphrase = resolvePassphrase(flagString(flags, "passphrase"));
545
+ closeDb();
546
+ const result = unlockDatabase(passphrase);
547
+ console.log(`Unlocked database → ${result.dbPath}`);
548
+ console.log("Keep AMEM_PASSPHRASE set in your shell if you re-lock between sessions.");
549
+ break;
550
+ }
551
+ case "backup": {
552
+ const sub = positional[1];
553
+ if (sub === "schedule") {
554
+ const out = flagString(flags, "out");
555
+ const hourRaw = flagString(flags, "hour");
556
+ const hour = hourRaw !== undefined ? Number(hourRaw) : undefined;
557
+ if (hour !== undefined && (!Number.isFinite(hour) || hour < 0 || hour > 23)) {
558
+ throw new Error("--hour must be 0–23");
559
+ }
560
+ const result = installBackupSchedule({ outDir: out, hour });
561
+ const helper = writeBackupHelperScript(result.outDir);
562
+ console.log(`Scheduled local backup (${result.platform}): ${result.path}`);
563
+ console.log(`Backup dir: ${result.outDir}`);
564
+ console.log(`Helper script: ${helper}`);
565
+ console.log("Passphrase (if set via AMEM_PASSPHRASE) is used by `amem backup` when encrypting.");
566
+ break;
567
+ }
568
+ if (sub === "unschedule") {
569
+ const result = uninstallBackupSchedule();
570
+ console.log(`Removed backup schedule (${result.platform}): ${result.path}`);
571
+ break;
572
+ }
573
+ if (sub && sub !== "now") {
574
+ throw new Error("Usage: amem backup [--out <dir>] [--passphrase …] | amem backup schedule|unschedule");
575
+ }
576
+ closeDb();
577
+ let passphrase;
578
+ try {
579
+ passphrase = resolvePassphrase(flagString(flags, "passphrase"));
580
+ }
581
+ catch {
582
+ passphrase = undefined;
583
+ }
584
+ const result = createBackup({
585
+ outDir: flagString(flags, "out") || defaultBackupDir(),
586
+ passphrase,
587
+ label: flagString(flags, "label"),
588
+ });
589
+ console.log(`Backup written: ${result.path}`);
590
+ console.log(`Encrypted: ${result.encrypted ? "yes" : "no (pass --passphrase to encrypt)"}`);
591
+ break;
592
+ }
593
+ case "restore": {
594
+ const file = flagString(flags, "file") || positional[1];
595
+ if (!file)
596
+ throw new Error("Usage: amem restore --file <backup.db|backup.db.enc>");
597
+ closeDb();
598
+ let passphrase;
599
+ try {
600
+ passphrase = resolvePassphrase(flagString(flags, "passphrase"));
601
+ }
602
+ catch {
603
+ passphrase = undefined;
604
+ }
605
+ const result = restoreBackup({ file: resolve(file), passphrase });
606
+ openDb();
607
+ console.log(`Restored ${result.dbPath} from ${result.from}`);
608
+ if (result.safetyCopy)
609
+ console.log(`Previous DB saved at ${result.safetyCopy}`);
610
+ break;
611
+ }
612
+ case "hygiene": {
613
+ const sub = positional[1];
614
+ if (sub === "schedule") {
615
+ const hourRaw = flagString(flags, "hour");
616
+ const hour = hourRaw !== undefined ? Number(hourRaw) : undefined;
617
+ if (hour !== undefined && (!Number.isFinite(hour) || hour < 0 || hour > 23)) {
618
+ throw new Error("--hour must be 0–23");
619
+ }
620
+ const result = installHygieneSchedule({ hour });
621
+ const helper = writeHygieneHelperScript();
622
+ console.log(`Scheduled weekly hygiene (${result.platform}): ${result.path}`);
623
+ console.log(`Runs Sunday ~${result.hour}:20 local (Pro/IT required at run time)`);
624
+ console.log(`Helper script: ${helper}`);
625
+ break;
626
+ }
627
+ if (sub === "unschedule") {
628
+ const result = uninstallHygieneSchedule();
629
+ console.log(`Removed hygiene schedule (${result.platform}): ${result.path}`);
630
+ break;
631
+ }
632
+ if (flags.get("scheduled") || sub === "run") {
633
+ const days = Number(flagString(flags, "days") ?? "90");
634
+ const result = runScheduledHygiene(Number.isFinite(days) ? days : 90);
635
+ if (result.skipped) {
636
+ console.log(`Hygiene schedule skipped: ${result.reason}`);
637
+ break;
638
+ }
639
+ for (const row of result.repos) {
640
+ if (row.error)
641
+ console.log(`${row.name}: error — ${row.error}`);
642
+ else
643
+ console.log(`${row.name}: decayed ${row.decayed}, merged ${row.merged}`);
644
+ }
645
+ break;
646
+ }
647
+ const repo = resolveBinding(flags);
648
+ const days = Number(flagString(flags, "days") ?? "90");
649
+ const keep = flagString(flags, "merge");
650
+ if (keep) {
651
+ const drop = positional[2] || positional[1];
652
+ if (!drop)
653
+ throw new Error("Usage: amem hygiene --merge <keepId> <dropId>");
654
+ const merged = mergeDuplicate(repo.id, keep, drop);
655
+ console.log(`Merged ${merged.dropId} into ${merged.keepId}`);
656
+ break;
657
+ }
658
+ if (flags.get("accept-safe")) {
659
+ const result = acceptSafeCleanups(repo.id, Number.isFinite(days) ? days : 90);
660
+ console.log(`Decayed ${result.decayed.length}; merged ${result.merged.length}`);
661
+ break;
662
+ }
663
+ const report = hygieneReport(repo.id, Number.isFinite(days) ? days : 90);
664
+ if (flags.get("decay")) {
665
+ const result = decayStaleClaims(repo.id, Number.isFinite(days) ? days : 90);
666
+ console.log(`Decayed ${result.decayed.length} unused facts`);
667
+ break;
668
+ }
669
+ if (flags.get("json"))
670
+ console.log(JSON.stringify(report, null, 2));
671
+ else {
672
+ console.log(`active: ${report.active}`);
673
+ console.log(`stale (unused ${Number.isFinite(days) ? days : 90}d): ${report.stale.length}`);
674
+ console.log(`duplicates: ${report.duplicates.length}`);
675
+ console.log(`pending drafts: ${report.pendingDrafts}`);
676
+ console.log(`schedule: ${isHygieneScheduleInstalled() ? hygieneSchedulePath() : "not installed"}`);
677
+ for (const d of report.duplicates.slice(0, 8)) {
678
+ console.log(` merge ${d.dropId} → ${d.keepId} (${Math.round(d.similarity * 100)}%)`);
679
+ }
680
+ }
681
+ break;
682
+ }
683
+ case "rules": {
684
+ if (positional[1] !== "sync")
685
+ throw new Error("Usage: amem rules sync");
686
+ const repo = resolveBinding(flags);
687
+ const result = syncPinnedRules(repo);
688
+ console.log(`Wrote ${result.pinned} pinned facts → ${result.path}`);
689
+ console.log("Keep this file out of shared git if it contains personal notes.");
690
+ break;
691
+ }
692
+ case "it-pack": {
693
+ const out = resolve(flagString(flags, "out") || join(amemHome(), "it-pack"));
694
+ const result = writeItPack(out);
695
+ console.log(`IT pack written to ${result.dir}`);
696
+ for (const f of result.files)
697
+ console.log(` ${f}`);
698
+ break;
699
+ }
700
+ case "wipe": {
701
+ if (!flags.get("yes")) {
702
+ throw new Error("Refusing to wipe without --yes");
703
+ }
704
+ if (flags.get("all")) {
705
+ const count = wipeAllRepos();
706
+ closeDb();
707
+ // Remove local memory home for IT offboarding (db + sessions + user policy).
708
+ rmSync(amemHome(), { recursive: true, force: true });
709
+ console.log(`Wiped all local amem data (${count} repos) under ${amemHome()}`);
710
+ break;
711
+ }
712
+ const repo = resolveBinding(flags);
713
+ wipeRepo(repo.id);
714
+ console.log(`Wiped local memory for ${repo.repo_name}`);
715
+ break;
716
+ }
717
+ case "session": {
718
+ const sub = positional[1];
719
+ if (sub !== "touch")
720
+ throw new Error("Usage: amem session touch --platform <p>");
721
+ const platform = flagString(flags, "platform");
722
+ if (platform !== "cursor" && platform !== "claude") {
723
+ throw new Error("--platform cursor|claude required");
724
+ }
725
+ assertPlatformAllowed(platform);
726
+ const repo = getRepoByCwd();
727
+ if (!repo) {
728
+ process.exit(0);
729
+ }
730
+ const sessionId = flagString(flags, "session-id") ??
731
+ process.env.AMEM_SESSION_ID ??
732
+ process.env.CURSOR_SESSION_ID ??
733
+ process.env.CLAUDE_SESSION_ID ??
734
+ "unknown";
735
+ touchSession(platform, sessionId, repo.id);
736
+ break;
737
+ }
738
+ case "hook": {
739
+ try {
740
+ const result = handleHookPayload(await readStdin());
741
+ process.stdout.write(`${JSON.stringify(result)}\n`);
742
+ }
743
+ catch {
744
+ process.stdout.write(`${JSON.stringify({ continue: true })}\n`);
745
+ }
746
+ break;
747
+ }
748
+ case "usage": {
749
+ const sub = positional[1];
750
+ if (sub === "export") {
751
+ const format = flagString(flags, "format") ?? "json";
752
+ const days = flagString(flags, "days") ?? "30";
753
+ const scope = flagString(flags, "scope") ?? "current";
754
+ const repo = scope === "all" ? null : resolveBinding(flags);
755
+ const result = handleApi({
756
+ method: "GET",
757
+ pathname: "/api/usage/export",
758
+ searchParams: new URLSearchParams({
759
+ format,
760
+ days,
761
+ scope,
762
+ ...(repo ? { repo: repo.id } : {}),
763
+ }),
764
+ body: null,
765
+ cwd: process.cwd(),
766
+ });
767
+ if (result.status >= 400) {
768
+ throw new Error(result.body.error || "export failed");
769
+ }
770
+ const payload = result.body;
771
+ const out = flagString(flags, "out") ||
772
+ join(process.cwd(), payload.filename || `amem-savings.${format}`);
773
+ if (payload.contentBase64) {
774
+ writeFileSync(out, Buffer.from(payload.contentBase64, "base64"));
775
+ }
776
+ else if (payload.markdown) {
777
+ writeFileSync(out, payload.markdown, "utf8");
778
+ }
779
+ else {
780
+ writeFileSync(out, JSON.stringify(payload.report ?? payload, null, 2), "utf8");
781
+ }
782
+ console.log(`Wrote savings export to ${out}`);
783
+ console.log("Proxy only — not a Cursor or model bill.");
784
+ break;
785
+ }
786
+ if (sub !== "report") {
787
+ throw new Error("Usage: amem usage report --saved <n> | amem usage export [--format json|md|pdf]");
788
+ }
789
+ const savedRaw = flagString(flags, "saved");
790
+ if (savedRaw === undefined)
791
+ throw new Error("--saved <n> required");
792
+ const saved = Number(savedRaw);
793
+ if (!Number.isFinite(saved) || saved < 0) {
794
+ throw new Error("--saved must be a non-negative number");
795
+ }
796
+ const eventId = flagString(flags, "event-id");
797
+ if (eventId) {
798
+ const event = setReportedTokensSaved(eventId, saved);
799
+ console.log(`Updated ${event.id} reported_tokens_saved=${saved}`);
800
+ break;
801
+ }
802
+ const repo = resolveBinding(flags);
803
+ const platform = flagString(flags, "platform") ?? repo.platform ?? "unknown";
804
+ const event = setReportedOnLatest(repo.id, platform, saved);
805
+ console.log(`Updated ${event.id} reported_tokens_saved=${saved}`);
806
+ break;
807
+ }
808
+ case "ui": {
809
+ assertUiAllowed();
810
+ const policy = loadPolicy().policy;
811
+ const port = Number(flagString(flags, "port") ?? "7843");
812
+ const openBrowser = !flags.get("no-open");
813
+ const landing = buildUiLandingUrl(port, process.cwd());
814
+ try {
815
+ const server = await startUiServer({
816
+ port,
817
+ cwd: process.cwd(),
818
+ openBrowser,
819
+ host: policy.ui_bind,
820
+ landingUrl: landing,
821
+ });
822
+ console.log(`amem ui at ${server.url} (localhost only)`);
823
+ console.log(`focused: ${process.cwd()}`);
824
+ console.log("Press Ctrl+C to stop.");
825
+ await new Promise(() => {
826
+ // run until killed
827
+ });
828
+ }
829
+ catch (error) {
830
+ if (!isAddrInUse(error))
831
+ throw error;
832
+ const probe = await probeUiHealth(port);
833
+ if (!probe.hasVault) {
834
+ console.error(`Port ${port} is already serving an older amem without lock/backup APIs.`);
835
+ console.error("Stop that process, then run amem ui again:");
836
+ console.error(` lsof -nP -iTCP:${port} -sTCP:LISTEN`);
837
+ closeDb();
838
+ process.exitCode = 1;
839
+ break;
840
+ }
841
+ if (openBrowser)
842
+ openUiInBrowser(landing);
843
+ console.log(`amem ui already running — opened setup:`);
844
+ console.log(landing);
845
+ closeDb();
846
+ }
847
+ break;
848
+ }
849
+ case "service": {
850
+ const sub = positional[1];
851
+ if (sub === "status") {
852
+ console.log(`platform: ${process.platform}`);
853
+ console.log(`supported: ${isServiceSupported() ? "yes" : "no"}`);
854
+ console.log(`login item: ${isServiceInstalled() ? "installed" : "not installed"}`);
855
+ break;
856
+ }
857
+ if (sub === "install") {
858
+ const result = installLoginService();
859
+ console.log(`Installed login item (${result.platform}): ${result.path}`);
860
+ console.log("amem ui will start on login (localhost only).");
861
+ break;
862
+ }
863
+ if (sub === "uninstall") {
864
+ const result = uninstallLoginService();
865
+ console.log(`Removed login item (${result.platform}): ${result.path}`);
866
+ break;
867
+ }
868
+ throw new Error("Usage: amem service install|uninstall|status");
869
+ }
870
+ case "mcp": {
871
+ if (flags.get("print-config")) {
872
+ const ws = flagString(flags, "workspace") || process.env.AMEM_WORKSPACE || "my-app";
873
+ console.log(JSON.stringify(mcpClientConfig(ws), null, 2));
874
+ break;
875
+ }
876
+ await runMcpServer();
877
+ break;
878
+ }
879
+ case "license": {
880
+ const sub = positional[1];
881
+ if (!sub || sub === "status") {
882
+ const status = licenseStatus();
883
+ if (flags.get("json"))
884
+ console.log(JSON.stringify(status, null, 2));
885
+ else {
886
+ console.log(`tier: ${status.tier} (${status.kind}${status.valid ? "" : ", invalid"})`);
887
+ console.log(`features: ${status.features.join(", ") || "(none)"}`);
888
+ console.log(`path: ${status.path}`);
889
+ if (status.issues.length) {
890
+ for (const issue of status.issues)
891
+ console.log(`issue: ${issue}`);
892
+ }
893
+ }
894
+ break;
895
+ }
896
+ if (sub === "apply") {
897
+ const file = flagString(flags, "file") || positional[2];
898
+ if (!file)
899
+ throw new Error("Usage: amem license apply --file <license.json>");
900
+ const status = applyLicenseFile(file);
901
+ console.log(`Applied ${status.kind} license · tier ${status.tier}`);
902
+ break;
903
+ }
904
+ if (sub === "activate") {
905
+ throw new Error("Self-activate is disabled. Buy Pro/IT at https://getamem.com then: amem license apply --file <amem-license.json>");
906
+ }
907
+ if (sub === "clear") {
908
+ clearLicense();
909
+ console.log("License cleared · tier free");
910
+ break;
911
+ }
912
+ if (sub === "issue") {
913
+ const priv = process.env.AMEM_LICENSE_PRIVKEY;
914
+ if (!priv)
915
+ throw new Error("Set AMEM_LICENSE_PRIVKEY to issue a signed license");
916
+ const tier = flagString(flags, "tier");
917
+ if (tier !== "pro" && tier !== "it" && tier !== "free") {
918
+ throw new Error("--tier must be free, pro, or it");
919
+ }
920
+ const issued = signLicense(priv, {
921
+ tier,
922
+ subject: flagString(flags, "subject"),
923
+ issued_at: new Date().toISOString(),
924
+ expires_at: flagString(flags, "expires"),
925
+ });
926
+ const out = flagString(flags, "out") || "amem-license.json";
927
+ writeFileSync(out, `${JSON.stringify(issued, null, 2)}\n`, "utf8");
928
+ console.log(`Wrote signed license to ${out}`);
929
+ break;
930
+ }
931
+ if (sub === "keys") {
932
+ const keys = generateLicenseKeys();
933
+ const dir = flagString(flags, "out-dir");
934
+ if (dir) {
935
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
936
+ writeFileSync(join(dir, "license.pub"), `${keys.publicKeyHex}\n`, { mode: 0o644 });
937
+ writeFileSync(join(dir, "license.priv"), `${keys.privateKeyHex}\n`, { mode: 0o600 });
938
+ console.log(`Wrote shop/.data-style keys under ${dir}`);
939
+ console.log(`public: ${keys.publicKeyHex}`);
940
+ console.log("Put that public hex in src/license.ts DEFAULT_LICENSE_PUBKEY_HEX if it differs.");
941
+ break;
942
+ }
943
+ console.log(`public: ${keys.publicKeyHex}`);
944
+ console.log(`private: ${keys.privateKeyHex}`);
945
+ break;
946
+ }
947
+ throw new Error("Usage: amem license status|apply|clear|issue|keys");
948
+ }
949
+ case "embed": {
950
+ const sub = positional[1];
951
+ if (!sub || sub === "status") {
952
+ const status = embedStatus();
953
+ if (flags.get("json"))
954
+ console.log(JSON.stringify(status, null, 2));
955
+ else {
956
+ console.log(`backend: ${status.backend} (requested ${status.requested})`);
957
+ console.log(`dim: ${status.dim}`);
958
+ console.log(`licensed: ${status.licensed}`);
959
+ }
960
+ break;
961
+ }
962
+ if (sub === "use") {
963
+ const backend = positional[2];
964
+ if (backend !== "hash" && backend !== "ngram" && backend !== "external") {
965
+ throw new Error("Usage: amem embed use hash|ngram|external --cmd <bin>");
966
+ }
967
+ const cmd = flagString(flags, "cmd") || flagString(flags, "command");
968
+ const dimRaw = flagString(flags, "dim");
969
+ const status = setEmbedBackend(backend, {
970
+ command: cmd,
971
+ args: flagString(flags, "args")?.split(/\s+/).filter(Boolean),
972
+ dim: dimRaw ? Number(dimRaw) : undefined,
973
+ });
974
+ console.log(`Embed backend ${status.backend} dim=${status.dim}`);
975
+ if (status.command)
976
+ console.log(`command: ${status.command} ${status.args.join(" ")}`);
977
+ console.log("Reindex with: amem embed reindex");
978
+ break;
979
+ }
980
+ if (sub === "reindex") {
981
+ const result = reindexAllEmbeds(openDb());
982
+ console.log(`Reindexed ${result.claims} claims across ${result.repos} repos (${embedStatus().backend})`);
983
+ break;
984
+ }
985
+ throw new Error("Usage: amem embed status|use hash|use ngram|reindex");
986
+ }
987
+ case "help":
988
+ case "--help":
989
+ case "-h":
990
+ usage();
991
+ break;
992
+ default:
993
+ console.error(`Unknown command: ${cmd}`);
994
+ usage();
995
+ }
996
+ }
997
+ finally {
998
+ if (cmd !== "ui" && cmd !== "mcp")
999
+ closeDb();
1000
+ }
1001
+ }
1002
+ main().catch((err) => {
1003
+ const message = err instanceof Error ? err.message : String(err);
1004
+ console.error(message);
1005
+ closeDb();
1006
+ process.exit(1);
1007
+ });