@agentmemory/agentmemory 0.9.13 → 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";
@@ -115,6 +115,7 @@ Commands:
115
115
  doctor Run diagnostic checks (server, flags, graph, providers)
116
116
  demo Seed sample sessions and show recall in action
117
117
  upgrade Upgrade local deps + iii runtime (best effort)
118
+ stop Stop the running iii-engine started by this CLI
118
119
  mcp Start standalone MCP server (no engine required)
119
120
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
120
121
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
@@ -128,8 +129,11 @@ Options:
128
129
  --port <N> Override REST port (default: 3111)
129
130
 
130
131
  Environment:
131
- AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
132
- 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}).
133
137
 
134
138
  Quick start:
135
139
  npx @agentmemory/agentmemory # start with local iii-engine or Docker
@@ -220,6 +224,130 @@ function fallbackIiiPaths() {
220
224
  if (!home) return ["/usr/local/bin/iii"];
221
225
  return [join(home, ".local", "bin", "iii"), "/usr/local/bin/iii"];
222
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
+ }
223
351
  let startupFailure = null;
224
352
  function spawnEngineBackground(bin, spawnArgs, label) {
225
353
  vlog(`spawn: ${bin} ${spawnArgs.join(" ")}`);
@@ -232,6 +360,8 @@ function spawnEngineBackground(bin, spawnArgs, label) {
232
360
  ],
233
361
  windowsHide: true
234
362
  });
363
+ const isDocker = label.includes("Docker");
364
+ if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
235
365
  const stderrChunks = [];
236
366
  let stderrBytes = 0;
237
367
  const MAX_STDERR_CAPTURE = 16 * 1024;
@@ -245,27 +375,47 @@ function spawnEngineBackground(bin, spawnArgs, label) {
245
375
  if (code !== null && code !== 0 || code === null && signal !== null) {
246
376
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
247
377
  startupFailure = {
248
- kind: label.includes("Docker") ? "docker-crashed" : "engine-crashed",
378
+ kind: isDocker ? "docker-crashed" : "engine-crashed",
249
379
  stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
250
380
  binary: bin
251
381
  };
252
382
  vlog(`engine exited early: code=${code} signal=${signal}`);
253
383
  if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
384
+ if (!isDocker) clearEnginePidfile();
385
+ clearEngineState();
254
386
  }
255
387
  });
256
388
  child.unref();
257
389
  return child;
258
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
+ }
259
402
  async function startEngine() {
260
403
  const configPath = findIiiConfig();
261
404
  let iiiBin = whichBinary("iii");
262
405
  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;
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;
269
419
  }
270
420
  const dockerBin = whichBinary("docker");
271
421
  vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
@@ -275,9 +425,73 @@ async function startEngine() {
275
425
  join(process.cwd(), "docker-compose.yml")
276
426
  ].find((c) => existsSync(c));
277
427
  vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
278
- 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) {
279
489
  const s = p.spinner();
280
490
  s.start("Starting iii-engine via Docker...");
491
+ writeEngineState({
492
+ kind: "docker",
493
+ composeFile
494
+ });
281
495
  spawnEngineBackground(dockerBin, [
282
496
  "compose",
283
497
  "-f",
@@ -288,21 +502,8 @@ async function startEngine() {
288
502
  s.stop("Docker compose started");
289
503
  return true;
290
504
  }
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" };
505
+ if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
506
+ else startupFailure = { kind: "no-engine" };
306
507
  return false;
307
508
  }
308
509
  async function waitForEngine(timeoutMs) {
@@ -316,43 +517,34 @@ async function waitForEngine(timeoutMs) {
316
517
  function installInstructions() {
317
518
  const releaseUrl = iiiReleaseUrl();
318
519
  if (IS_WINDOWS) return [
319
- `agentmemory requires the \`iii-engine\` runtime, pinned to v${IIPINNED_VERSION}. Pick one:`,
520
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
320
521
  "",
321
522
  " A) Download the prebuilt Windows binary:",
322
523
  ` 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",
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)",
327
526
  " 4. Re-run: npx @agentmemory/agentmemory",
328
527
  "",
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",
528
+ ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
529
+ " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
334
530
  "",
335
- "Or skip the engine entirely for standalone MCP:",
336
- " npx @agentmemory/agentmemory mcp"
531
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
532
+ "",
533
+ "Docs: https://iii.dev/docs"
337
534
  ];
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}`;
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}`;
339
536
  return [
340
- `agentmemory requires the \`iii-engine\` runtime, pinned to v${IIPINNED_VERSION}. Pick one:`,
537
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
341
538
  "",
342
539
  linuxInstall,
343
- ` (installs iii v${IIPINNED_VERSION} into ~/.local/bin/iii)`,
540
+ " Then re-run: npx @agentmemory/agentmemory",
344
541
  "",
345
- ` 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",
346
544
  "",
347
- "Or skip the engine entirely for standalone MCP:",
348
- " npx @agentmemory/agentmemory mcp",
545
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
349
546
  "",
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.`
547
+ "Docs: https://iii.dev/docs"
356
548
  ];
357
549
  }
358
550
  function portInUseDiagnostic(port) {
@@ -362,12 +554,12 @@ async function main() {
362
554
  p.intro("agentmemory");
363
555
  if (skipEngine) {
364
556
  p.log.info("Skipping engine check (--no-engine)");
365
- await import("./src-Ca9oX6Hq.mjs");
557
+ await import("./src-BBI-ah3h.mjs");
366
558
  return;
367
559
  }
368
560
  if (await isEngineRunning()) {
369
561
  p.log.success("iii-engine is running");
370
- await import("./src-Ca9oX6Hq.mjs");
562
+ await import("./src-BBI-ah3h.mjs");
371
563
  return;
372
564
  }
373
565
  if (!await startEngine()) {
@@ -411,7 +603,7 @@ async function main() {
411
603
  process.exit(1);
412
604
  }
413
605
  s.stop("iii-engine is ready");
414
- await import("./src-Ca9oX6Hq.mjs");
606
+ await import("./src-BBI-ah3h.mjs");
415
607
  }
416
608
  async function apiFetch(base, path, timeoutMs = 5e3) {
417
609
  try {
@@ -872,36 +1064,16 @@ async function runUpgrade() {
872
1064
  });
873
1065
  } else p.log.warn("No package manager found (pnpm/npm). Skipping JS dependency upgrade.");
874
1066
  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.");
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.");
905
1077
  if (dockerBin) runCommand(dockerBin, ["pull", `iiidev/iii:${IIPINNED_VERSION}`], {
906
1078
  label: `Pulling iii Docker image v${IIPINNED_VERSION} (pinned)`,
907
1079
  optional: true
@@ -916,8 +1088,154 @@ async function runUpgrade() {
916
1088
  " 3) restart agentmemory process"
917
1089
  ].join("\n"), "agentmemory upgrade");
918
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
+ }
919
1237
  async function runMcp() {
920
- await import("./standalone-BpbiNqr9.mjs");
1238
+ await import("./standalone-Cf5sp0XM.mjs");
921
1239
  }
922
1240
  async function runImportJsonl() {
923
1241
  const VALUE_FLAGS = new Set(["--port", "--tools"]);
@@ -1027,6 +1345,7 @@ async function runImportJsonl() {
1027
1345
  doctor: runDoctor,
1028
1346
  demo: runDemo,
1029
1347
  upgrade: runUpgrade,
1348
+ stop: runStop,
1030
1349
  mcp: runMcp,
1031
1350
  "import-jsonl": runImportJsonl
1032
1351
  }[args[0] ?? ""] ?? main)().catch((err) => {