@agentmemory/agentmemory 0.9.13 → 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, readFileSync, readdirSync, readlinkSync, statSync } 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,10 +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)
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).
118
860
  mcp Start standalone MCP server (no engine required)
119
861
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
120
862
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
@@ -122,14 +864,18 @@ Commands:
122
864
 
123
865
  Options:
124
866
  --help, -h Show this help
125
- --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
126
869
  --tools all|core Tool visibility (default: core = 7 tools)
127
870
  --no-engine Skip auto-starting iii-engine
128
871
  --port <N> Override REST port (default: 3111)
129
872
 
130
873
  Environment:
131
- AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
132
- Honored by status, doctor, and MCP shim commands.
874
+ AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
875
+ Honored by status, doctor, and MCP shim commands.
876
+ AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
877
+ native iii-engine binary on first run.
878
+ AGENTMEMORY_III_VERSION Override pinned iii-engine version (default ${IIPINNED_VERSION}).
133
879
 
134
880
  Quick start:
135
881
  npx @agentmemory/agentmemory # start with local iii-engine or Docker
@@ -220,6 +966,179 @@ function fallbackIiiPaths() {
220
966
  if (!home) return ["/usr/local/bin/iii"];
221
967
  return [join(home, ".local", "bin", "iii"), "/usr/local/bin/iii"];
222
968
  }
