@agentmemory/agentmemory 0.9.12 → 0.9.14

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,6 +1,6 @@
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 { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, unlinkSync, writeFileSync } 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";
@@ -110,10 +110,12 @@ Usage: agentmemory [command] [options]
110
110
 
111
111
  Commands:
112
112
  (default) Start agentmemory worker
113
+ init Copy bundled .env.example to ~/.agentmemory/.env if absent
113
114
  status Show connection status, memory count, flags, and health
114
115
  doctor Run diagnostic checks (server, flags, graph, providers)
115
116
  demo Seed sample sessions and show recall in action
116
117
  upgrade Upgrade local deps + iii runtime (best effort)
118
+ stop Stop the running iii-engine started by this CLI
117
119
  mcp Start standalone MCP server (no engine required)
118
120
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
119
121
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
@@ -127,8 +129,11 @@ Options:
127
129
  --port <N> Override REST port (default: 3111)
128
130
 
129
131
  Environment:
130
- AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
131
- Honored by status, doctor, and MCP shim commands.
132
+ AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
133
+ Honored by status, doctor, and MCP shim commands.
134
+ AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
135
+ native iii-engine binary on first run.
136
+ AGENTMEMORY_III_VERSION Override pinned iii-engine version (default ${IIPINNED_VERSION}).
132
137
 
133
138
  Quick start:
134
139
  npx @agentmemory/agentmemory # start with local iii-engine or Docker
@@ -219,6 +224,130 @@ function fallbackIiiPaths() {
219
224
  if (!home) return ["/usr/local/bin/iii"];
220
225
  return [join(home, ".local", "bin", "iii"), "/usr/local/bin/iii"];
221
226
  }
