@agentmemory/agentmemory 0.9.14 → 0.9.15

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/dist/cli.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, spawn, spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
4
  import { delimiter, dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { homedir, platform } from "node:os";
7
7
  import * as p from "@clack/prompts";
8
8
  import { createHash } from "node:crypto";
9
+ import { copyFile, mkdir } from "node:fs/promises";
9
10
 
10
11
  //#region src/state/schema.ts
11
12
  const KV = {
@@ -75,12 +76,741 @@ function jaccardSimilarity(a, b) {
75
76
  return intersection / (setA.size + setB.size - intersection);
76
77
  }
77
78
 
79
+ //#endregion
80
+ //#region src/cli/doctor-diagnostics.ts
81
+ /** Common placeholder values shipped in .env.example. */
82
+ const PLACEHOLDER_VALUES = new Set([
83
+ "",
84
+ "your-key-here",
85
+ "sk-ant-...",
86
+ "sk-...",
87
+ "changeme",
88
+ "todo",
89
+ "xxx"
90
+ ]);
91
+ const PROVIDER_KEY_NAMES = [
92
+ "ANTHROPIC_API_KEY",
93
+ "OPENAI_API_KEY",
94
+ "GEMINI_API_KEY",
95
+ "GOOGLE_API_KEY",
96
+ "OPENROUTER_API_KEY",
97
+ "MINIMAX_API_KEY"
98
+ ];
99
+ function parseEnvFile(content) {
100
+ const out = {};
101
+ for (const rawLine of content.split(/\r?\n/)) {
102
+ const line = rawLine.trim();
103
+ if (!line || line.startsWith("#")) continue;
104
+ const eq = line.indexOf("=");
105
+ if (eq < 0) continue;
106
+ const key = line.slice(0, eq).trim();
107
+ let value = line.slice(eq + 1).trim();
108
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
109
+ out[key] = value;
110
+ }
111
+ return out;
112
+ }
113
+ /** Returns the list of provider keys that look real (non-placeholder). */
114
+ function realProviderKeys(env) {
115
+ return PROVIDER_KEY_NAMES.filter((k) => {
116
+ const v = (env[k] ?? "").trim();
117
+ if (!v) return false;
118
+ if (PLACEHOLDER_VALUES.has(v.toLowerCase())) return false;
119
+ if (/^x+$/i.test(v.replace(/[-_]/g, ""))) return false;
120
+ return true;
121
+ });
122
+ }
123
+ /** Returns the list of provider key NAMES that exist but are placeholders. */
124
+ function placeholderProviderKeys(env) {
125
+ return PROVIDER_KEY_NAMES.filter((k) => {
126
+ const v = (env[k] ?? "").trim();
127
+ if (!v) return false;
128
+ if (PLACEHOLDER_VALUES.has(v.toLowerCase())) return true;
129
+ if (/^x+$/i.test(v.replace(/[-_]/g, ""))) return true;
130
+ return false;
131
+ });
132
+ }
133
+ function buildDiagnostics(effects) {
134
+ return [
135
+ {
136
+ id: "env-missing",
137
+ message: "~/.agentmemory/.env is missing.",
138
+ fixPreview: "Copy .env.example into ~/.agentmemory/.env (your keys file).",
139
+ moreInfo: "agentmemory reads provider API keys (Anthropic, OpenAI, Gemini, …) from ~/.agentmemory/.env. Without this file the daemon falls back to BM25-only search and no LLM-backed enrichment runs.",
140
+ check: async () => ({
141
+ ok: effects.envFileExists(),
142
+ detail: effects.envFileExists() ? void 0 : "no env file"
143
+ }),
144
+ fix: () => effects.runInit()
145
+ },
146
+ {
147
+ id: "no-llm-provider-key",
148
+ message: "No LLM provider API key found in ~/.agentmemory/.env.",
149
+ fixPreview: "Open ~/.agentmemory/.env in $EDITOR and paste your key, then re-check.",
150
+ moreInfo: "Set at least one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY. The daemon picks the first that resolves to a real (non-placeholder) value at startup.",
151
+ check: async () => {
152
+ if (!effects.envFileExists()) return {
153
+ ok: false,
154
+ detail: "env file missing (run env-missing fix first)"
155
+ };
156
+ const real = realProviderKeys(effects.readEnvFile());
157
+ return {
158
+ ok: real.length > 0,
159
+ detail: real.length > 0 ? `found: ${real.join(", ")}` : "no provider key set"
160
+ };
161
+ },
162
+ fix: (ctx) => effects.openEditor(ctx.envPath)
163
+ },
164
+ {
165
+ id: "engine-version-mismatch",
166
+ message: "iii binary on PATH doesn't match the version agentmemory pins to.",
167
+ fixPreview: "Re-run the iii installer for the pinned version and restart the engine.",
168
+ moreInfo: "agentmemory pins the iii engine to a specific release because newer engines use a different worker model. Running a mismatched binary surfaces as EPIPE reconnect loops and empty search results.",
169
+ check: async (ctx) => {
170
+ const bin = effects.findIiiBinary();
171
+ if (!bin) return {
172
+ ok: false,
173
+ detail: "iii not on PATH"
174
+ };
175
+ const v = effects.iiiBinaryVersion(bin);
176
+ if (!v) return {
177
+ ok: false,
178
+ detail: "iii on PATH but --version failed"
179
+ };
180
+ return {
181
+ ok: v === ctx.pinnedVersion,
182
+ detail: `${v} (pinned ${ctx.pinnedVersion})`
183
+ };
184
+ },
185
+ fix: async () => {
186
+ const r = await effects.runIiiInstaller();
187
+ if (!r.ok) return r;
188
+ await effects.runStop();
189
+ return effects.runStart();
190
+ }
191
+ },
192
+ {
193
+ id: "viewer-unreachable",
194
+ message: "Viewer port not reachable.",
195
+ fixPreview: "Stop the engine, restart it, and retry the viewer probe.",
196
+ moreInfo: "The viewer is served on REST port + 2 (default 3113). If it never came up the most common cause is port collision; a sibling PR ships auto-bump for this case. If that lands first this check just verifies; otherwise restart the engine to retry binding.",
197
+ check: async () => ({
198
+ ok: await effects.viewerReachable(),
199
+ detail: void 0
200
+ }),
201
+ fix: async () => {
202
+ const stopped = await effects.runStop();
203
+ if (!stopped.ok) return stopped;
204
+ return effects.runStart();
205
+ }
206
+ },
207
+ {
208
+ id: "stale-pidfile",
209
+ message: "Stale pidfile: pid recorded but the process is gone.",
210
+ fixPreview: "Clear ~/.agentmemory/iii.pid + engine-state.json, then restart.",
211
+ moreInfo: "When the engine crashes hard (kill -9, OOM, host reboot) the pidfile sticks around. agentmemory refuses to start a second engine on top of a stale pid, so this state must be cleared explicitly.",
212
+ check: async () => {
213
+ if (!effects.pidfileExists()) return {
214
+ ok: true,
215
+ detail: "no pidfile"
216
+ };
217
+ const alive = effects.pidfilePidIsAlive();
218
+ if (alive === null) return {
219
+ ok: true,
220
+ detail: "pidfile unreadable"
221
+ };
222
+ return {
223
+ ok: alive,
224
+ detail: alive ? "pid is alive" : "pid is gone"
225
+ };
226
+ },
227
+ fix: async () => {
228
+ effects.clearEnginePidAndState();
229
+ return effects.runStart();
230
+ }
231
+ },
232
+ {
233
+ id: "env-placeholder-keys",
234
+ message: "~/.agentmemory/.env contains placeholder/empty API keys.",
235
+ fixPreview: "Open ~/.agentmemory/.env in $EDITOR to paste real values.",
236
+ moreInfo: "Lines like ANTHROPIC_API_KEY=sk-ant-... or =your-key-here are treated as absent. The daemon will fall back to BM25-only search. Replace placeholders with real keys or comment the line out.",
237
+ check: async () => {
238
+ if (!effects.envFileExists()) return {
239
+ ok: true,
240
+ detail: "env file missing (handled by env-missing)"
241
+ };
242
+ const placeholders = placeholderProviderKeys(effects.readEnvFile());
243
+ return {
244
+ ok: placeholders.length === 0,
245
+ detail: placeholders.length === 0 ? void 0 : `placeholder: ${placeholders.join(", ")}`
246
+ };
247
+ },
248
+ fix: (ctx) => effects.openEditor(ctx.envPath)
249
+ },
250
+ {
251
+ id: "iii-on-path-not-local-bin",
252
+ message: "iii is on PATH but not in ~/.local/bin/iii (where we install).",
253
+ fixPreview: "Suggest re-installing the pinned version via the installer — won't touch your PATH.",
254
+ moreInfo: "agentmemory's installer writes to ~/.local/bin/iii. When a user-managed iii lives somewhere else (homebrew, cargo, $XDG_BIN) we don't auto-overwrite it. If you want our pinned build, run the installer; otherwise this is informational.",
255
+ manualOnly: true,
256
+ check: async () => {
257
+ const bin = effects.findIiiBinary();
258
+ if (!bin) return {
259
+ ok: true,
260
+ detail: "iii not on PATH (handled elsewhere)"
261
+ };
262
+ const localBin = effects.localBinIiiPath();
263
+ return {
264
+ ok: bin === localBin,
265
+ detail: bin === localBin ? void 0 : `iii at: ${bin}`
266
+ };
267
+ },
268
+ fix: async () => effects.runIiiInstaller().then((r) => ({
269
+ ok: r.ok,
270
+ message: r.message ?? "Installer wrote to ~/.local/bin/iii. Your PATH wasn't modified — adjust it yourself if needed."
271
+ }))
272
+ }
273
+ ];
274
+ }
275
+ /**
276
+ * Dry-run output: each failing check's fix preview, prefixed by the diagnostic
277
+ * message. Pure function so we can snapshot-test the format.
278
+ */
279
+ function dryRunPlan(ctx, results) {
280
+ const lines = [];
281
+ let n = 0;
282
+ for (const { diagnostic, status } of results) {
283
+ if (status.ok) continue;
284
+ n++;
285
+ lines.push(`${n}. [${diagnostic.id}] ${diagnostic.message}`);
286
+ lines.push(` would fix: ${diagnostic.fixPreview}`);
287
+ if (status.detail) lines.push(` detail: ${status.detail}`);
288
+ }
289
+ if (lines.length === 0) lines.push(`All checks passing for ${ctx.baseUrl} — no fixes to run.`);
290
+ return lines;
291
+ }
292
+
293
+ //#endregion
294
+ //#region src/cli/remove-plan.ts
295
+ function pidfilePath(home) {
296
+ return join(home, ".agentmemory", "iii.pid");
297
+ }
298
+ function enginePath(home) {
299
+ return join(home, ".agentmemory", "engine-state.json");
300
+ }
301
+ function envPath(home) {
302
+ return join(home, ".agentmemory", ".env");
303
+ }
304
+ function preferencesPath(home) {
305
+ return join(home, ".agentmemory", "preferences.json");
306
+ }
307
+ function backupsDir(home) {
308
+ return join(home, ".agentmemory", "backups");
309
+ }
310
+ function dataDir(home) {
311
+ return join(home, ".agentmemory", "data");
312
+ }
313
+ function localBinIii(home) {
314
+ return join(home, ".local", "bin", "iii");
315
+ }
316
+ function safeSize(path) {
317
+ try {
318
+ return statSync(path).size;
319
+ } catch {
320
+ return -1;
321
+ }
322
+ }
323
+ function pathExists(path) {
324
+ try {
325
+ return existsSync(path);
326
+ } catch {
327
+ return false;
328
+ }
329
+ }
330
+ /**
331
+ * Build the destruction plan for `agentmemory remove`.
332
+ *
333
+ * Plan items are returned regardless of whether `applicable` is true — the
334
+ * caller can decide whether to skip-and-log or hide entirely. This keeps
335
+ * the structure stable for tests.
336
+ */
337
+ function buildRemovePlan(ctx, options) {
338
+ const { home, pinnedVersion, localBinIiiVersion, connectManifest } = ctx;
339
+ const plan = [];
340
+ plan.push({
341
+ id: "stop-engine",
342
+ description: "Stop running iii-engine (if any) cleanly",
343
+ path: null,
344
+ alwaysAsk: false,
345
+ applicable: pathExists(pidfilePath(home)) || pathExists(enginePath(home)),
346
+ sizeBytes: -1
347
+ });
348
+ plan.push({
349
+ id: "pidfile",
350
+ description: "Delete pidfile",
351
+ path: pidfilePath(home),
352
+ alwaysAsk: false,
353
+ applicable: pathExists(pidfilePath(home)),
354
+ sizeBytes: safeSize(pidfilePath(home))
355
+ });
356
+ plan.push({
357
+ id: "engine-state",
358
+ description: "Delete engine-state.json",
359
+ path: enginePath(home),
360
+ alwaysAsk: false,
361
+ applicable: pathExists(enginePath(home)),
362
+ sizeBytes: safeSize(enginePath(home))
363
+ });
364
+ plan.push({
365
+ id: "env",
366
+ description: "Delete .env (your API keys) — will ask separately",
367
+ path: envPath(home),
368
+ alwaysAsk: true,
369
+ applicable: !options.keepData && pathExists(envPath(home)),
370
+ sizeBytes: safeSize(envPath(home))
371
+ });
372
+ plan.push({
373
+ id: "preferences",
374
+ description: "Delete preferences.json",
375
+ path: preferencesPath(home),
376
+ alwaysAsk: false,
377
+ applicable: !options.keepData && pathExists(preferencesPath(home)),
378
+ sizeBytes: safeSize(preferencesPath(home))
379
+ });
380
+ plan.push({
381
+ id: "backups",
382
+ description: "Delete backups/ directory (connect manifest + backups)",
383
+ path: backupsDir(home),
384
+ alwaysAsk: false,
385
+ applicable: !options.keepData && pathExists(backupsDir(home)),
386
+ sizeBytes: -1
387
+ });
388
+ if (connectManifest?.installed?.length) for (const entry of connectManifest.installed) plan.push({
389
+ id: `connect:${entry.target}`,
390
+ description: `Remove agent connection (${entry.agent ?? "unknown"})`,
391
+ path: entry.target,
392
+ alwaysAsk: false,
393
+ applicable: pathExists(entry.target),
394
+ sizeBytes: safeSize(entry.target)
395
+ });
396
+ const localIii = localBinIii(home);
397
+ if (pathExists(localIii)) {
398
+ const matches = localBinIiiVersion === pinnedVersion;
399
+ plan.push({
400
+ id: "local-bin-iii",
401
+ description: matches ? `Delete ~/.local/bin/iii (matches pinned v${pinnedVersion})` : `Delete ~/.local/bin/iii (version ${localBinIiiVersion ?? "unknown"} != pinned v${pinnedVersion}) — will ask`,
402
+ path: localIii,
403
+ alwaysAsk: !matches,
404
+ applicable: true,
405
+ sizeBytes: safeSize(localIii)
406
+ });
407
+ }
408
+ plan.push({
409
+ id: "data-dir",
410
+ description: "Delete memory data directory (~/.agentmemory/data/) — will ask separately",
411
+ path: dataDir(home),
412
+ alwaysAsk: true,
413
+ applicable: !options.keepData && pathExists(dataDir(home)),
414
+ sizeBytes: -1
415
+ });
416
+ return plan;
417
+ }
418
+ /** Format a plan for the user — one line per item. */
419
+ function formatPlan(plan) {
420
+ return plan.filter((p) => p.applicable).map((p, i) => {
421
+ const tag = p.alwaysAsk ? " [asks]" : "";
422
+ const sz = p.sizeBytes > 0 ? ` (${humanBytes(p.sizeBytes)})` : "";
423
+ return ` ${i + 1}. ${p.description}${tag}${sz}${p.path ? `\n ${p.path}` : ""}`;
424
+ }).join("\n");
425
+ }
426
+ function humanBytes(n) {
427
+ if (n < 1024) return `${n} B`;
428
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
429
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
430
+ }
431
+
432
+ //#endregion
433
+ //#region src/cli/splash.ts
434
+ const IS_COLOR_TTY = !!process.stdout.isTTY && !process.env["NO_COLOR"];
435
+ function accent(s) {
436
+ return IS_COLOR_TTY ? `\x1b[38;5;208m${s}\x1b[0m` : s;
437
+ }
438
+ function dim(s) {
439
+ return IS_COLOR_TTY ? `\x1b[2m${s}\x1b[22m` : s;
440
+ }
441
+ function bold(s) {
442
+ return IS_COLOR_TTY ? `\x1b[1m${s}\x1b[22m` : s;
443
+ }
444
+ function getTerminalWidth() {
445
+ const w = process.stdout.columns;
446
+ return typeof w === "number" && w > 0 ? w : 80;
447
+ }
448
+ const TAGLINE = "Persistent memory for AI coding agents";
449
+ function fullBanner(version) {
450
+ const lines = ["", ...[
451
+ " _ ",
452
+ " __ _ __ _ ___ _ _ | |_ _ __ ___ _ __ ___ ___ _ __ _ _ ",
453
+ " / _` |/ _` |/ _ \\ '_\\| __| ' \\/ -_) ' \\ _ \\ / _ \\| '__| | | | ",
454
+ "| (_| | (_| | __/ | || |_| | | \\___| | | | | | (_) | | | |_| | ",
455
+ " \\__,_|\\__, |\\___|_| \\__|_| |_| |_| |_| |_|\\___/|_| \\__, | ",
456
+ " |___/ |___/ "
457
+ ].map((line) => " " + accent(line))];
458
+ lines.push("");
459
+ lines.push(" " + bold(TAGLINE) + " " + dim(`v${version}`));
460
+ lines.push("");
461
+ return lines.join("\n");
462
+ }
463
+ function compactBanner(version) {
464
+ return [
465
+ "",
466
+ " " + bold(accent("agentmemory")),
467
+ " " + dim(`v${version} · ${TAGLINE}`),
468
+ ""
469
+ ].join("\n");
470
+ }
471
+ function minimalBanner(version) {
472
+ return `${accent("agentmemory")} ${dim(`v${version}`)}`;
473
+ }
474
+ function renderSplash(version) {
475
+ const width = getTerminalWidth();
476
+ let out;
477
+ if (width >= 120) out = fullBanner(version);
478
+ else if (width >= 80) out = compactBanner(version);
479
+ else out = minimalBanner(version);
480
+ process.stdout.write(out + "\n");
481
+ }
482
+
483
+ //#endregion
484
+ //#region src/cli/preferences.ts
485
+ const DEFAULTS = {
486
+ schemaVersion: 1,
487
+ lastAgent: null,
488
+ lastAgents: [],
489
+ lastProvider: null,
490
+ skipSplash: false,
491
+ skipNpxHint: false,
492
+ firstRunAt: null
493
+ };
494
+ function prefsDir() {
495
+ return join(homedir(), ".agentmemory");
496
+ }
497
+ function prefsPath() {
498
+ return join(prefsDir(), "preferences.json");
499
+ }
500
+ function readPrefs() {
501
+ try {
502
+ if (!existsSync(prefsPath())) return { ...DEFAULTS };
503
+ const raw = readFileSync(prefsPath(), "utf-8");
504
+ const parsed = JSON.parse(raw);
505
+ return {
506
+ ...DEFAULTS,
507
+ ...parsed,
508
+ schemaVersion: 1
509
+ };
510
+ } catch {
511
+ return { ...DEFAULTS };
512
+ }
513
+ }
514
+ function writePrefs(p) {
515
+ try {
516
+ mkdirSync(prefsDir(), { recursive: true });
517
+ const next = {
518
+ ...readPrefs(),
519
+ ...p,
520
+ schemaVersion: 1
521
+ };
522
+ const target = prefsPath();
523
+ const tmp = target + ".tmp";
524
+ const fd = openSync(tmp, "w", 384);
525
+ try {
526
+ writeSync(fd, JSON.stringify(next, null, 2) + "\n");
527
+ try {
528
+ fsyncSync(fd);
529
+ } catch {}
530
+ } finally {
531
+ closeSync(fd);
532
+ }
533
+ renameSync(tmp, target);
534
+ } catch {}
535
+ }
536
+ function resetPrefs() {
537
+ try {
538
+ unlinkSync(prefsPath());
539
+ } catch {}
540
+ }
541
+ function isFirstRun() {
542
+ if (!existsSync(prefsPath())) return true;
543
+ return readPrefs().firstRunAt === null;
544
+ }
545
+
546
+ //#endregion
547
+ //#region src/cli/onboarding.ts
548
+ const __dirname$1 = dirname(fileURLToPath(import.meta.url));
549
+ const NATIVE_AGENTS = [
550
+ {
551
+ value: "claude-code",
552
+ label: "Claude Code",
553
+ glyph: "⟁"
554
+ },
555
+ {
556
+ value: "codex",
557
+ label: "Codex",
558
+ glyph: "◎"
559
+ },
560
+ {
561
+ value: "openhuman",
562
+ label: "OpenHuman",
563
+ glyph: "◇"
564
+ },
565
+ {
566
+ value: "openclaw",
567
+ label: "OpenClaw",
568
+ glyph: "◇"
569
+ },
570
+ {
571
+ value: "hermes",
572
+ label: "Hermes",
573
+ glyph: "◇"
574
+ },
575
+ {
576
+ value: "pi",
577
+ label: "Pi",
578
+ glyph: "◇"
579
+ },
580
+ {
581
+ value: "cursor",
582
+ label: "Cursor",
583
+ glyph: "◫"
584
+ },
585
+ {
586
+ value: "gemini-cli",
587
+ label: "Gemini CLI",
588
+ glyph: "✦"
589
+ }
590
+ ];
591
+ const MCP_AGENTS = [
592
+ {
593
+ value: "opencode",
594
+ label: "OpenCode",
595
+ glyph: "⬡"
596
+ },
597
+ {
598
+ value: "cline",
599
+ label: "Cline",
600
+ glyph: "◇"
601
+ },
602
+ {
603
+ value: "goose",
604
+ label: "Goose",
605
+ glyph: "◇"
606
+ },
607
+ {
608
+ value: "kilo",
609
+ label: "Kilo",
610
+ glyph: "◇"
611
+ },
612
+ {
613
+ value: "aider",
614
+ label: "Aider",
615
+ glyph: "◇"
616
+ },
617
+ {
618
+ value: "claude-desktop",
619
+ label: "Claude Desktop",
620
+ glyph: "⟁"
621
+ },
622
+ {
623
+ value: "windsurf",
624
+ label: "Windsurf",
625
+ glyph: "◇"
626
+ },
627
+ {
628
+ value: "roo",
629
+ label: "Roo",
630
+ glyph: "◇"
631
+ }
632
+ ];
633
+ const PROVIDERS = [
634
+ {
635
+ value: "anthropic",
636
+ label: "Anthropic — claude",
637
+ envKey: "ANTHROPIC_API_KEY"
638
+ },
639
+ {
640
+ value: "openai",
641
+ label: "OpenAI — gpt",
642
+ envKey: "OPENAI_API_KEY"
643
+ },
644
+ {
645
+ value: "gemini",
646
+ label: "Google — gemini",
647
+ envKey: "GEMINI_API_KEY"
648
+ },
649
+ {
650
+ value: "openrouter",
651
+ label: "OpenRouter — multi-model",
652
+ envKey: "OPENROUTER_API_KEY"
653
+ },
654
+ {
655
+ value: "minimax",
656
+ label: "MiniMax — minimax-m1",
657
+ envKey: "MINIMAX_API_KEY"
658
+ },
659
+ {
660
+ value: "skip",
661
+ label: "Skip — BM25-only mode (no LLM key)",
662
+ envKey: null
663
+ }
664
+ ];
665
+ function buildAgentOptions() {
666
+ return [...NATIVE_AGENTS.map((a) => ({
667
+ value: a.value,
668
+ label: `${a.glyph} ${a.label}`,
669
+ hint: "native plugin"
670
+ })), ...MCP_AGENTS.map((a) => ({
671
+ value: a.value,
672
+ label: `${a.glyph} ${a.label}`,
673
+ hint: "MCP server"
674
+ }))];
675
+ }
676
+ function findEnvExample$1() {
677
+ const candidates = [
678
+ join(__dirname$1, "..", "..", ".env.example"),
679
+ join(__dirname$1, "..", ".env.example"),
680
+ join(__dirname$1, ".env.example"),
681
+ join(process.cwd(), ".env.example")
682
+ ];
683
+ for (const c of candidates) if (existsSync(c)) return c;
684
+ return null;
685
+ }
686
+ async function seedEnvFile(provider) {
687
+ const target = join(homedir(), ".agentmemory", ".env");
688
+ await mkdir(dirname(target), { recursive: true });
689
+ const template = findEnvExample$1();
690
+ if (template && !existsSync(target)) try {
691
+ await copyFile(template, target, constants.COPYFILE_EXCL);
692
+ } catch (err) {
693
+ if (err?.code !== "EEXIST") return null;
694
+ }
695
+ else if (!template && !existsSync(target)) {
696
+ const lines = [
697
+ "# agentmemory environment — uncomment what you need",
698
+ "# AGENTMEMORY_URL=http://localhost:3111",
699
+ ""
700
+ ];
701
+ const envKey = PROVIDERS.find((x) => x.value === provider)?.envKey;
702
+ if (envKey) lines.push(`# ${envKey}=`);
703
+ writeFileSync(target, lines.join("\n"), { mode: 384 });
704
+ }
705
+ return target;
706
+ }
707
+ async function runOnboarding() {
708
+ p.note([
709
+ "Welcome to agentmemory.",
710
+ "",
711
+ "Persistent memory for your AI coding agents. We'll pick which",
712
+ "agents to wire up and which provider (if any) handles compression",
713
+ "and consolidation. Either step can be changed later in ~/.agentmemory/.env."
714
+ ].join("\n"), "first-run setup");
715
+ const agentsPicked = await p.multiselect({
716
+ message: "Which agents will use agentmemory? (space to toggle, enter to confirm)",
717
+ options: buildAgentOptions(),
718
+ required: false,
719
+ initialValues: ["claude-code"]
720
+ });
721
+ if (p.isCancel(agentsPicked)) {
722
+ p.cancel("Setup cancelled. Re-run any time with: agentmemory --reset");
723
+ process.exit(0);
724
+ }
725
+ const providerPicked = await p.select({
726
+ message: "Which LLM provider should agentmemory use for compress/consolidate?",
727
+ options: PROVIDERS.map(({ value, label }) => ({
728
+ value,
729
+ label
730
+ })),
731
+ initialValue: "anthropic"
732
+ });
733
+ if (p.isCancel(providerPicked)) {
734
+ p.cancel("Setup cancelled. Re-run any time with: agentmemory --reset");
735
+ process.exit(0);
736
+ }
737
+ const provider = providerPicked === "skip" ? null : providerPicked;
738
+ const agents = agentsPicked ?? [];
739
+ const envPath = await seedEnvFile(provider);
740
+ writePrefs({
741
+ lastAgent: agents[0] ?? null,
742
+ lastAgents: agents,
743
+ lastProvider: provider,
744
+ skipSplash: true,
745
+ firstRunAt: (/* @__PURE__ */ new Date()).toISOString()
746
+ });
747
+ const lines = [`✓ Saved preferences to ${join(homedir(), ".agentmemory", "preferences.json")}`];
748
+ if (envPath) lines.push(`✓ Wrote ${envPath} (edit to add your API key)`);
749
+ else lines.push(`! Could not write ~/.agentmemory/.env — run \`agentmemory init\` after this completes.`);
750
+ if (provider) {
751
+ const envKey = PROVIDERS.find((x) => x.value === provider)?.envKey;
752
+ if (envKey) lines.push(` Uncomment ${envKey}= in that file to enable ${provider}.`);
753
+ } else lines.push(" No provider chosen — agentmemory will run in BM25-only mode.");
754
+ p.note(lines.join("\n"), "ready");
755
+ return {
756
+ agents,
757
+ provider
758
+ };
759
+ }
760
+
761
+ //#endregion
762
+ //#region src/logger.ts
763
+ function fmt(level, msg, fields) {
764
+ if (!fields || Object.keys(fields).length === 0) return `[agentmemory] ${level} ${msg}`;
765
+ try {
766
+ return `[agentmemory] ${level} ${msg} ${JSON.stringify(fields)}`;
767
+ } catch {
768
+ return `[agentmemory] ${level} ${msg}`;
769
+ }
770
+ }
771
+ function emit(level, msg, fields) {
772
+ try {
773
+ process.stderr.write(fmt(level, msg, fields) + "\n");
774
+ } catch {}
775
+ }
776
+ const logger = {
777
+ info(msg, fields) {
778
+ emit("info", msg, fields);
779
+ },
780
+ warn(msg, fields) {
781
+ emit("warn", msg, fields);
782
+ },
783
+ error(msg, fields) {
784
+ emit("error", msg, fields);
785
+ }
786
+ };
787
+ let bootVerbose = process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
788
+ const bootBuffer = [];
789
+ function setBootVerbose(enabled) {
790
+ bootVerbose = enabled;
791
+ }
792
+ function bootLog(msg) {
793
+ if (bootVerbose) {
794
+ try {
795
+ process.stderr.write(`[agentmemory] ${msg}\n`);
796
+ } catch {}
797
+ return;
798
+ }
799
+ if (bootBuffer.length < 500) bootBuffer.push(msg);
800
+ }
801
+
802
+ //#endregion
803
+ //#region src/version.ts
804
+ const VERSION = "0.9.15";
805
+
78
806
  //#endregion
79
807
  //#region src/cli.ts
80
808
  const __dirname = dirname(fileURLToPath(import.meta.url));
81
809
  const args = process.argv.slice(2);
82
810
  const IS_WINDOWS = platform() === "win32";
83
- const IS_VERBOSE = args.includes("--verbose") || args.includes("-v");
811
+ const IS_VERBOSE = args.includes("--verbose") || args.includes("-v") || process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
812
+ setBootVerbose(IS_VERBOSE);
813
+ const IS_RESET = args.includes("--reset");
84
814
  const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || "0.11.2";
85
815
  function iiiReleaseAsset() {
86
816
  const p = platform();
@@ -111,11 +841,22 @@ Usage: agentmemory [command] [options]
111
841
  Commands:
112
842
  (default) Start agentmemory worker
113
843
  init Copy bundled .env.example to ~/.agentmemory/.env if absent
844
+ connect [agent] Wire agentmemory into an installed agent (claude-code, codex,
845
+ cursor, gemini-cli, openclaw, hermes, pi, openhuman).
846
+ No arg = interactive picker. --all wires every detected agent.
847
+ --dry-run shows what would change. --force re-installs.
114
848
  status Show connection status, memory count, flags, and health
115
- doctor Run diagnostic checks (server, flags, graph, providers)
849
+ doctor Interactive diagnostic + fixer. [F]ix · [S]kip · [?]more · [Q]uit
850
+ --all: apply every fix without prompting (CI)
851
+ --dry-run: show what each fix would do, don't execute
852
+ remove Cleanly uninstall agentmemory (pidfile, state, .env, binaries).
853
+ --force: skip confirmations · --keep-data: keep memory data
116
854
  demo Seed sample sessions and show recall in action
117
855
  upgrade Upgrade local deps + iii runtime (best effort)
118
- stop Stop the running iii-engine started by this CLI
856
+ stop [--force] Stop the running iii-engine started by this CLI.
857
+ --force bypasses the Docker-heuristic guard and signals
858
+ whatever pidfile+lsof report on the REST port (use when
859
+ the engine was started natively but state file is missing).
119
860
  mcp Start standalone MCP server (no engine required)
120
861
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
121
862
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
@@ -123,7 +864,8 @@ Commands:
123
864
 
124
865
  Options:
125
866
  --help, -h Show this help
126
- --verbose, -v Show engine stderr and diagnostic info on startup
867
+ --verbose, -v Show engine stderr, boot log, and diagnostic info
868
+ --reset Wipe ~/.agentmemory/preferences.json and re-run onboarding
127
869
  --tools all|core Tool visibility (default: core = 7 tools)
128
870
  --no-engine Skip auto-starting iii-engine
129
871
  --port <N> Override REST port (default: 3111)
@@ -240,6 +982,16 @@ function iiiBinVersion(binPath) {
240
982
  return null;
241
983
  }
242
984
  }
985
+ let warnedVersionMismatch = false;
986
+ function warnIfEngineVersionMismatch(iiiBinPath) {
987
+ if (!iiiBinPath || warnedVersionMismatch) return;
988
+ const detected = iiiBinVersion(iiiBinPath);
989
+ if (!detected || detected === IIPINNED_VERSION) return;
990
+ warnedVersionMismatch = true;
991
+ const asset = iiiReleaseAsset();
992
+ const downloadHint = asset ? `curl -fsSL https://github.com/iii-hq/iii/releases/download/iii/v${IIPINNED_VERSION}/${asset} | tar -xz -C ~/.local/bin` : `download v${IIPINNED_VERSION} from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
993
+ p.log.warn(`iii-engine on PATH is v${detected} but agentmemory v0.9.14+ pins v${IIPINNED_VERSION}. Set AGENTMEMORY_III_VERSION=${detected} to silence, or downgrade with: \`${downloadHint}\``);
994
+ }
243
995
  function enginePidfilePath() {
244
996
  return join(homedir(), ".agentmemory", "iii.pid");
245
997
  }
@@ -300,6 +1052,45 @@ function discoverComposeFile() {
300
1052
  join(process.cwd(), "docker-compose.yml")
301
1053
  ].find((c) => existsSync(c)) ?? null;
302
1054
  }