969
+ function iiiBinVersion(binPath) {
970
+ try {
971
+ const match = execFileSync(binPath, ["--version"], {
972
+ encoding: "utf-8",
973
+ stdio: [
974
+ "ignore",
975
+ "pipe",
976
+ "ignore"
977
+ ],
978
+ timeout: 3e3
979
+ }).match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
980
+ return match ? match[1] : null;
981
+ } catch {
982
+ return null;
983
+ }
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
+ }
995
+ function enginePidfilePath() {
996
+ return join(homedir(), ".agentmemory", "iii.pid");
997
+ }
998
+ function engineStatePath() {
999
+ return join(homedir(), ".agentmemory", "engine-state.json");
1000
+ }
1001
+ function writeEnginePidfile(pid) {
1002
+ try {
1003
+ const pidPath = enginePidfilePath();
1004
+ mkdirSync(dirname(pidPath), { recursive: true });
1005
+ writeFileSync(pidPath, `${pid}\n`, { encoding: "utf-8" });
1006
+ } catch (err) {
1007
+ vlog(`writeEnginePidfile: ${err instanceof Error ? err.message : String(err)}`);
1008
+ }
1009
+ }
1010
+ function readEnginePidfile() {
1011
+ try {
1012
+ const pidStr = readFileSync(enginePidfilePath(), "utf-8").trim();
1013
+ const pid = parseInt(pidStr, 10);
1014
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
1015
+ } catch {
1016
+ return null;
1017
+ }
1018
+ }
1019
+ function clearEnginePidfile() {
1020
+ try {
1021
+ unlinkSync(enginePidfilePath());
1022
+ } catch {}
1023
+ }
1024
+ function writeEngineState(state) {
1025
+ try {
1026
+ const statePath = engineStatePath();
1027
+ mkdirSync(dirname(statePath), { recursive: true });
1028
+ writeFileSync(statePath, `${JSON.stringify(state)}\n`, { encoding: "utf-8" });
1029
+ } catch (err) {
1030
+ vlog(`writeEngineState: ${err instanceof Error ? err.message : String(err)}`);
1031
+ }
1032
+ }
1033
+ function readEngineState() {
1034
+ try {
1035
+ const raw = readFileSync(engineStatePath(), "utf-8");
1036
+ const parsed = JSON.parse(raw);
1037
+ if (parsed && (parsed.kind === "native" || parsed.kind === "docker")) return parsed;
1038
+ return null;
1039
+ } catch {
1040
+ return null;
1041
+ }
1042
+ }
1043
+ function clearEngineState() {
1044
+ try {
1045
+ unlinkSync(engineStatePath());
1046
+ } catch {}
1047
+ }
1048
+ function discoverComposeFile() {
1049
+ return [
1050
+ join(__dirname, "..", "docker-compose.yml"),
1051
+ join(__dirname, "docker-compose.yml"),
1052
+ join(process.cwd(), "docker-compose.yml")
1053
+ ].find((c) => existsSync(c)) ?? null;
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
+ }
1094
+ async function runIiiInstaller() {
1095
+ const releaseUrl = iiiReleaseUrl();
1096
+ const asset = iiiReleaseAsset();
1097
+ const isZipAsset = asset?.endsWith(".zip") === true;
1098
+ if (!releaseUrl) {
1099
+ p.log.warn(`iii-engine binary not available for ${platform()}/${process.arch}. Use Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
1100
+ return {
1101
+ ok: false,
1102
+ binPath: null
1103
+ };
1104
+ }
1105
+ if (IS_WINDOWS || isZipAsset) {
1106
+ p.log.info(`Auto-install unavailable on ${platform()} — ${asset} isn't tar-compatible. Install manually:\n 1. Download ${releaseUrl}\n 2. Extract iii.exe and place it on PATH (e.g. %USERPROFILE%\\.local\\bin)\nOr use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`);
1107
+ return {
1108
+ ok: false,
1109
+ binPath: null
1110
+ };
1111
+ }
1112
+ const shBin = whichBinary("sh");
1113
+ const curlBin = whichBinary("curl");
1114
+ if (!shBin || !curlBin) {
1115
+ p.log.warn("curl or sh not found. Cannot auto-install iii-engine.");
1116
+ return {
1117
+ ok: false,
1118
+ binPath: null
1119
+ };
1120
+ }
1121
+ const binDir = join(homedir(), ".local", "bin");
1122
+ const binPath = join(binDir, "iii");
1123
+ if (!runCommand(shBin, ["-c", [
1124
+ `mkdir -p "${binDir}"`,
1125
+ `curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
1126
+ `chmod +x "${binPath}"`
1127
+ ].join(" && ")], {
1128
+ label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
1129
+ optional: true
1130
+ })) {
1131
+ p.log.warn(`iii-engine installer failed. Fallbacks: Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
1132
+ return {
1133
+ ok: false,
1134
+ binPath: null
1135
+ };
1136
+ }
1137
+ return {
1138
+ ok: true,
1139
+ binPath
1140
+ };
1141
+ }
223
1142
  let startupFailure = null;
224
1143
  function spawnEngineBackground(bin, spawnArgs, label) {
225
1144
  vlog(`spawn: ${bin} ${spawnArgs.join(" ")}`);
@@ -232,6 +1151,8 @@ function spawnEngineBackground(bin, spawnArgs, label) {
232
1151
  ],
233
1152
  windowsHide: true
234
1153
  });
1154
+ const isDocker = label.includes("Docker");
1155
+ if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
235
1156
  const stderrChunks = [];
236
1157
  let stderrBytes = 0;
237
1158
  const MAX_STDERR_CAPTURE = 16 * 1024;
@@ -245,27 +1166,48 @@ function spawnEngineBackground(bin, spawnArgs, label) {
245
1166
  if (code !== null && code !== 0 || code === null && signal !== null) {
246
1167
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
247
1168
  startupFailure = {
248
- kind: label.includes("Docker") ? "docker-crashed" : "engine-crashed",
1169
+ kind: isDocker ? "docker-crashed" : "engine-crashed",
249
1170
  stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
250
1171
  binary: bin
251
1172
  };
252
1173
  vlog(`engine exited early: code=${code} signal=${signal}`);
253
1174
  if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
1175
+ if (!isDocker) clearEnginePidfile();
1176
+ clearEngineState();
254
1177
  }
255
1178
  });
256
1179
  child.unref();
257
1180
  return child;
258
1181
  }
1182
+ function startIiiBin(iiiBin, configPath) {
1183
+ warnIfEngineVersionMismatch(iiiBin);
1184
+ const s = p.spinner();
1185
+ s.start(`Starting iii-engine: ${iiiBin}`);
1186
+ writeEngineState({
1187
+ kind: "native",
1188
+ configPath
1189
+ });
1190
+ spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
1191
+ s.stop("iii-engine process started");
1192
+ return true;
1193
+ }
259
1194
  async function startEngine() {
260
1195
  const configPath = findIiiConfig();
261
1196
  let iiiBin = whichBinary("iii");
262
1197
  vlog(`iii binary: ${iiiBin ?? "(not on PATH)"}, config: ${configPath || "(not found)"}`);
263
- if (iiiBin && configPath) {
264
- const s = p.spinner();
265
- s.start(`Starting iii-engine: ${iiiBin}`);
266
- spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
267
- s.stop("iii-engine process started");
268
- return true;
1198
+ if (iiiBin && configPath) return startIiiBin(iiiBin, configPath);
1199
+ for (const iiiPath of fallbackIiiPaths()) if (existsSync(iiiPath)) {
1200
+ const v = iiiBinVersion(iiiPath);
1201
+ vlog(`fallback iii at ${iiiPath} reports version: ${v ?? "unknown"}`);
1202
+ p.log.info(`Found iii at: ${iiiPath}${v ? ` (v${v})` : ""}`);
1203
+ process.env["PATH"] = `${dirname(iiiPath)}${delimiter}${process.env["PATH"] ?? ""}`;
1204
+ iiiBin = iiiPath;
1205
+ break;
1206
+ }
1207
+ if (iiiBin && configPath) return startIiiBin(iiiBin, configPath);
1208
+ if (!configPath) {
1209
+ startupFailure = { kind: "no-engine" };
1210
+ return false;
269
1211
  }
270
1212
  const dockerBin = whichBinary("docker");
271
1213
  vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
@@ -275,9 +1217,73 @@ async function startEngine() {
275
1217
  join(process.cwd(), "docker-compose.yml")
276
1218
  ].find((c) => existsSync(c));
277
1219
  vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
278
- if (dockerBin && composeFile) {
1220
+ const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
1221
+ const interactive = !!process.stdin.isTTY && !process.env["CI"];
1222
+ let choice;
1223
+ if (dockerOptIn && dockerBin && composeFile) choice = "docker";
1224
+ else if (!interactive) {
1225
+ choice = "install";
1226
+ p.log.info("Non-interactive environment detected — auto-installing iii-engine.");
1227
+ } else {
1228
+ p.log.warn(`iii-engine binary not found locally.`);
1229
+ const options = [{
1230
+ value: "install",
1231
+ label: `Install iii v${IIPINNED_VERSION} to ~/.local/bin (~6MB, ~5s)`,
1232
+ hint: "recommended"
1233
+ }];
1234
+ if (dockerBin && composeFile) options.push({
1235
+ value: "docker",
1236
+ label: "Use Docker compose",
1237
+ hint: "advanced"
1238
+ });
1239
+ options.push({
1240
+ value: "manual",
1241
+ label: "Show manual install steps and exit"
1242
+ });
1243
+ const picked = await p.select({
1244
+ message: "How would you like to start iii-engine?",
1245
+ options,
1246
+ initialValue: "install"
1247
+ });
1248
+ if (p.isCancel(picked)) {
1249
+ startupFailure = { kind: "no-engine" };
1250
+ return false;
1251
+ }
1252
+ choice = picked;
1253
+ }
1254
+ if (choice === "manual") {
1255
+ startupFailure = { kind: "no-engine" };
1256
+ return false;
1257
+ }
1258
+ if (choice === "install") {
1259
+ const result = await runIiiInstaller();
1260
+ if (result.ok && result.binPath) {
1261
+ process.env["PATH"] = `${dirname(result.binPath)}${delimiter}${process.env["PATH"] ?? ""}`;
1262
+ iiiBin = result.binPath;
1263
+ return startIiiBin(iiiBin, configPath);
1264
+ }
1265
+ if (dockerBin && composeFile && interactive) {
1266
+ const fallback = await p.confirm({
1267
+ message: "Auto-install failed. Try Docker compose instead?",
1268
+ initialValue: true
1269
+ });
1270
+ if (p.isCancel(fallback) || fallback !== true) {
1271
+ startupFailure = { kind: "no-engine" };
1272
+ return false;
1273
+ }
1274
+ choice = "docker";
1275
+ } else {
1276
+ startupFailure = { kind: "no-engine" };
1277
+ return false;
1278
+ }
1279
+ }
1280
+ if (choice === "docker" && dockerBin && composeFile) {
279
1281
  const s = p.spinner();
280
1282
  s.start("Starting iii-engine via Docker...");
1283
+ writeEngineState({
1284
+ kind: "docker",
1285
+ composeFile
1286
+ });
281
1287
  spawnEngineBackground(dockerBin, [
282
1288
  "compose",
283
1289
  "-f",
@@ -288,21 +1294,8 @@ async function startEngine() {
288
1294
  s.stop("Docker compose started");
289
1295
  return true;
290
1296
  }
291
- for (const iiiPath of fallbackIiiPaths()) if (existsSync(iiiPath)) {
292
- p.log.info(`Found iii at: ${iiiPath}`);
293
- process.env["PATH"] = `${dirname(iiiPath)}${delimiter}${process.env["PATH"] ?? ""}`;
294
- iiiBin = iiiPath;
295
- break;
296
- }
297
- if (iiiBin && configPath) {
298
- const s = p.spinner();
299
- s.start(`Starting iii-engine: ${iiiBin}`);
300
- spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
301
- s.stop("iii-engine process started");
302
- return true;
303
- }
304
- if (!iiiBin && (!dockerBin || !composeFile)) startupFailure = { kind: "no-engine" };
305
- else if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
1297
+ if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
1298
+ else startupFailure = { kind: "no-engine" };
306
1299
  return false;
307
1300
  }
308
1301
  async function waitForEngine(timeoutMs) {
@@ -316,58 +1309,70 @@ async function waitForEngine(timeoutMs) {
316
1309
  function installInstructions() {
317
1310
  const releaseUrl = iiiReleaseUrl();
318
1311
  if (IS_WINDOWS) return [
319
- `agentmemory requires the \`iii-engine\` runtime, pinned to v${IIPINNED_VERSION}. Pick one:`,
1312
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
320
1313
  "",
321
1314
  " A) Download the prebuilt Windows binary:",
322
1315
  ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
323
- ` 2. Download iii-x86_64-pc-windows-msvc.zip`,
324
- " (or iii-aarch64-pc-windows-msvc.zip on ARM)",
325
- " 3. Extract iii.exe and either add its folder to PATH",
326
- " or move it to %USERPROFILE%\\.local\\bin\\iii.exe",
1316
+ ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
1317
+ " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
327
1318
  " 4. Re-run: npx @agentmemory/agentmemory",
328
1319
  "",
329
- " B) Docker Desktop:",
330
- " 1. Install Docker Desktop for Windows",
331
- ` 2. docker pull iiidev/iii:${IIPINNED_VERSION}`,
332
- " 3. Start Docker Desktop (engine must be running)",
333
- " 4. Re-run: npx @agentmemory/agentmemory",
1320
+ ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
1321
+ " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
334
1322
  "",
335
- "Or skip the engine entirely for standalone MCP:",
336
- " npx @agentmemory/agentmemory mcp"
1323
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
1324
+ "",
1325
+ "Docs: https://iii.dev/docs"
337
1326
  ];
338
- const linuxInstall = releaseUrl ? ` A) mkdir -p ~/.local/bin && curl -fsSL "${releaseUrl}" | tar -xz -C ~/.local/bin && chmod +x ~/.local/bin/iii` : ` A) Manual download from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
1327
+ const linuxInstall = releaseUrl ? ` A) curl -fsSL "${releaseUrl}" | tar -xz -C ~/.local/bin && chmod +x ~/.local/bin/iii` : ` A) Manual download: https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
339
1328
  return [
340
- `agentmemory requires the \`iii-engine\` runtime, pinned to v${IIPINNED_VERSION}. Pick one:`,
1329
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
341
1330
  "",
342
1331
  linuxInstall,
343
- ` (installs iii v${IIPINNED_VERSION} into ~/.local/bin/iii)`,
1332
+ " Then re-run: npx @agentmemory/agentmemory",
344
1333
  "",
345
- ` B) Docker: \`docker pull iiidev/iii:${IIPINNED_VERSION}\``,
1334
+ ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
1335
+ " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
346
1336
  "",
347
- "Or skip the engine entirely for standalone MCP:",
348
- " npx @agentmemory/agentmemory mcp",
1337
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
349
1338
  "",
350
- "Docs: https://iii.dev/docs",
351
- `Why pinned: iii v0.11.6 introduces the new sandbox-everything model`,
352
- `(\`iii worker add\` registration). agentmemory still uses the older`,
353
- `iii-exec config-file worker model and needs a refactor before it`,
354
- `runs cleanly under the new engine. Override with`,
355
- `AGENTMEMORY_III_VERSION=<version> when you've migrated manually.`
1339
+ "Docs: https://iii.dev/docs"
356
1340
  ];
357
1341
  }
358
1342
  function portInUseDiagnostic(port) {
359
1343
  return IS_WINDOWS ? ` netstat -ano | findstr :${port}` : ` lsof -i :${port} # or: ss -tlnp | grep :${port}`;
360
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
+ }
361
1357
  async function main() {
362
- 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();
363
1363
  if (skipEngine) {
364
- p.log.info("Skipping engine check (--no-engine)");
365
- await import("./src-Ca9oX6Hq.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();
366
1367
  return;
367
1368
  }
368
1369
  if (await isEngineRunning()) {
369
- p.log.success("iii-engine is running");
370
- await import("./src-Ca9oX6Hq.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();
371
1376
  return;
372
1377
  }
373
1378
  if (!await startEngine()) {
@@ -411,7 +1416,10 @@ async function main() {
411
1416
  process.exit(1);
412
1417
  }
413
1418
  s.stop("iii-engine is ready");
414
- await import("./src-Ca9oX6Hq.mjs");
1419
+ maybeEmitNpxHint();
1420
+ await import("./src-BGcqJR1a.mjs");
1421
+ if (await waitForAgentmemoryReady(15e3)) printReadyHint();
1422
+ writePrefs({ skipSplash: true });
415
1423
  }
416
1424
  async function apiFetch(base, path, timeoutMs = 5e3) {
417
1425
  try {
@@ -525,10 +1533,143 @@ function checkClaudeCodeHooks() {
525
1533
  if (content.includes("Loading hooks from plugin: agentmemory")) return { state: "loaded" };
526
1534
  return { state: "not-loaded" };
527
1535
  }
528
- async function runDoctor() {
529
- 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() {
530
1672
  const base = getBaseUrl();
531
- const viewerUrl = getViewerUrl();
532
1673
  const checks = [];
533
1674
  const serverUp = await isEngineRunning();
534
1675
  checks.push({
@@ -536,16 +1677,12 @@ async function runDoctor() {
536
1677
  ok: serverUp,
537
1678
  hint: serverUp ? void 0 : `Start with: npx @agentmemory/agentmemory (tried ${base})`
538
1679
  });
539
- if (!serverUp) {
540
- p.note(formatChecks(checks), "server unreachable");
541
- process.exit(1);
542
- }
1680
+ if (!serverUp) return checks;
543
1681
  const [health, flags, graph] = await Promise.all([
544
1682
  apiFetch(base, "health", 3e3),
545
1683
  apiFetch(base, "config/flags", 3e3),
546
1684
  apiFetch(base, "graph/stats", 3e3)
547
1685
  ]);
548
- const viewerUp = await fetch(viewerUrl, { signal: AbortSignal.timeout(2e3) }).then((r) => r.ok).catch(() => false);
549
1686
  const hasLlm = flags?.provider === "llm";
550
1687
  const hasEmbed = flags?.embeddingProvider === "embeddings";
551
1688
  const graphHas = Number(graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? 0) > 0;
@@ -553,18 +1690,14 @@ async function runDoctor() {
553
1690
  name: "Health status",
554
1691
  ok: health?.status === "healthy",
555
1692
  hint: health?.status === "healthy" ? void 0 : `Status: ${health?.status || "unknown"}`
556
- }, {
557
- name: "Viewer reachable",
558
- ok: viewerUp,
559
- hint: viewerUp ? void 0 : `${viewerUrl} not responding`
560
1693
  }, {
561
1694
  name: "LLM provider",
562
1695
  ok: hasLlm,
563
- 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"
564
1697
  }, {
565
1698
  name: "Embedding provider",
566
1699
  ok: hasEmbed,
567
- 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"
568
1701
  });
569
1702
  for (const f of flags?.flags || []) checks.push({
570
1703
  name: f.label,
@@ -580,7 +1713,7 @@ async function runDoctor() {
580
1713
  };
581
1714
  case "not-loaded": return {
582
1715
  ok: false,
583
- 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."
584
1717
  };
585
1718
  case "no-debug-log": return {
586
1719
  ok: false,
@@ -596,16 +1729,136 @@ async function runDoctor() {
596
1729
  checks.push({
597
1730
  name: "Knowledge graph populated",
598
1731
  ok: graphHas,
599
- 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."
600
1733
  });
601
- const passed = checks.filter((c) => c.ok).length;
602
- const total = checks.length;
603
- p.note(formatChecks(checks), `${passed}/${total} checks passing`);
604
- if (passed === total) p.outro("✓ All checks passed. agentmemory is healthy.");
605
- else {
606
- p.outro(`${total - passed} issue(s) — follow hints above to fix.`);
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"
1759
+ });
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}`);
607
1850
  process.exit(1);
608
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);
609
1862
  }
610
1863
  function buildDemoSessions() {
611
1864
  return [
@@ -872,36 +2125,16 @@ async function runUpgrade() {
872
2125
  });
873
2126
  } else p.log.warn("No package manager found (pnpm/npm). Skipping JS dependency upgrade.");
874
2127
  else p.log.warn("No package.json in current directory. Skipping JS dependency upgrade.");
875
- const shBin = whichBinary("sh");
876
- const curlBin = whichBinary("curl");
877
- if (shBin && curlBin) {
878
- const upgradeEngine = await p.confirm({
879
- message: "Re-run the iii-engine install script (curl | sh)?",
880
- initialValue: true
881
- });
882
- if (p.isCancel(upgradeEngine)) {
883
- p.cancel("Cancelled.");
884
- return process.exit(0);
885
- }
886
- if (upgradeEngine === true) {
887
- const releaseUrl = iiiReleaseUrl();
888
- const asset = iiiReleaseAsset();
889
- const isZipAsset = asset?.endsWith(".zip") === true;
890
- if (!releaseUrl) p.log.warn(`iii-engine binary not available for ${platform()}/${process.arch}. Use Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
891
- else if (IS_WINDOWS || isZipAsset) p.log.info(`Skipping auto-install on ${platform()} — the ${asset} asset isn't tar-compatible. Install manually:\n 1. Download ${releaseUrl}\n 2. Extract iii.exe and place it on PATH (e.g. %USERPROFILE%\\.local\\bin)\nOr use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`);
892
- else {
893
- const binDir = join(homedir(), ".local", "bin");
894
- if (!runCommand(shBin, ["-c", [
895
- `mkdir -p "${binDir}"`,
896
- `curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
897
- `chmod +x "${binDir}/iii"`
898
- ].join(" && ")], {
899
- label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
900
- optional: true
901
- })) p.log.warn(`iii-engine installer failed. Fallbacks: Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
902
- }
903
- } else p.log.info("Skipped iii-engine installer.");
904
- } else p.log.warn("curl or sh not found. Skipping iii-engine installer.");
2128
+ const upgradeEngine = await p.confirm({
2129
+ message: "Re-run the iii-engine install script (curl | sh)?",
2130
+ initialValue: true
2131
+ });
2132
+ if (p.isCancel(upgradeEngine)) {
2133
+ p.cancel("Cancelled.");
2134
+ return process.exit(0);
2135
+ }
2136
+ if (upgradeEngine === true) await runIiiInstaller();
2137
+ else p.log.info("Skipped iii-engine installer.");
905
2138
  if (dockerBin) runCommand(dockerBin, ["pull", `iiidev/iii:${IIPINNED_VERSION}`], {
906
2139
  label: `Pulling iii Docker image v${IIPINNED_VERSION} (pinned)`,
907
2140
  optional: true
@@ -916,8 +2149,160 @@ async function runUpgrade() {
916
2149
  " 3) restart agentmemory process"
917
2150
  ].join("\n"), "agentmemory upgrade");
918
2151
  }
2152
+ function pidAlive(pid) {
2153
+ try {
2154
+ process.kill(pid, 0);
2155
+ return true;
2156
+ } catch (err) {
2157
+ return err?.code === "EPERM";
2158
+ }
2159
+ }
2160
+ async function signalAndWait(pid, initialSignal, timeoutMs) {
2161
+ try {
2162
+ process.kill(pid, initialSignal);
2163
+ } catch (err) {
2164
+ const code = err?.code;
2165
+ if (code === "ESRCH") return true;
2166
+ if (code === "EPERM") {
2167
+ p.log.warn(`No permission to signal pid ${pid}. Try: kill ${pid}`);
2168
+ return false;
2169
+ }
2170
+ vlog(`${initialSignal} ${pid}: ${err instanceof Error ? err.message : String(err)}`);
2171
+ return false;
2172
+ }
2173
+ const deadline = Date.now() + timeoutMs;
2174
+ while (Date.now() < deadline) {
2175
+ if (!pidAlive(pid)) return true;
2176
+ await new Promise((r) => setTimeout(r, 200));
2177
+ }
2178
+ if (!pidAlive(pid)) return true;
2179
+ try {
2180
+ process.kill(pid, "SIGKILL");
2181
+ } catch (err) {
2182
+ if (err?.code === "ESRCH") return true;
2183
+ vlog(`SIGKILL ${pid}: ${err instanceof Error ? err.message : String(err)}`);
2184
+ return false;
2185
+ }
2186
+ await new Promise((r) => setTimeout(r, 200));
2187
+ return !pidAlive(pid);
2188
+ }
2189
+ function findEnginePidsByPort(port) {
2190
+ if (IS_WINDOWS) return [];
2191
+ const lsof = whichBinary("lsof");
2192
+ if (!lsof) return [];
2193
+ const selfPid = process.pid;
2194
+ try {
2195
+ return execFileSync(lsof, [
2196
+ "-i",
2197
+ `:${port}`,
2198
+ "-sTCP:LISTEN",
2199
+ "-t"
2200
+ ], {
2201
+ encoding: "utf-8",
2202
+ stdio: [
2203
+ "ignore",
2204
+ "pipe",
2205
+ "ignore"
2206
+ ]
2207
+ }).split(/\s+/).map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n) && n > 0 && n !== selfPid);
2208
+ } catch (err) {
2209
+ vlog(`lsof :${port}: ${err instanceof Error ? err.message : String(err)}`);
2210
+ return [];
2211
+ }
2212
+ }
2213
+ async function stopDockerEngine(composeFile, port) {
2214
+ const dockerBin = whichBinary("docker");
2215
+ if (!dockerBin) {
2216
+ p.log.error(`Engine was started via Docker compose, but \`docker\` is no longer on PATH. Stop it manually:\n docker compose -f ${composeFile} down`);
2217
+ process.exit(1);
2218
+ }
2219
+ if (!existsSync(composeFile)) {
2220
+ p.log.error(`Engine state references ${composeFile}, but the file is gone. Stop it manually:\n docker compose down (from the dir holding the original docker-compose.yml)`);
2221
+ process.exit(1);
2222
+ }
2223
+ const ok = runCommand(dockerBin, [
2224
+ "compose",
2225
+ "-f",
2226
+ composeFile,
2227
+ "down"
2228
+ ], { label: `docker compose -f ${composeFile} down` });
2229
+ clearEnginePidfile();
2230
+ clearEngineState();
2231
+ if (!ok) {
2232
+ p.log.error(`docker compose down failed. The engine may still be running on :${port}. Inspect with:\n docker compose -f ${composeFile} ps`);
2233
+ process.exit(1);
2234
+ }
2235
+ p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
2236
+ }
2237
+ async function runStop() {
2238
+ p.intro("agentmemory stop");
2239
+ const port = getRestPort();
2240
+ const state = readEngineState();
2241
+ const running = await isEngineRunning();
2242
+ const force = args.includes("--force");
2243
+ if (state?.kind === "docker") {
2244
+ if (!running) {
2245
+ p.log.info(`No engine responding on port ${port}.`);
2246
+ clearEnginePidfile();
2247
+ clearEngineState();
2248
+ p.outro("Nothing to stop.");
2249
+ return;
2250
+ }
2251
+ await stopDockerEngine(state.composeFile, port);
2252
+ return;
2253
+ }
2254
+ const portPids = findEnginePidsByPort(port);
2255
+ const pidfilePid = readEnginePidfile();
2256
+ if (!running) {
2257
+ if (portPids.length === 0 && pidfilePid === null) {
2258
+ clearEnginePidfile();
2259
+ clearEngineState();
2260
+ p.outro("Nothing to stop.");
2261
+ return;
2262
+ }
2263
+ const survivors = new Set(portPids);
2264
+ if (pidfilePid) survivors.add(pidfilePid);
2265
+ p.log.warn(`Engine not responding on :${port}, but ${survivors.size} process(es) still hold the port or pidfile: ${[...survivors].join(", ")}`);
2266
+ p.log.info(`Preserving ~/.agentmemory/iii.pid. Investigate before manual cleanup:\n ps -p ${[...survivors].join(",")} -o pid,ppid,comm,etime\n ${IS_WINDOWS ? "netstat -ano | findstr :" + port : "lsof -i :" + port}`);
2267
+ process.exit(1);
2268
+ }
2269
+ if (!state) {
2270
+ const compose = discoverComposeFile();
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.`);
2274
+ process.exit(1);
2275
+ }
2276
+ }
2277
+ const candidates = /* @__PURE__ */ new Set();
2278
+ if (pidfilePid) candidates.add(pidfilePid);
2279
+ for (const pid of portPids) candidates.add(pid);
2280
+ if (candidates.size === 0) {
2281
+ p.log.error(`Could not locate engine process. Try:\n ${IS_WINDOWS ? "netstat -ano | findstr :" + port : "lsof -i :" + port + " -t | xargs kill -9"}`);
2282
+ process.exit(1);
2283
+ }
2284
+ let allStopped = true;
2285
+ for (const pid of candidates) {
2286
+ const s = p.spinner();
2287
+ s.start(`Stopping iii-engine (pid ${pid})...`);
2288
+ const ok = await signalAndWait(pid, "SIGTERM", 3e3);
2289
+ s.stop(ok ? `Stopped pid ${pid}` : `Failed to stop pid ${pid}`);
2290
+ if (!ok) allStopped = false;
2291
+ }
2292
+ clearEnginePidfile();
2293
+ clearEngineState();
2294
+ if (!allStopped) {
2295
+ p.log.error("One or more engine processes survived SIGKILL. Investigate with `ps`.");
2296
+ process.exit(1);
2297
+ }
2298
+ p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
2299
+ }
919
2300
  async function runMcp() {
920
- await import("./standalone-BpbiNqr9.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));
921
2306
  }
922
2307
  async function runImportJsonl() {
923
2308
  const VALUE_FLAGS = new Set(["--port", "--tools"]);
@@ -1021,12 +2406,126 @@ async function runImportJsonl() {
1021
2406
  process.exit(1);
1022
2407
  }
1023
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
+ }
1024
2520
  ({
1025
2521
  init: runInit,
2522
+ connect: runConnectCmd,
1026
2523
  status: runStatus,
1027
2524
  doctor: runDoctor,
1028
2525
  demo: runDemo,
1029
2526
  upgrade: runUpgrade,
2527
+ stop: runStop,
2528
+ remove: runRemove,
1030
2529
  mcp: runMcp,
1031
2530
  "import-jsonl": runImportJsonl
1032
2531
  }[args[0] ?? ""] ?? main)().catch((err) => {
@@ -1035,5 +2534,5 @@ async function runImportJsonl() {
1035
2534
  });
1036
2535
 
1037
2536
  //#endregion
1038
- 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 };
1039
2538
  //# sourceMappingURL=cli.mjs.map