227
+ function iiiBinVersion(binPath) {
228
+ try {
229
+ const match = execFileSync(binPath, ["--version"], {
230
+ encoding: "utf-8",
231
+ stdio: [
232
+ "ignore",
233
+ "pipe",
234
+ "ignore"
235
+ ],
236
+ timeout: 3e3
237
+ }).match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
238
+ return match ? match[1] : null;
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ function enginePidfilePath() {
244
+ return join(homedir(), ".agentmemory", "iii.pid");
245
+ }
246
+ function engineStatePath() {
247
+ return join(homedir(), ".agentmemory", "engine-state.json");
248
+ }
249
+ function writeEnginePidfile(pid) {
250
+ try {
251
+ const pidPath = enginePidfilePath();
252
+ mkdirSync(dirname(pidPath), { recursive: true });
253
+ writeFileSync(pidPath, `${pid}\n`, { encoding: "utf-8" });
254
+ } catch (err) {
255
+ vlog(`writeEnginePidfile: ${err instanceof Error ? err.message : String(err)}`);
256
+ }
257
+ }
258
+ function readEnginePidfile() {
259
+ try {
260
+ const pidStr = readFileSync(enginePidfilePath(), "utf-8").trim();
261
+ const pid = parseInt(pidStr, 10);
262
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+ function clearEnginePidfile() {
268
+ try {
269
+ unlinkSync(enginePidfilePath());
270
+ } catch {}
271
+ }
272
+ function writeEngineState(state) {
273
+ try {
274
+ const statePath = engineStatePath();
275
+ mkdirSync(dirname(statePath), { recursive: true });
276
+ writeFileSync(statePath, `${JSON.stringify(state)}\n`, { encoding: "utf-8" });
277
+ } catch (err) {
278
+ vlog(`writeEngineState: ${err instanceof Error ? err.message : String(err)}`);
279
+ }
280
+ }
281
+ function readEngineState() {
282
+ try {
283
+ const raw = readFileSync(engineStatePath(), "utf-8");
284
+ const parsed = JSON.parse(raw);
285
+ if (parsed && (parsed.kind === "native" || parsed.kind === "docker")) return parsed;
286
+ return null;
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+ function clearEngineState() {
292
+ try {
293
+ unlinkSync(engineStatePath());
294
+ } catch {}
295
+ }
296
+ function discoverComposeFile() {
297
+ return [
298
+ join(__dirname, "..", "docker-compose.yml"),
299
+ join(__dirname, "docker-compose.yml"),
300
+ join(process.cwd(), "docker-compose.yml")
301
+ ].find((c) => existsSync(c)) ?? null;
302
+ }
303
+ async function runIiiInstaller() {
304
+ const releaseUrl = iiiReleaseUrl();
305
+ const asset = iiiReleaseAsset();
306
+ const isZipAsset = asset?.endsWith(".zip") === true;
307
+ if (!releaseUrl) {
308
+ 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}.`);
309
+ return {
310
+ ok: false,
311
+ binPath: null
312
+ };
313
+ }
314
+ if (IS_WINDOWS || isZipAsset) {
315
+ 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}`);
316
+ return {
317
+ ok: false,
318
+ binPath: null
319
+ };
320
+ }
321
+ const shBin = whichBinary("sh");
322
+ const curlBin = whichBinary("curl");
323
+ if (!shBin || !curlBin) {
324
+ p.log.warn("curl or sh not found. Cannot auto-install iii-engine.");
325
+ return {
326
+ ok: false,
327
+ binPath: null
328
+ };
329
+ }
330
+ const binDir = join(homedir(), ".local", "bin");
331
+ const binPath = join(binDir, "iii");
332
+ if (!runCommand(shBin, ["-c", [
333
+ `mkdir -p "${binDir}"`,
334
+ `curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
335
+ `chmod +x "${binPath}"`
336
+ ].join(" && ")], {
337
+ label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
338
+ optional: true
339
+ })) {
340
+ 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}.`);
341
+ return {
342
+ ok: false,
343
+ binPath: null
344
+ };
345
+ }
346
+ return {
347
+ ok: true,
348
+ binPath
349
+ };
350
+ }
222
351
  let startupFailure = null;
223
352
  function spawnEngineBackground(bin, spawnArgs, label) {
224
353
  vlog(`spawn: ${bin} ${spawnArgs.join(" ")}`);
@@ -231,6 +360,8 @@ function spawnEngineBackground(bin, spawnArgs, label) {
231
360
  ],
232
361
  windowsHide: true
233
362
  });
363
+ const isDocker = label.includes("Docker");
364
+ if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
234
365
  const stderrChunks = [];
235
366
  let stderrBytes = 0;
236
367
  const MAX_STDERR_CAPTURE = 16 * 1024;
@@ -244,27 +375,47 @@ function spawnEngineBackground(bin, spawnArgs, label) {
244
375
  if (code !== null && code !== 0 || code === null && signal !== null) {
245
376
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
246
377
  startupFailure = {
247
- kind: label.includes("Docker") ? "docker-crashed" : "engine-crashed",
378
+ kind: isDocker ? "docker-crashed" : "engine-crashed",
248
379
  stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
249
380
  binary: bin
250
381
  };
251
382
  vlog(`engine exited early: code=${code} signal=${signal}`);
252
383
  if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
384
+ if (!isDocker) clearEnginePidfile();
385
+ clearEngineState();
253
386
  }
254
387
  });
255
388
  child.unref();
256
389
  return child;
257
390
  }
391
+ function startIiiBin(iiiBin, configPath) {
392
+ const s = p.spinner();
393
+ s.start(`Starting iii-engine: ${iiiBin}`);
394
+ writeEngineState({
395
+ kind: "native",
396
+ configPath
397
+ });
398
+ spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
399
+ s.stop("iii-engine process started");
400
+ return true;
401
+ }
258
402
  async function startEngine() {
259
403
  const configPath = findIiiConfig();
260
404
  let iiiBin = whichBinary("iii");
261
405
  vlog(`iii binary: ${iiiBin ?? "(not on PATH)"}, config: ${configPath || "(not found)"}`);
262
- if (iiiBin && configPath) {
263
- const s = p.spinner();
264
- s.start(`Starting iii-engine: ${iiiBin}`);
265
- spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
266
- s.stop("iii-engine process started");
267
- return true;
406
+ if (iiiBin && configPath) return startIiiBin(iiiBin, configPath);
407
+ for (const iiiPath of fallbackIiiPaths()) if (existsSync(iiiPath)) {
408
+ const v = iiiBinVersion(iiiPath);
409
+ vlog(`fallback iii at ${iiiPath} reports version: ${v ?? "unknown"}`);
410
+ p.log.info(`Found iii at: ${iiiPath}${v ? ` (v${v})` : ""}`);
411
+ process.env["PATH"] = `${dirname(iiiPath)}${delimiter}${process.env["PATH"] ?? ""}`;
412
+ iiiBin = iiiPath;
413
+ break;
414
+ }
415
+ if (iiiBin && configPath) return startIiiBin(iiiBin, configPath);
416
+ if (!configPath) {
417
+ startupFailure = { kind: "no-engine" };
418
+ return false;
268
419
  }
269
420
  const dockerBin = whichBinary("docker");
270
421
  vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
@@ -274,9 +425,73 @@ async function startEngine() {
274
425
  join(process.cwd(), "docker-compose.yml")
275
426
  ].find((c) => existsSync(c));
276
427
  vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
277
- if (dockerBin && composeFile) {
428
+ const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
429
+ const interactive = !!process.stdin.isTTY && !process.env["CI"];
430
+ let choice;
431
+ if (dockerOptIn && dockerBin && composeFile) choice = "docker";
432
+ else if (!interactive) {
433
+ choice = "install";
434
+ p.log.info("Non-interactive environment detected — auto-installing iii-engine.");
435
+ } else {
436
+ p.log.warn(`iii-engine binary not found locally.`);
437
+ const options = [{
438
+ value: "install",
439
+ label: `Install iii v${IIPINNED_VERSION} to ~/.local/bin (~6MB, ~5s)`,
440
+ hint: "recommended"
441
+ }];
442
+ if (dockerBin && composeFile) options.push({
443
+ value: "docker",
444
+ label: "Use Docker compose",
445
+ hint: "advanced"
446
+ });
447
+ options.push({
448
+ value: "manual",
449
+ label: "Show manual install steps and exit"
450
+ });
451
+ const picked = await p.select({
452
+ message: "How would you like to start iii-engine?",
453
+ options,
454
+ initialValue: "install"
455
+ });
456
+ if (p.isCancel(picked)) {
457
+ startupFailure = { kind: "no-engine" };
458
+ return false;
459
+ }
460
+ choice = picked;
461
+ }
462
+ if (choice === "manual") {
463
+ startupFailure = { kind: "no-engine" };
464
+ return false;
465
+ }
466
+ if (choice === "install") {
467
+ const result = await runIiiInstaller();
468
+ if (result.ok && result.binPath) {
469
+ process.env["PATH"] = `${dirname(result.binPath)}${delimiter}${process.env["PATH"] ?? ""}`;
470
+ iiiBin = result.binPath;
471
+ return startIiiBin(iiiBin, configPath);
472
+ }
473
+ if (dockerBin && composeFile && interactive) {
474
+ const fallback = await p.confirm({
475
+ message: "Auto-install failed. Try Docker compose instead?",
476
+ initialValue: true
477
+ });
478
+ if (p.isCancel(fallback) || fallback !== true) {
479
+ startupFailure = { kind: "no-engine" };
480
+ return false;
481
+ }
482
+ choice = "docker";
483
+ } else {
484
+ startupFailure = { kind: "no-engine" };
485
+ return false;
486
+ }
487
+ }
488
+ if (choice === "docker" && dockerBin && composeFile) {
278
489
  const s = p.spinner();
279
490
  s.start("Starting iii-engine via Docker...");
491
+ writeEngineState({
492
+ kind: "docker",
493
+ composeFile
494
+ });
280
495
  spawnEngineBackground(dockerBin, [
281
496
  "compose",
282
497
  "-f",
@@ -287,21 +502,8 @@ async function startEngine() {
287
502
  s.stop("Docker compose started");
288
503
  return true;
289
504
  }
290
- for (const iiiPath of fallbackIiiPaths()) if (existsSync(iiiPath)) {
291
- p.log.info(`Found iii at: ${iiiPath}`);
292
- process.env["PATH"] = `${dirname(iiiPath)}${delimiter}${process.env["PATH"] ?? ""}`;
293
- iiiBin = iiiPath;
294
- break;
295
- }
296
- if (iiiBin && configPath) {
297
- const s = p.spinner();
298
- s.start(`Starting iii-engine: ${iiiBin}`);
299
- spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
300
- s.stop("iii-engine process started");
301
- return true;
302
- }
303
- if (!iiiBin && (!dockerBin || !composeFile)) startupFailure = { kind: "no-engine" };
304
- else if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
505
+ if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
506
+ else startupFailure = { kind: "no-engine" };
305
507
  return false;
306
508
  }
307
509
  async function waitForEngine(timeoutMs) {
@@ -315,43 +517,34 @@ async function waitForEngine(timeoutMs) {
315
517
  function installInstructions() {
316
518
  const releaseUrl = iiiReleaseUrl();
317
519
  if (IS_WINDOWS) return [
318
- `agentmemory requires the \`iii-engine\` runtime, pinned to v${IIPINNED_VERSION}. Pick one:`,
520
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
319
521
  "",
320
522
  " A) Download the prebuilt Windows binary:",
321
523
  ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
322
- ` 2. Download iii-x86_64-pc-windows-msvc.zip`,
323
- " (or iii-aarch64-pc-windows-msvc.zip on ARM)",
324
- " 3. Extract iii.exe and either add its folder to PATH",
325
- " or move it to %USERPROFILE%\\.local\\bin\\iii.exe",
524
+ ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
525
+ " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
326
526
  " 4. Re-run: npx @agentmemory/agentmemory",
327
527
  "",
328
- " B) Docker Desktop:",
329
- " 1. Install Docker Desktop for Windows",
330
- ` 2. docker pull iiidev/iii:${IIPINNED_VERSION}`,
331
- " 3. Start Docker Desktop (engine must be running)",
332
- " 4. Re-run: npx @agentmemory/agentmemory",
528
+ ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
529
+ " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
530
+ "",
531
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
333
532
  "",
334
- "Or skip the engine entirely for standalone MCP:",
335
- " npx @agentmemory/agentmemory mcp"
533
+ "Docs: https://iii.dev/docs"
336
534
  ];
337
- 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}`;
535
+ 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}`;
338
536
  return [
339
- `agentmemory requires the \`iii-engine\` runtime, pinned to v${IIPINNED_VERSION}. Pick one:`,
537
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
340
538
  "",
341
539
  linuxInstall,
342
- ` (installs iii v${IIPINNED_VERSION} into ~/.local/bin/iii)`,
540
+ " Then re-run: npx @agentmemory/agentmemory",
343
541
  "",
344
- ` B) Docker: \`docker pull iiidev/iii:${IIPINNED_VERSION}\``,
542
+ ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
543
+ " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
345
544
  "",
346
- "Or skip the engine entirely for standalone MCP:",
347
- " npx @agentmemory/agentmemory mcp",
545
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
348
546
  "",
349
- "Docs: https://iii.dev/docs",
350
- `Why pinned: iii v0.11.6 introduces the new sandbox-everything model`,
351
- `(\`iii worker add\` registration). agentmemory still uses the older`,
352
- `iii-exec config-file worker model and needs a refactor before it`,
353
- `runs cleanly under the new engine. Override with`,
354
- `AGENTMEMORY_III_VERSION=<version> when you've migrated manually.`
547
+ "Docs: https://iii.dev/docs"
355
548
  ];
356
549
  }
357
550
  function portInUseDiagnostic(port) {
@@ -361,12 +554,12 @@ async function main() {
361
554
  p.intro("agentmemory");
362
555
  if (skipEngine) {
363
556
  p.log.info("Skipping engine check (--no-engine)");
364
- await import("./src-Cqsy23f_.mjs");
557
+ await import("./src-BBI-ah3h.mjs");
365
558
  return;
366
559
  }
367
560
  if (await isEngineRunning()) {
368
561
  p.log.success("iii-engine is running");
369
- await import("./src-Cqsy23f_.mjs");
562
+ await import("./src-BBI-ah3h.mjs");
370
563
  return;
371
564
  }
372
565
  if (!await startEngine()) {
@@ -410,7 +603,7 @@ async function main() {
410
603
  process.exit(1);
411
604
  }
412
605
  s.stop("iii-engine is ready");
413
- await import("./src-Cqsy23f_.mjs");
606
+ await import("./src-BBI-ah3h.mjs");
414
607
  }
415
608
  async function apiFetch(base, path, timeoutMs = 5e3) {
416
609
  try {
@@ -732,6 +925,50 @@ async function runDemoSearch(base, query) {
732
925
  topTitle: items[0]?.title ?? "(no results)"
733
926
  };
734
927
  }
928
+ function findEnvExample() {
929
+ const candidates = [
930
+ join(__dirname, "..", ".env.example"),
931
+ join(__dirname, ".env.example"),
932
+ join(process.cwd(), ".env.example")
933
+ ];
934
+ for (const c of candidates) if (existsSync(c)) return c;
935
+ return null;
936
+ }
937
+ async function runInit() {
938
+ p.intro("agentmemory init");
939
+ const target = join(homedir(), ".agentmemory", ".env");
940
+ const template = findEnvExample();
941
+ if (!template) {
942
+ p.log.error("Could not locate .env.example in the package. Re-install with: npm i -g @agentmemory/agentmemory");
943
+ process.exit(1);
944
+ }
945
+ const dir = dirname(target);
946
+ const { mkdir, copyFile } = await import("node:fs/promises");
947
+ const { constants: fsConstants } = await import("node:fs");
948
+ try {
949
+ await mkdir(dir, { recursive: true });
950
+ await copyFile(template, target, fsConstants.COPYFILE_EXCL);
951
+ } catch (err) {
952
+ if (err?.code === "EEXIST") {
953
+ p.log.warn(`${target} already exists — leaving it untouched.`);
954
+ p.log.info(`Compare against the latest template: diff ${target} ${template}`);
955
+ p.outro("Nothing changed.");
956
+ return;
957
+ }
958
+ p.log.error(`Failed to copy template: ${err instanceof Error ? err.message : String(err)}`);
959
+ process.exit(1);
960
+ }
961
+ p.log.success(`Wrote ${target}`);
962
+ p.note([
963
+ "All keys are commented out by default. Uncomment the ones you want.",
964
+ "",
965
+ "Common next steps:",
966
+ " 1. Pick an LLM provider key (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY / etc.)",
967
+ " 2. Run `npx @agentmemory/agentmemory doctor` to verify the daemon sees them",
968
+ " 3. Run `npx @agentmemory/agentmemory` to start the worker"
969
+ ].join("\n"), "Next steps");
970
+ p.outro(`Edit ${target} and you're set.`);
971
+ }
735
972
  async function runDemo() {
736
973
  const port = getRestPort();
737
974
  const base = `http://localhost:${port}`;
@@ -827,36 +1064,16 @@ async function runUpgrade() {
827
1064
  });
828
1065
  } else p.log.warn("No package manager found (pnpm/npm). Skipping JS dependency upgrade.");
829
1066
  else p.log.warn("No package.json in current directory. Skipping JS dependency upgrade.");
830
- const shBin = whichBinary("sh");
831
- const curlBin = whichBinary("curl");
832
- if (shBin && curlBin) {
833
- const upgradeEngine = await p.confirm({
834
- message: "Re-run the iii-engine install script (curl | sh)?",
835
- initialValue: true
836
- });
837
- if (p.isCancel(upgradeEngine)) {
838
- p.cancel("Cancelled.");
839
- return process.exit(0);
840
- }
841
- if (upgradeEngine === true) {
842
- const releaseUrl = iiiReleaseUrl();
843
- const asset = iiiReleaseAsset();
844
- const isZipAsset = asset?.endsWith(".zip") === true;
845
- 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}.`);
846
- 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}`);
847
- else {
848
- const binDir = join(homedir(), ".local", "bin");
849
- if (!runCommand(shBin, ["-c", [
850
- `mkdir -p "${binDir}"`,
851
- `curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
852
- `chmod +x "${binDir}/iii"`
853
- ].join(" && ")], {
854
- label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
855
- optional: true
856
- })) 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}.`);
857
- }
858
- } else p.log.info("Skipped iii-engine installer.");
859
- } else p.log.warn("curl or sh not found. Skipping iii-engine installer.");
1067
+ const upgradeEngine = await p.confirm({
1068
+ message: "Re-run the iii-engine install script (curl | sh)?",
1069
+ initialValue: true
1070
+ });
1071
+ if (p.isCancel(upgradeEngine)) {
1072
+ p.cancel("Cancelled.");
1073
+ return process.exit(0);
1074
+ }
1075
+ if (upgradeEngine === true) await runIiiInstaller();
1076
+ else p.log.info("Skipped iii-engine installer.");
860
1077
  if (dockerBin) runCommand(dockerBin, ["pull", `iiidev/iii:${IIPINNED_VERSION}`], {
861
1078
  label: `Pulling iii Docker image v${IIPINNED_VERSION} (pinned)`,
862
1079
  optional: true
@@ -871,8 +1088,154 @@ async function runUpgrade() {
871
1088
  " 3) restart agentmemory process"
872
1089
  ].join("\n"), "agentmemory upgrade");
873
1090
  }
1091
+ function pidAlive(pid) {
1092
+ try {
1093
+ process.kill(pid, 0);
1094
+ return true;
1095
+ } catch (err) {
1096
+ return err?.code === "EPERM";
1097
+ }
1098
+ }
1099
+ async function signalAndWait(pid, initialSignal, timeoutMs) {
1100
+ try {
1101
+ process.kill(pid, initialSignal);
1102
+ } catch (err) {
1103
+ const code = err?.code;
1104
+ if (code === "ESRCH") return true;
1105
+ if (code === "EPERM") {
1106
+ p.log.warn(`No permission to signal pid ${pid}. Try: kill ${pid}`);
1107
+ return false;
1108
+ }
1109
+ vlog(`${initialSignal} ${pid}: ${err instanceof Error ? err.message : String(err)}`);
1110
+ return false;
1111
+ }
1112
+ const deadline = Date.now() + timeoutMs;
1113
+ while (Date.now() < deadline) {
1114
+ if (!pidAlive(pid)) return true;
1115
+ await new Promise((r) => setTimeout(r, 200));
1116
+ }
1117
+ if (!pidAlive(pid)) return true;
1118
+ try {
1119
+ process.kill(pid, "SIGKILL");
1120
+ } catch (err) {
1121
+ if (err?.code === "ESRCH") return true;
1122
+ vlog(`SIGKILL ${pid}: ${err instanceof Error ? err.message : String(err)}`);
1123
+ return false;
1124
+ }
1125
+ await new Promise((r) => setTimeout(r, 200));
1126
+ return !pidAlive(pid);
1127
+ }
1128
+ function findEnginePidsByPort(port) {
1129
+ if (IS_WINDOWS) return [];
1130
+ const lsof = whichBinary("lsof");
1131
+ if (!lsof) return [];
1132
+ const selfPid = process.pid;
1133
+ try {
1134
+ return execFileSync(lsof, [
1135
+ "-i",
1136
+ `:${port}`,
1137
+ "-sTCP:LISTEN",
1138
+ "-t"
1139
+ ], {
1140
+ encoding: "utf-8",
1141
+ stdio: [
1142
+ "ignore",
1143
+ "pipe",
1144
+ "ignore"
1145
+ ]
1146
+ }).split(/\s+/).map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n) && n > 0 && n !== selfPid);
1147
+ } catch (err) {
1148
+ vlog(`lsof :${port}: ${err instanceof Error ? err.message : String(err)}`);
1149
+ return [];
1150
+ }
1151
+ }
1152
+ async function stopDockerEngine(composeFile, port) {
1153
+ const dockerBin = whichBinary("docker");
1154
+ if (!dockerBin) {
1155
+ 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`);
1156
+ process.exit(1);
1157
+ }
1158
+ if (!existsSync(composeFile)) {
1159
+ 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)`);
1160
+ process.exit(1);
1161
+ }
1162
+ const ok = runCommand(dockerBin, [
1163
+ "compose",
1164
+ "-f",
1165
+ composeFile,
1166
+ "down"
1167
+ ], { label: `docker compose -f ${composeFile} down` });
1168
+ clearEnginePidfile();
1169
+ clearEngineState();
1170
+ if (!ok) {
1171
+ p.log.error(`docker compose down failed. The engine may still be running on :${port}. Inspect with:\n docker compose -f ${composeFile} ps`);
1172
+ process.exit(1);
1173
+ }
1174
+ p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
1175
+ }
1176
+ async function runStop() {
1177
+ p.intro("agentmemory stop");
1178
+ const port = getRestPort();
1179
+ const state = readEngineState();
1180
+ const running = await isEngineRunning();
1181
+ if (state?.kind === "docker") {
1182
+ if (!running) {
1183
+ p.log.info(`No engine responding on port ${port}.`);
1184
+ clearEnginePidfile();
1185
+ clearEngineState();
1186
+ p.outro("Nothing to stop.");
1187
+ return;
1188
+ }
1189
+ await stopDockerEngine(state.composeFile, port);
1190
+ return;
1191
+ }
1192
+ const portPids = findEnginePidsByPort(port);
1193
+ const pidfilePid = readEnginePidfile();
1194
+ if (!running) {
1195
+ if (portPids.length === 0 && pidfilePid === null) {
1196
+ clearEnginePidfile();
1197
+ clearEngineState();
1198
+ p.outro("Nothing to stop.");
1199
+ return;
1200
+ }
1201
+ const survivors = new Set(portPids);
1202
+ if (pidfilePid) survivors.add(pidfilePid);
1203
+ p.log.warn(`Engine not responding on :${port}, but ${survivors.size} process(es) still hold the port or pidfile: ${[...survivors].join(", ")}`);
1204
+ 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}`);
1205
+ process.exit(1);
1206
+ }
1207
+ if (!state) {
1208
+ 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.`);
1211
+ process.exit(1);
1212
+ }
1213
+ }
1214
+ const candidates = /* @__PURE__ */ new Set();
1215
+ if (pidfilePid) candidates.add(pidfilePid);
1216
+ for (const pid of portPids) candidates.add(pid);
1217
+ if (candidates.size === 0) {
1218
+ p.log.error(`Could not locate engine process. Try:\n ${IS_WINDOWS ? "netstat -ano | findstr :" + port : "lsof -i :" + port + " -t | xargs kill -9"}`);
1219
+ process.exit(1);
1220
+ }
1221
+ let allStopped = true;
1222
+ for (const pid of candidates) {
1223
+ const s = p.spinner();
1224
+ s.start(`Stopping iii-engine (pid ${pid})...`);
1225
+ const ok = await signalAndWait(pid, "SIGTERM", 3e3);
1226
+ s.stop(ok ? `Stopped pid ${pid}` : `Failed to stop pid ${pid}`);
1227
+ if (!ok) allStopped = false;
1228
+ }
1229
+ clearEnginePidfile();
1230
+ clearEngineState();
1231
+ if (!allStopped) {
1232
+ p.log.error("One or more engine processes survived SIGKILL. Investigate with `ps`.");
1233
+ process.exit(1);
1234
+ }
1235
+ p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
1236
+ }
874
1237
  async function runMcp() {
875
- await import("./standalone-DNt6O3zG.mjs");
1238
+ await import("./standalone-Cf5sp0XM.mjs");
876
1239
  }
877
1240
  async function runImportJsonl() {
878
1241
  const VALUE_FLAGS = new Set(["--port", "--tools"]);
@@ -977,10 +1340,12 @@ async function runImportJsonl() {
977
1340
  }
978
1341
  }
979
1342
  ({
1343
+ init: runInit,
980
1344
  status: runStatus,
981
1345
  doctor: runDoctor,
982
1346
  demo: runDemo,
983
1347
  upgrade: runUpgrade,
1348
+ stop: runStop,
984
1349
  mcp: runMcp,
985
1350
  "import-jsonl": runImportJsonl
986
1351
  }[args[0] ?? ""] ?? main)().catch((err) => {