1055
+ function isInvokedViaNpx() {
1056
+ if (process.env["npm_lifecycle_event"] === "npx") return true;
1057
+ if ((process.argv[1] ?? "").includes("_npx")) return true;
1058
+ const ua = process.env["npm_config_user_agent"] ?? "";
1059
+ if (ua.startsWith("npm/") || ua.includes(" npm/")) return true;
1060
+ return false;
1061
+ }
1062
+ function shouldSkipNpxHint() {
1063
+ try {
1064
+ const prefsPath = join(homedir(), ".agentmemory", "preferences.json");
1065
+ if (!existsSync(prefsPath)) return false;
1066
+ const raw = readFileSync(prefsPath, "utf-8");
1067
+ return JSON.parse(raw)?.skipNpxHint === true;
1068
+ } catch {
1069
+ return false;
1070
+ }
1071
+ }
1072
+ function maybeEmitNpxHint() {
1073
+ if (!isInvokedViaNpx()) return;
1074
+ if (shouldSkipNpxHint()) return;
1075
+ p.log.info("Tip: install globally for the bare `agentmemory` command:\n npm install -g @agentmemory/agentmemory");
1076
+ }
1077
+ function adoptRunningEngine() {
1078
+ try {
1079
+ const existingState = readEngineState();
1080
+ const existingPid = readEnginePidfile();
1081
+ if (existingState && existingPid) return;
1082
+ const enginePid = findEnginePidsByPort(getRestPort())[0];
1083
+ if (enginePid && !existingPid) writeEnginePidfile(enginePid);
1084
+ if (!existingState) writeEngineState({
1085
+ kind: "native",
1086
+ configPath: findIiiConfig() || "",
1087
+ attached: true
1088
+ });
1089
+ if (enginePid && !existingPid) p.log.info(`Attached to existing iii-engine (pid ${enginePid})`);
1090
+ } catch (err) {
1091
+ vlog(`adoptRunningEngine: ${err instanceof Error ? err.message : String(err)}`);
1092
+ }
1093
+ }
303
1094
  async function runIiiInstaller() {
304
1095
  const releaseUrl = iiiReleaseUrl();
305
1096
  const asset = iiiReleaseAsset();
@@ -389,6 +1180,7 @@ function spawnEngineBackground(bin, spawnArgs, label) {
389
1180
  return child;
390
1181
  }
391
1182
  function startIiiBin(iiiBin, configPath) {
1183
+ warnIfEngineVersionMismatch(iiiBin);
392
1184
  const s = p.spinner();
393
1185
  s.start(`Starting iii-engine: ${iiiBin}`);
394
1186
  writeEngineState({
@@ -550,16 +1342,37 @@ function installInstructions() {
550
1342
  function portInUseDiagnostic(port) {
551
1343
  return IS_WINDOWS ? ` netstat -ano | findstr :${port}` : ` lsof -i :${port} # or: ss -tlnp | grep :${port}`;
552
1344
  }
1345
+ async function waitForAgentmemoryReady(timeoutMs) {
1346
+ const start = Date.now();
1347
+ while (Date.now() - start < timeoutMs) {
1348
+ if (await isAgentmemoryReady()) return true;
1349
+ await new Promise((r) => setTimeout(r, 250));
1350
+ }
1351
+ return false;
1352
+ }
1353
+ function printReadyHint() {
1354
+ const hint = `Memory ready on :${getRestPort()} · viewer on ${getViewerUrl()} · try: agentmemory demo`;
1355
+ process.stdout.write("\n" + hint + "\n");
1356
+ }
553
1357
  async function main() {
554
- p.intro("agentmemory");
1358
+ if (IS_RESET) resetPrefs();
1359
+ const firstRun = isFirstRun();
1360
+ const prefs = readPrefs();
1361
+ if (firstRun || IS_RESET || IS_VERBOSE || !prefs.skipSplash) renderSplash(VERSION);
1362
+ if (firstRun || IS_RESET) await runOnboarding();
555
1363
  if (skipEngine) {
556
- p.log.info("Skipping engine check (--no-engine)");
557
- await import("./src-BBI-ah3h.mjs");
1364
+ if (IS_VERBOSE) p.log.info("Skipping engine check (--no-engine)");
1365
+ await import("./src-BGcqJR1a.mjs");
1366
+ if (await waitForAgentmemoryReady(15e3)) printReadyHint();
558
1367
  return;
559
1368
  }
560
1369
  if (await isEngineRunning()) {
561
- p.log.success("iii-engine is running");
562
- await import("./src-BBI-ah3h.mjs");
1370
+ if (IS_VERBOSE) p.log.success("iii-engine is running");
1371
+ warnIfEngineVersionMismatch(whichBinary("iii") ?? fallbackIiiPaths().find((p) => existsSync(p)) ?? null);
1372
+ adoptRunningEngine();
1373
+ maybeEmitNpxHint();
1374
+ await import("./src-BGcqJR1a.mjs");
1375
+ if (await waitForAgentmemoryReady(15e3)) printReadyHint();
563
1376
  return;
564
1377
  }
565
1378
  if (!await startEngine()) {
@@ -603,7 +1416,10 @@ async function main() {
603
1416
  process.exit(1);
604
1417
  }
605
1418
  s.stop("iii-engine is ready");
606
- await import("./src-BBI-ah3h.mjs");
1419
+ maybeEmitNpxHint();
1420
+ await import("./src-BGcqJR1a.mjs");
1421
+ if (await waitForAgentmemoryReady(15e3)) printReadyHint();
1422
+ writePrefs({ skipSplash: true });
607
1423
  }
608
1424
  async function apiFetch(base, path, timeoutMs = 5e3) {
609
1425
  try {
@@ -717,10 +1533,143 @@ function checkClaudeCodeHooks() {
717
1533
  if (content.includes("Loading hooks from plugin: agentmemory")) return { state: "loaded" };
718
1534
  return { state: "not-loaded" };
719
1535
  }
720
- async function runDoctor() {
721
- p.intro("agentmemory doctor");
1536
+ function buildDoctorContext() {
1537
+ return {
1538
+ baseUrl: getBaseUrl(),
1539
+ viewerUrl: getViewerUrl(),
1540
+ envPath: join(homedir(), ".agentmemory", ".env"),
1541
+ pidfilePath: enginePidfilePath(),
1542
+ enginePath: engineStatePath(),
1543
+ pinnedVersion: IIPINNED_VERSION
1544
+ };
1545
+ }
1546
+ function buildDoctorEffects() {
1547
+ return {
1548
+ envFileExists: () => existsSync(join(homedir(), ".agentmemory", ".env")),
1549
+ readEnvFile: () => {
1550
+ try {
1551
+ return parseEnvFile(readFileSync(join(homedir(), ".agentmemory", ".env"), "utf-8"));
1552
+ } catch {
1553
+ return {};
1554
+ }
1555
+ },
1556
+ pidfileExists: () => existsSync(enginePidfilePath()),
1557
+ pidfilePidIsAlive: () => {
1558
+ const pid = readEnginePidfile();
1559
+ if (pid === null) return null;
1560
+ return pidAlive(pid);
1561
+ },
1562
+ findIiiBinary: () => whichBinary("iii"),
1563
+ localBinIiiPath: () => join(homedir(), ".local", "bin", IS_WINDOWS ? "iii.exe" : "iii"),
1564
+ iiiBinaryVersion: (binPath) => iiiBinVersion(binPath),
1565
+ viewerReachable: async (timeoutMs = 2e3) => {
1566
+ try {
1567
+ return (await fetch(getViewerUrl(), { signal: AbortSignal.timeout(timeoutMs) })).ok;
1568
+ } catch {
1569
+ return false;
1570
+ }
1571
+ },
1572
+ runInit: async () => {
1573
+ try {
1574
+ await runInit();
1575
+ return {
1576
+ ok: true,
1577
+ message: "Wrote ~/.agentmemory/.env"
1578
+ };
1579
+ } catch (err) {
1580
+ return {
1581
+ ok: false,
1582
+ message: err instanceof Error ? err.message : String(err)
1583
+ };
1584
+ }
1585
+ },
1586
+ openEditor: async (path) => {
1587
+ const editor = process.env["EDITOR"] || process.env["VISUAL"] || "nano";
1588
+ p.log.info(`Opening ${path} in ${editor}…`);
1589
+ try {
1590
+ const result = spawnSync(editor, [path], { stdio: "inherit" });
1591
+ if (result.error) return {
1592
+ ok: false,
1593
+ message: `Failed to launch ${editor}: ${result.error.message}`
1594
+ };
1595
+ if ((result.status ?? 0) !== 0) return {
1596
+ ok: false,
1597
+ message: `${editor} exited with code ${result.status}`
1598
+ };
1599
+ return {
1600
+ ok: true,
1601
+ message: `Saved ${path}`
1602
+ };
1603
+ } catch (err) {
1604
+ return {
1605
+ ok: false,
1606
+ message: err instanceof Error ? err.message : String(err)
1607
+ };
1608
+ }
1609
+ },
1610
+ runIiiInstaller: async () => {
1611
+ const r = await runIiiInstaller();
1612
+ return {
1613
+ ok: r.ok,
1614
+ message: r.ok ? `Installed iii v${IIPINNED_VERSION} to ${r.binPath}` : "iii installer failed (see warnings above)"
1615
+ };
1616
+ },
1617
+ runStop: async () => {
1618
+ try {
1619
+ const portPids = findEnginePidsByPort(getRestPort());
1620
+ const pidfilePid = readEnginePidfile();
1621
+ if (portPids.length === 0 && pidfilePid === null) {
1622
+ clearEnginePidfile();
1623
+ clearEngineState();
1624
+ return {
1625
+ ok: true,
1626
+ message: "Nothing to stop."
1627
+ };
1628
+ }
1629
+ const candidates = /* @__PURE__ */ new Set();
1630
+ if (pidfilePid) candidates.add(pidfilePid);
1631
+ for (const pid of portPids) candidates.add(pid);
1632
+ let allStopped = true;
1633
+ for (const pid of candidates) if (!await signalAndWait(pid, "SIGTERM", 3e3)) allStopped = false;
1634
+ clearEnginePidfile();
1635
+ clearEngineState();
1636
+ return {
1637
+ ok: allStopped,
1638
+ message: allStopped ? "Engine stopped." : "Some engine pids survived."
1639
+ };
1640
+ } catch (err) {
1641
+ return {
1642
+ ok: false,
1643
+ message: err instanceof Error ? err.message : String(err)
1644
+ };
1645
+ }
1646
+ },
1647
+ runStart: async () => {
1648
+ try {
1649
+ if (!await startEngine()) return {
1650
+ ok: false,
1651
+ message: "startEngine() returned false"
1652
+ };
1653
+ const ready = await waitForEngine(15e3);
1654
+ return {
1655
+ ok: ready,
1656
+ message: ready ? "Engine ready" : "Engine did not become ready within 15s"
1657
+ };
1658
+ } catch (err) {
1659
+ return {
1660
+ ok: false,
1661
+ message: err instanceof Error ? err.message : String(err)
1662
+ };
1663
+ }
1664
+ },
1665
+ clearEnginePidAndState: () => {
1666
+ clearEnginePidfile();
1667
+ clearEngineState();
1668
+ }
1669
+ };
1670
+ }
1671
+ async function passiveServerChecks() {
722
1672
  const base = getBaseUrl();
723
- const viewerUrl = getViewerUrl();
724
1673
  const checks = [];
725
1674
  const serverUp = await isEngineRunning();
726
1675
  checks.push({
@@ -728,16 +1677,12 @@ async function runDoctor() {
728
1677
  ok: serverUp,
729
1678
  hint: serverUp ? void 0 : `Start with: npx @agentmemory/agentmemory (tried ${base})`
730
1679
  });
731
- if (!serverUp) {
732
- p.note(formatChecks(checks), "server unreachable");
733
- process.exit(1);
734
- }
1680
+ if (!serverUp) return checks;
735
1681
  const [health, flags, graph] = await Promise.all([
736
1682
  apiFetch(base, "health", 3e3),
737
1683
  apiFetch(base, "config/flags", 3e3),
738
1684
  apiFetch(base, "graph/stats", 3e3)
739
1685
  ]);
740
- const viewerUp = await fetch(viewerUrl, { signal: AbortSignal.timeout(2e3) }).then((r) => r.ok).catch(() => false);
741
1686
  const hasLlm = flags?.provider === "llm";
742
1687
  const hasEmbed = flags?.embeddingProvider === "embeddings";
743
1688
  const graphHas = Number(graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? 0) > 0;
@@ -745,18 +1690,14 @@ async function runDoctor() {
745
1690
  name: "Health status",
746
1691
  ok: health?.status === "healthy",
747
1692
  hint: health?.status === "healthy" ? void 0 : `Status: ${health?.status || "unknown"}`
748
- }, {
749
- name: "Viewer reachable",
750
- ok: viewerUp,
751
- hint: viewerUp ? void 0 : `${viewerUrl} not responding`
752
1693
  }, {
753
1694
  name: "LLM provider",
754
1695
  ok: hasLlm,
755
- hint: hasLlm ? void 0 : "export ANTHROPIC_API_KEY=sk-ant-... (or GEMINI/OPENROUTER/MINIMAX) then restart"
1696
+ hint: hasLlm ? void 0 : "set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env"
756
1697
  }, {
757
1698
  name: "Embedding provider",
758
1699
  ok: hasEmbed,
759
- hint: hasEmbed ? void 0 : "Running BM25-only. Add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST for semantic recall"
1700
+ hint: hasEmbed ? void 0 : "Running BM25-only. Add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST"
760
1701
  });
761
1702
  for (const f of flags?.flags || []) checks.push({
762
1703
  name: f.label,
@@ -772,7 +1713,7 @@ async function runDoctor() {
772
1713
  };
773
1714
  case "not-loaded": return {
774
1715
  ok: false,
775
- hint: "Plugin enabled but hooks not loaded by Claude Code. Try: /plugin uninstall agentmemory@agentmemory && /plugin install agentmemory@agentmemory, then restart the session. CC must be >= 2.1.x for plugin-hook auto-load."
1716
+ hint: "Plugin enabled but hooks not loaded by Claude Code. Try: /plugin uninstall agentmemory@agentmemory && /plugin install agentmemory@agentmemory, then restart the session."
776
1717
  };
777
1718
  case "no-debug-log": return {
778
1719
  ok: false,
@@ -788,16 +1729,136 @@ async function runDoctor() {
788
1729
  checks.push({
789
1730
  name: "Knowledge graph populated",
790
1731
  ok: graphHas,
791
- hint: graphHas ? void 0 : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true, or POST /agentmemory/graph/extract"
1732
+ hint: graphHas ? void 0 : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true."
1733
+ });
1734
+ return checks;
1735
+ }
1736
+ async function askFixAction(d) {
1737
+ const choice = await p.select({
1738
+ message: `[${d.id}] ${d.message}`,
1739
+ options: [
1740
+ {
1741
+ value: "fix",
1742
+ label: "F Fix",
1743
+ hint: d.fixPreview
1744
+ },
1745
+ {
1746
+ value: "skip",
1747
+ label: "S Skip"
1748
+ },
1749
+ {
1750
+ value: "more",
1751
+ label: "? More info"
1752
+ },
1753
+ {
1754
+ value: "quit",
1755
+ label: "Q Quit doctor"
1756
+ }
1757
+ ],
1758
+ initialValue: "fix"
792
1759
  });
793
- const passed = checks.filter((c) => c.ok).length;
794
- const total = checks.length;
795
- p.note(formatChecks(checks), `${passed}/${total} checks passing`);
796
- if (passed === total) p.outro("✓ All checks passed. agentmemory is healthy.");
797
- else {
798
- p.outro(`${total - passed} issue(s) — follow hints above to fix.`);
1760
+ if (p.isCancel(choice)) return "quit";
1761
+ return choice;
1762
+ }
1763
+ async function applyFixWithReport(d, ctx, dryRun) {
1764
+ if (dryRun) {
1765
+ p.log.info(`[dry-run] would: ${d.fixPreview}`);
1766
+ return {
1767
+ ok: true,
1768
+ message: "(dry-run)"
1769
+ };
1770
+ }
1771
+ const result = await d.fix(ctx);
1772
+ if (result.ok) p.log.success(result.message ?? `${d.id} fixed.`);
1773
+ else p.log.error(result.message ?? `${d.id} fix failed.`);
1774
+ return result;
1775
+ }
1776
+ async function runDoctor() {
1777
+ p.intro("agentmemory doctor");
1778
+ const applyAll = args.includes("--all");
1779
+ const dryRun = args.includes("--dry-run");
1780
+ if (applyAll && dryRun) {
1781
+ p.log.error("Cannot combine --all and --dry-run.");
1782
+ process.exit(2);
1783
+ }
1784
+ const passive = await passiveServerChecks();
1785
+ const passivePassed = passive.filter((c) => c.ok).length;
1786
+ p.note(formatChecks(passive), `server: ${passivePassed}/${passive.length} passing`);
1787
+ const ctx = buildDoctorContext();
1788
+ const diagnostics = buildDiagnostics(buildDoctorEffects());
1789
+ if (dryRun) {
1790
+ const results = [];
1791
+ for (const d of diagnostics) results.push({
1792
+ diagnostic: d,
1793
+ status: await d.check(ctx)
1794
+ });
1795
+ const lines = dryRunPlan(ctx, results);
1796
+ p.note(lines.join("\n"), "dry-run plan");
1797
+ p.outro("Dry-run complete. Re-run without --dry-run to apply.");
1798
+ return;
1799
+ }
1800
+ let failed = 0;
1801
+ let fixed = 0;
1802
+ let skipped = 0;
1803
+ let quit = false;
1804
+ for (const d of diagnostics) {
1805
+ if (quit) {
1806
+ skipped++;
1807
+ continue;
1808
+ }
1809
+ const status = await d.check(ctx);
1810
+ if (status.ok) {
1811
+ p.log.success(`${d.id} ✓${status.detail ? ` (${status.detail})` : ""}`);
1812
+ continue;
1813
+ }
1814
+ failed++;
1815
+ p.log.warn(`${d.id} ✗ ${status.detail ?? ""}`.trim());
1816
+ p.log.info(`why: ${d.fixPreview}`);
1817
+ if (d.manualOnly) p.log.info(`(manual fix only — see "${d.id}" docs)`);
1818
+ if (applyAll) {
1819
+ if ((await applyFixWithReport(d, ctx, false)).ok) fixed++;
1820
+ if (!(await d.check(ctx)).ok) p.log.warn(`${d.id} still failing after fix.`);
1821
+ continue;
1822
+ }
1823
+ while (true) {
1824
+ const action = await askFixAction(d);
1825
+ if (action === "fix") {
1826
+ if ((await applyFixWithReport(d, ctx, false)).ok) {
1827
+ const after = await d.check(ctx);
1828
+ if (after.ok) fixed++;
1829
+ else p.log.warn(`${d.id} still failing after fix: ${after.detail ?? ""}`);
1830
+ }
1831
+ break;
1832
+ }
1833
+ if (action === "skip") {
1834
+ skipped++;
1835
+ break;
1836
+ }
1837
+ if (action === "more") {
1838
+ p.note(d.moreInfo, `[${d.id}] more info`);
1839
+ continue;
1840
+ }
1841
+ if (action === "quit") {
1842
+ quit = true;
1843
+ break;
1844
+ }
1845
+ }
1846
+ }
1847
+ const summary = `${diagnostics.length} checks · ${failed} failing · ${fixed} fixed · ${skipped} skipped`;
1848
+ if (quit) {
1849
+ p.outro(`Quit early. ${summary}`);
799
1850
  process.exit(1);
800
1851
  }
1852
+ if (failed === 0) {
1853
+ p.outro("All diagnostics passing. agentmemory is healthy.");
1854
+ return;
1855
+ }
1856
+ if (failed - fixed === 0) {
1857
+ p.outro(`All fixes applied. ${summary}`);
1858
+ return;
1859
+ }
1860
+ p.outro(summary);
1861
+ process.exit(1);
801
1862
  }
802
1863
  function buildDemoSessions() {
803
1864
  return [
@@ -1178,6 +2239,7 @@ async function runStop() {
1178
2239
  const port = getRestPort();
1179
2240
  const state = readEngineState();
1180
2241
  const running = await isEngineRunning();
2242
+ const force = args.includes("--force");
1181
2243
  if (state?.kind === "docker") {
1182
2244
  if (!running) {
1183
2245
  p.log.info(`No engine responding on port ${port}.`);
@@ -1206,8 +2268,9 @@ async function runStop() {
1206
2268
  }
1207
2269
  if (!state) {
1208
2270
  const compose = discoverComposeFile();
1209
- if (compose && pidfilePid === null) {
1210
- p.log.error(`Engine is running on :${port} but no pidfile or state file is present. It may have been started via Docker compose by a different shell. Refusing to signal host PIDs.\n\nStop it with:\n docker compose -f ${compose} down\n\nOr re-run with AGENTMEMORY_USE_DOCKER=1 to record state next time.`);
2271
+ if (compose && pidfilePid === null) if (force) p.log.warn(`--force: bypassing Docker-heuristic guard. Falling back to native pidfile + lsof on :${port}.`);
2272
+ else {
2273
+ p.log.error(`Engine is running on :${port} but no pidfile or state file is present. It may have been started via Docker compose by a different shell. Refusing to signal host PIDs.\n\nStop it with:\n docker compose -f ${compose} down\n\nOr re-run with --force to signal whatever lsof finds on :${port}, or AGENTMEMORY_USE_DOCKER=1 to record state next time.`);
1211
2274
  process.exit(1);
1212
2275
  }
1213
2276
  }
@@ -1235,7 +2298,11 @@ async function runStop() {
1235
2298
  p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
1236
2299
  }
1237
2300
  async function runMcp() {
1238
- await import("./standalone-Cf5sp0XM.mjs");
2301
+ await import("./standalone-BQOaGF4z.mjs");
2302
+ }
2303
+ async function runConnectCmd() {
2304
+ const { runConnect } = await import("./connect-hRTF7E2c.mjs");
2305
+ await runConnect(args.slice(1));
1239
2306
  }
1240
2307
  async function runImportJsonl() {
1241
2308
  const VALUE_FLAGS = new Set(["--port", "--tools"]);
@@ -1339,13 +2406,126 @@ async function runImportJsonl() {
1339
2406
  process.exit(1);
1340
2407
  }
1341
2408
  }
2409
+ function loadConnectManifest(home) {
2410
+ const path = join(home, ".agentmemory", "backups", "connect-manifest.json");
2411
+ try {
2412
+ const raw = readFileSync(path, "utf-8");
2413
+ const parsed = JSON.parse(raw);
2414
+ if (Array.isArray(parsed?.installed)) return { installed: parsed.installed };
2415
+ return null;
2416
+ } catch {
2417
+ return null;
2418
+ }
2419
+ }
2420
+ function probeLocalBinIiiVersion(home) {
2421
+ const path = localBinIii(home);
2422
+ if (!existsSync(path)) return null;
2423
+ return iiiBinVersion(path);
2424
+ }
2425
+ function safeDelete(path) {
2426
+ try {
2427
+ if (!existsSync(path)) return {
2428
+ ok: true,
2429
+ message: `not present (${path})`
2430
+ };
2431
+ if (statSync(path).isDirectory()) rmSync(path, {
2432
+ recursive: true,
2433
+ force: true
2434
+ });
2435
+ else unlinkSync(path);
2436
+ return {
2437
+ ok: true,
2438
+ message: `deleted ${path}`
2439
+ };
2440
+ } catch (err) {
2441
+ return {
2442
+ ok: false,
2443
+ message: `failed ${path}: ${err instanceof Error ? err.message : String(err)}`
2444
+ };
2445
+ }
2446
+ }
2447
+ async function runRemove() {
2448
+ p.intro("agentmemory remove");
2449
+ const force = args.includes("--force");
2450
+ const keepData = args.includes("--keep-data");
2451
+ const home = homedir();
2452
+ const connectManifest = loadConnectManifest(home);
2453
+ const plan = buildRemovePlan({
2454
+ home,
2455
+ pinnedVersion: IIPINNED_VERSION,
2456
+ localBinIiiVersion: probeLocalBinIiiVersion(home),
2457
+ connectManifest
2458
+ }, {
2459
+ force,
2460
+ keepData
2461
+ });
2462
+ if (plan.filter((it) => it.applicable).length === 0) {
2463
+ p.outro("Nothing to remove. agentmemory is already gone.");
2464
+ return;
2465
+ }
2466
+ p.note(formatPlan(plan), "destruction plan");
2467
+ if (!force) {
2468
+ const proceed = await p.confirm({
2469
+ message: "Proceed with these deletions?",
2470
+ initialValue: false
2471
+ });
2472
+ if (p.isCancel(proceed) || proceed !== true) {
2473
+ p.cancel("Cancelled. Nothing was deleted.");
2474
+ return;
2475
+ }
2476
+ const sure = await p.confirm({
2477
+ message: "This is irreversible. Continue?",
2478
+ initialValue: false
2479
+ });
2480
+ if (p.isCancel(sure) || sure !== true) {
2481
+ p.cancel("Cancelled. Nothing was deleted.");
2482
+ return;
2483
+ }
2484
+ }
2485
+ for (const item of plan) {
2486
+ if (!item.applicable) continue;
2487
+ if (item.alwaysAsk) {
2488
+ const ok = await p.confirm({
2489
+ message: `${item.description} — really delete${item.path ? ` ${item.path}` : ""}?`,
2490
+ initialValue: false
2491
+ });
2492
+ if (p.isCancel(ok) || ok !== true) {
2493
+ p.log.info(`skipped: ${item.id}`);
2494
+ continue;
2495
+ }
2496
+ }
2497
+ if (item.id === "stop-engine") {
2498
+ try {
2499
+ const portPids = findEnginePidsByPort(getRestPort());
2500
+ const pidfilePid = readEnginePidfile();
2501
+ const cands = /* @__PURE__ */ new Set();
2502
+ if (pidfilePid) cands.add(pidfilePid);
2503
+ for (const pid of portPids) cands.add(pid);
2504
+ for (const pid of cands) await signalAndWait(pid, "SIGTERM", 3e3);
2505
+ clearEnginePidfile();
2506
+ clearEngineState();
2507
+ p.log.success(cands.size > 0 ? `stopped engine (${cands.size} pid${cands.size === 1 ? "" : "s"})` : "no engine running");
2508
+ } catch (err) {
2509
+ p.log.warn(`engine stop best-effort: ${err instanceof Error ? err.message : String(err)}`);
2510
+ }
2511
+ continue;
2512
+ }
2513
+ if (!item.path) continue;
2514
+ const r = safeDelete(item.path);
2515
+ if (r.ok) p.log.success(r.message);
2516
+ else p.log.error(r.message);
2517
+ }
2518
+ p.outro("Done. agentmemory cleanly removed. The npm package itself: npm uninstall -g @agentmemory/agentmemory");
2519
+ }
1342
2520
  ({
1343
2521
  init: runInit,
2522
+ connect: runConnectCmd,
1344
2523
  status: runStatus,
1345
2524
  doctor: runDoctor,
1346
2525
  demo: runDemo,
1347
2526
  upgrade: runUpgrade,
1348
2527
  stop: runStop,
2528
+ remove: runRemove,
1349
2529
  mcp: runMcp,
1350
2530
  "import-jsonl": runImportJsonl
1351
2531
  }[args[0] ?? ""] ?? main)().catch((err) => {
@@ -1354,5 +2534,5 @@ async function runImportJsonl() {
1354
2534
  });
1355
2535
 
1356
2536
  //#endregion
1357
- export { jaccardSimilarity as a, generateId as i, STREAM as n, fingerprintId as r, KV as t };
2537
+ export { STREAM as a, jaccardSimilarity as c, KV as i, bootLog as n, fingerprintId as o, logger as r, generateId as s, VERSION as t };
1358
2538
  //# sourceMappingURL=cli.mjs.map