@agentmemory/agentmemory 0.9.28 → 0.9.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.env.example +98 -28
  2. package/AGENTS.md +19 -26
  3. package/README.md +619 -274
  4. package/dist/.env.example +98 -28
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +1328 -244
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{connect-JnsJXc68.mjs → connect-B4_ca6BD.mjs} +619 -70
  9. package/dist/connect-B4_ca6BD.mjs.map +1 -0
  10. package/dist/docker-compose.yml +42 -16
  11. package/dist/hooks/_capture.d.mts +22 -0
  12. package/dist/hooks/_capture.d.mts.map +1 -0
  13. package/dist/hooks/_capture.mjs +908 -0
  14. package/dist/hooks/_capture.mjs.map +1 -0
  15. package/dist/hooks/antigravity-bridge.d.mts +8 -0
  16. package/dist/hooks/antigravity-bridge.d.mts.map +1 -0
  17. package/dist/hooks/antigravity-bridge.mjs +123 -0
  18. package/dist/hooks/antigravity-bridge.mjs.map +1 -0
  19. package/dist/hooks/notification.mjs +34 -27
  20. package/dist/hooks/notification.mjs.map +1 -1
  21. package/dist/hooks/post-commit.mjs +84 -2
  22. package/dist/hooks/post-commit.mjs.map +1 -1
  23. package/dist/hooks/post-tool-failure.mjs +71 -26
  24. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  25. package/dist/hooks/post-tool-use.mjs +99 -36
  26. package/dist/hooks/post-tool-use.mjs.map +1 -1
  27. package/dist/hooks/pre-compact.mjs +95 -5
  28. package/dist/hooks/pre-compact.mjs.map +1 -1
  29. package/dist/hooks/pre-tool-use.mjs +72 -2
  30. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  31. package/dist/hooks/prompt-submit.mjs +25 -22
  32. package/dist/hooks/prompt-submit.mjs.map +1 -1
  33. package/dist/hooks/session-end.mjs +93 -24
  34. package/dist/hooks/session-end.mjs.map +1 -1
  35. package/dist/hooks/session-start.mjs +101 -6
  36. package/dist/hooks/session-start.mjs.map +1 -1
  37. package/dist/hooks/stop.mjs +72 -8
  38. package/dist/hooks/stop.mjs.map +1 -1
  39. package/dist/hooks/subagent-start.mjs +28 -26
  40. package/dist/hooks/subagent-start.mjs.map +1 -1
  41. package/dist/hooks/subagent-stop.mjs +29 -26
  42. package/dist/hooks/subagent-stop.mjs.map +1 -1
  43. package/dist/hooks/task-completed.mjs +31 -28
  44. package/dist/hooks/task-completed.mjs.map +1 -1
  45. package/dist/iii-config.docker.yaml +10 -10
  46. package/dist/iii-config.yaml +3 -1
  47. package/dist/{image-refs-BKLmp_sD.mjs → image-refs-BaT-9oMU.mjs} +2 -2
  48. package/dist/image-refs-BaT-9oMU.mjs.map +1 -0
  49. package/dist/{image-refs-C2YU7BIm.mjs → image-refs-T_urbwHM.mjs} +3 -3
  50. package/dist/image-refs-T_urbwHM.mjs.map +1 -0
  51. package/dist/{keyed-mutex-DTHtcZrt.mjs → image-store-BsN4vyjr.mjs} +2 -14
  52. package/dist/image-store-BsN4vyjr.mjs.map +1 -0
  53. package/dist/index.mjs +14021 -6494
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/keyed-mutex-DchXsyfP.mjs +221 -0
  56. package/dist/keyed-mutex-DchXsyfP.mjs.map +1 -0
  57. package/dist/secret-store-BnY3A50C.mjs +2348 -0
  58. package/dist/secret-store-BnY3A50C.mjs.map +1 -0
  59. package/dist/{src-CzgoepGU.mjs → src-Jsq9LCeH.mjs} +7349 -2940
  60. package/dist/src-Jsq9LCeH.mjs.map +1 -0
  61. package/dist/{standalone-YsZFFQBM.mjs → standalone-DKCdrf1d.mjs} +62 -24
  62. package/dist/standalone-DKCdrf1d.mjs.map +1 -0
  63. package/dist/standalone.d.mts.map +1 -1
  64. package/dist/standalone.mjs +298 -27
  65. package/dist/standalone.mjs.map +1 -1
  66. package/dist/status-IRz7ta8-.mjs +2344 -0
  67. package/dist/status-IRz7ta8-.mjs.map +1 -0
  68. package/dist/viewer/favicon.svg +35 -1
  69. package/dist/viewer/index.html +7428 -2608
  70. package/docker-compose.yml +42 -16
  71. package/iii-config.docker.yaml +10 -10
  72. package/iii-config.yaml +3 -1
  73. package/integrations/pi/README.md +77 -0
  74. package/integrations/pi/index.ts +402 -0
  75. package/integrations/pi/package.json +32 -0
  76. package/integrations/pi/security.ts +87 -0
  77. package/package.json +15 -7
  78. package/plugin/.claude-plugin/plugin.json +2 -2
  79. package/plugin/.codex-plugin/plugin.json +26 -4
  80. package/plugin/.devin-plugin/plugin.json +32 -0
  81. package/plugin/.mcp.codex.json +12 -0
  82. package/plugin/.mcp.copilot.json +2 -7
  83. package/plugin/.mcp.json +2 -7
  84. package/plugin/assets/icon.svg +35 -0
  85. package/plugin/cursor/hooks.json +41 -0
  86. package/plugin/cursor/mcp.json +12 -0
  87. package/plugin/hooks/hooks.antigravity.json +43 -0
  88. package/plugin/hooks/hooks.copilot.json +11 -11
  89. package/plugin/hooks/hooks.devin.json +67 -0
  90. package/plugin/hooks/hooks.droid.json +55 -0
  91. package/plugin/opencode/README.md +267 -31
  92. package/plugin/opencode/agentmemory-capture.ts +644 -15
  93. package/plugin/opencode/plugin.json +1 -1
  94. package/plugin/plugin.json +2 -2
  95. package/plugin/scripts/_capture.d.mts +22 -0
  96. package/plugin/scripts/_capture.d.mts.map +1 -0
  97. package/plugin/scripts/_capture.mjs +908 -0
  98. package/plugin/scripts/_capture.mjs.map +1 -0
  99. package/plugin/scripts/antigravity-bridge.d.mts +8 -0
  100. package/plugin/scripts/antigravity-bridge.d.mts.map +1 -0
  101. package/plugin/scripts/antigravity-bridge.mjs +123 -0
  102. package/plugin/scripts/antigravity-bridge.mjs.map +1 -0
  103. package/plugin/scripts/notification.mjs +34 -27
  104. package/plugin/scripts/notification.mjs.map +1 -1
  105. package/plugin/scripts/plugin-bridge.mjs +378 -0
  106. package/plugin/scripts/post-commit.mjs +84 -2
  107. package/plugin/scripts/post-commit.mjs.map +1 -1
  108. package/plugin/scripts/post-tool-failure.mjs +71 -26
  109. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  110. package/plugin/scripts/post-tool-use.mjs +99 -36
  111. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  112. package/plugin/scripts/pre-compact.mjs +95 -5
  113. package/plugin/scripts/pre-compact.mjs.map +1 -1
  114. package/plugin/scripts/pre-tool-use.mjs +72 -2
  115. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  116. package/plugin/scripts/prompt-submit.mjs +25 -22
  117. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  118. package/plugin/scripts/session-end.mjs +93 -24
  119. package/plugin/scripts/session-end.mjs.map +1 -1
  120. package/plugin/scripts/session-start.mjs +101 -6
  121. package/plugin/scripts/session-start.mjs.map +1 -1
  122. package/plugin/scripts/stop.mjs +72 -8
  123. package/plugin/scripts/stop.mjs.map +1 -1
  124. package/plugin/scripts/subagent-start.mjs +28 -26
  125. package/plugin/scripts/subagent-start.mjs.map +1 -1
  126. package/plugin/scripts/subagent-stop.mjs +29 -26
  127. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  128. package/plugin/scripts/task-completed.mjs +31 -28
  129. package/plugin/scripts/task-completed.mjs.map +1 -1
  130. package/plugin/skills/_shared/TROUBLESHOOTING.md +22 -6
  131. package/plugin/skills/agentmemory-agents/REFERENCE.md +8 -5
  132. package/plugin/skills/agentmemory-config/REFERENCE.md +52 -1
  133. package/plugin/skills/agentmemory-config/SKILL.md +44 -7
  134. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +4 -3
  135. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +23 -2
  136. package/plugin/skills/forget/SKILL.md +4 -1
  137. package/plugin/skills/handoff/SKILL.md +4 -2
  138. package/plugin/skills/lesson/SKILL.md +64 -0
  139. package/plugin/skills/memory-discipline/SKILL.md +65 -0
  140. package/plugin/skills/recall/SKILL.md +4 -1
  141. package/plugin/skills/remember/SKILL.md +6 -1
  142. package/dist/config-ChC9EtgV.mjs +0 -259
  143. package/dist/config-ChC9EtgV.mjs.map +0 -1
  144. package/dist/connect-JnsJXc68.mjs.map +0 -1
  145. package/dist/image-refs-BKLmp_sD.mjs.map +0 -1
  146. package/dist/image-refs-C2YU7BIm.mjs.map +0 -1
  147. package/dist/keyed-mutex-DTHtcZrt.mjs.map +0 -1
  148. package/dist/logger-BEB7pCI9.mjs +0 -43
  149. package/dist/logger-BEB7pCI9.mjs.map +0 -1
  150. package/dist/schema-CNiCWzV9.mjs +0 -78
  151. package/dist/schema-CNiCWzV9.mjs.map +0 -1
  152. package/dist/src-CzgoepGU.mjs.map +0 -1
  153. package/dist/standalone-YsZFFQBM.mjs.map +0 -1
  154. package/dist/tools-registry-D3NEoe5m.mjs +0 -1115
  155. package/dist/tools-registry-D3NEoe5m.mjs.map +0 -1
  156. package/plugin/scripts/diagnostics.d.mts +0 -17
  157. package/plugin/scripts/diagnostics.d.mts.map +0 -1
  158. package/plugin/scripts/diagnostics.mjs.map +0 -1
package/dist/cli.mjs CHANGED
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { a as runAdapter, i as resolveAdapter, r as knownAgents, t as ADAPTERS } from "./connect-JnsJXc68.mjs";
3
- import { i as generateId } from "./schema-CNiCWzV9.mjs";
4
- import { r as setBootVerbose } from "./logger-BEB7pCI9.mjs";
5
- import { i as VERSION, n as getAllTools, t as ESSENTIAL_TOOLS } from "./tools-registry-D3NEoe5m.mjs";
2
+ import { a as runAdapter, i as resolveAdapter, r as knownAgents, t as ADAPTERS } from "./connect-B4_ca6BD.mjs";
3
+ import { o as generateId } from "./keyed-mutex-DchXsyfP.mjs";
4
+ import { I as hydrateProcessEnvFromFile, M as getStateSaveIntervalMs, O as getRedisUrl, _ as VERSION, at as isBundledConfig, ct as resolveEngineCwd, dt as runtimeConfigPath, g as III_PINNED_VERSION, gt as renderEngineConfig, ht as engineStateConfigPaths, i as resolveClientSecret, it as engineChildEnv, j as getStateBackend, lt as resolveLaunchRenderFailure, m as getAllTools, mt as engineFlushWaitMs, nt as dockerComposeArgs, ot as legacyDataMigrations, p as ESSENTIAL_TOOLS, pt as clearPersistedBuiltinConfig, rt as dockerProjectName, st as redactCredentialUrls, t as bearerHeaders, tt as setBootVerbose, ut as rewriteBundledConfig } from "./secret-store-BnY3A50C.mjs";
5
+ import { A as SHUTDOWN_HARD_EXIT_MS, C as spoolSummary, M as runtimeMetadataPath, O as resolveDataDir, _ as drainSpool, b as retainSent, t as CURL_AUTH_HEADER, v as parseSentMark } from "./status-IRz7ta8-.mjs";
6
6
  import { execFileSync, spawn, spawnSync } from "node:child_process";
7
- import { appendFileSync, closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
8
- import { delimiter, dirname, join } from "node:path";
7
+ import { appendFileSync, closeSync, constants, cpSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
8
+ import { delimiter, dirname, join, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { homedir, platform } from "node:os";
11
11
  import * as p from "@clack/prompts";
12
12
  import pc from "picocolors";
13
+ import { createHash } from "node:crypto";
13
14
  import { copyFile, mkdir } from "node:fs/promises";
14
15
  //#region src/cli/doctor-diagnostics.ts
15
16
  /** Common placeholder values shipped in .env.example. */
@@ -225,11 +226,14 @@ function dryRunPlan(ctx, results) {
225
226
  }
226
227
  //#endregion
227
228
  //#region src/cli/remove-plan.ts
228
- function pidfilePath(home) {
229
- return join(home, ".agentmemory", "iii.pid");
229
+ function pidfilePath(runtimeDir) {
230
+ return join(runtimeDir, "iii.pid");
230
231
  }
231
- function enginePath(home) {
232
- return join(home, ".agentmemory", "engine-state.json");
232
+ function workerPidfilePath$1(runtimeDir) {
233
+ return join(runtimeDir, "worker.pid");
234
+ }
235
+ function enginePath(runtimeDir) {
236
+ return join(runtimeDir, "engine-state.json");
233
237
  }
234
238
  function envPath(home) {
235
239
  return join(home, ".agentmemory", ".env");
@@ -240,9 +244,6 @@ function preferencesPath(home) {
240
244
  function backupsDir(home) {
241
245
  return join(home, ".agentmemory", "backups");
242
246
  }
243
- function dataDir(home) {
244
- return join(home, ".agentmemory", "data");
245
- }
246
247
  function iiiBinFile() {
247
248
  return process.platform === "win32" ? "iii.exe" : "iii";
248
249
  }
@@ -274,31 +275,39 @@ function pathExists(path) {
274
275
  * the structure stable for tests.
275
276
  */
276
277
  function buildRemovePlan(ctx, options) {
277
- const { home, pinnedVersion, localBinIiiVersion, connectManifest } = ctx;
278
+ const { home, runtimeDir, dataDir, pinnedVersion, localBinIiiVersion, connectManifest } = ctx;
278
279
  const plan = [];
279
280
  plan.push({
280
281
  id: "stop-engine",
281
282
  description: "Stop running iii-engine (if any) cleanly",
282
283
  path: null,
283
284
  alwaysAsk: false,
284
- applicable: pathExists(pidfilePath(home)) || pathExists(enginePath(home)),
285
+ applicable: pathExists(pidfilePath(runtimeDir)) || pathExists(workerPidfilePath$1(runtimeDir)) || pathExists(enginePath(runtimeDir)),
285
286
  sizeBytes: -1
286
287
  });
287
288
  plan.push({
288
289
  id: "pidfile",
289
290
  description: "Delete pidfile",
290
- path: pidfilePath(home),
291
+ path: pidfilePath(runtimeDir),
291
292
  alwaysAsk: false,
292
- applicable: pathExists(pidfilePath(home)),
293
- sizeBytes: safeSize(pidfilePath(home))
293
+ applicable: pathExists(pidfilePath(runtimeDir)),
294
+ sizeBytes: safeSize(pidfilePath(runtimeDir))
295
+ });
296
+ plan.push({
297
+ id: "worker-pidfile",
298
+ description: "Delete worker pidfile",
299
+ path: workerPidfilePath$1(runtimeDir),
300
+ alwaysAsk: false,
301
+ applicable: pathExists(workerPidfilePath$1(runtimeDir)),
302
+ sizeBytes: safeSize(workerPidfilePath$1(runtimeDir))
294
303
  });
295
304
  plan.push({
296
305
  id: "engine-state",
297
306
  description: "Delete engine-state.json",
298
- path: enginePath(home),
307
+ path: enginePath(runtimeDir),
299
308
  alwaysAsk: false,
300
- applicable: pathExists(enginePath(home)),
301
- sizeBytes: safeSize(enginePath(home))
309
+ applicable: !options.preserveRuntimeState && pathExists(enginePath(runtimeDir)),
310
+ sizeBytes: safeSize(enginePath(runtimeDir))
302
311
  });
303
312
  plan.push({
304
313
  id: "env",
@@ -324,6 +333,14 @@ function buildRemovePlan(ctx, options) {
324
333
  applicable: !options.keepData && pathExists(backupsDir(home)),
325
334
  sizeBytes: -1
326
335
  });
336
+ plan.push({
337
+ id: "runtime-config",
338
+ description: "Delete generated iii-config.runtime.yaml",
339
+ path: runtimeConfigPath(dataDir),
340
+ alwaysAsk: false,
341
+ applicable: pathExists(runtimeConfigPath(dataDir)),
342
+ sizeBytes: safeSize(runtimeConfigPath(dataDir))
343
+ });
327
344
  if (connectManifest?.installed?.length) for (const entry of connectManifest.installed) plan.push({
328
345
  id: `connect:${entry.target}`,
329
346
  description: `Remove agent connection (${entry.agent ?? "unknown"})`,
@@ -355,10 +372,10 @@ function buildRemovePlan(ctx, options) {
355
372
  }
356
373
  plan.push({
357
374
  id: "data-dir",
358
- description: "Delete memory data directory (~/.agentmemory/data/) — will ask separately",
359
- path: dataDir(home),
375
+ description: `Delete memory data directory (${dataDir}) — will ask separately`,
376
+ path: dataDir,
360
377
  alwaysAsk: true,
361
- applicable: !options.keepData && pathExists(dataDir(home)),
378
+ applicable: !options.keepData && pathExists(dataDir),
362
379
  sizeBytes: -1
363
380
  });
364
381
  return plan;
@@ -377,6 +394,197 @@ function humanBytes(n) {
377
394
  return `${(n / (1024 * 1024)).toFixed(1)} MB`;
378
395
  }
379
396
  //#endregion
397
+ //#region src/cli/ready-hint.ts
398
+ const DEFAULT_REST_PORT = 3111;
399
+ function portFlagSuffix(restPort, instance) {
400
+ if (instance > 0 && restPort === DEFAULT_REST_PORT + instance * 100) return ` --instance ${instance}`;
401
+ return restPort === DEFAULT_REST_PORT ? "" : ` --port ${restPort}`;
402
+ }
403
+ const III_INSTALL_SPAWN_TIMEOUT_MS = 345 * 1e3;
404
+ const III_RELEASE_SHA256 = { "0.22.1": {
405
+ "iii-aarch64-apple-darwin.tar.gz": "2b309019b909a896cae874dc947e2cdf877b4f3c51dd026b79850af858517fa4",
406
+ "iii-x86_64-apple-darwin.tar.gz": "6d33940db2d3ad6a9aef9837aa5b89bf5c5e4acd0679e8178fec258af229d7e1",
407
+ "iii-x86_64-unknown-linux-gnu.tar.gz": "34d4bd2ec1873e6114bb836be7db9cb3f410d0d63353d240917f0bc9c30df13b",
408
+ "iii-aarch64-unknown-linux-gnu.tar.gz": "87efc60210c1580ef2e11b13c679a4dad91917a9d80f737c5ba7e3475b3ca413",
409
+ "iii-armv7-unknown-linux-gnueabihf.tar.gz": "af5dfc45149678c647e5adaca55c055428eebfa73d65884b052c30c98abcfd19",
410
+ "iii-x86_64-pc-windows-msvc.zip": "d862bf7cf1e864f3290c8a6fc17c308c6ecac097cb2666e4a739efc0040aae96",
411
+ "iii-aarch64-pc-windows-msvc.zip": "4777a48cee3fc1a742daee929e3d943df09e4ddd3357d27a33d31c9dc9e520ac"
412
+ } };
413
+ function expectedIiiSha256(version, asset, checksums = III_RELEASE_SHA256) {
414
+ return checksums[version]?.[asset] ?? null;
415
+ }
416
+ function sha256File(path) {
417
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
418
+ }
419
+ function curlTimeoutFlags() {
420
+ return `--connect-timeout 15 --max-time 300`;
421
+ }
422
+ function iiiArchivePath(binDir) {
423
+ return join(binDir, ".iii-download.tar.gz");
424
+ }
425
+ function iiiDownloadShellCommand() {
426
+ return `mkdir -p "$2" && rm -f "$3" && if curl -fsSL ${curlTimeoutFlags()} -o "$3" "$1"; then :; else code=$?; rm -f "$3"; exit $code; fi`;
427
+ }
428
+ function iiiExtractShellCommand() {
429
+ return "if tar -xzf \"$1\" -C \"$2\"; then rm -f \"$1\"; else code=$?; rm -f \"$1\"; exit $code; fi && chmod +x \"$3\"";
430
+ }
431
+ function iiiManualInstallCommand(releaseUrl, binDir, binPath, sha256) {
432
+ return `mkdir -p "${binDir}" && archive="$(mktemp "${binDir}/.iii-download.XXXXXX")" && { curl -fL --connect-timeout 15 -o "$archive" "${releaseUrl}"${sha256 ? ` && printf '%s %s\\n' "${sha256}" "$archive" | { sha256sum -c - 2>/dev/null || shasum -a 256 -c -; }` : ""} && tar -xzf "$archive" -C "${binDir}"; code=$?; rm -f "$archive"; (exit $code); } && chmod +x "${binPath}"`;
433
+ }
434
+ function describeInstallFailure(result) {
435
+ if (result.error?.code === "ETIMEDOUT") return `timed out after ${Math.round(III_INSTALL_SPAWN_TIMEOUT_MS / 1e3)} s`;
436
+ const output = (result.stderr || result.stdout || "").toString().trim();
437
+ if (output) return output;
438
+ if (result.status === 28) return `download timed out (no connection within 15 s or not finished within 300 s)`;
439
+ if (result.signal) return `installer stopped by ${result.signal}`;
440
+ if (result.error) return result.error.message;
441
+ return `installer exited with status ${result.status ?? "unknown"}`;
442
+ }
443
+ function installIiiArchive(opts) {
444
+ const expected = expectedIiiSha256(opts.version, opts.asset, opts.checksums);
445
+ if (!expected) return {
446
+ ok: false,
447
+ reason: "no-checksum",
448
+ detail: `no pinned SHA-256 for ${opts.asset} in iii v${opts.version}, so the download cannot be verified`
449
+ };
450
+ const run = (script, args) => spawnSync(opts.sh, [
451
+ "-c",
452
+ script,
453
+ "sh",
454
+ ...args
455
+ ], {
456
+ stdio: "pipe",
457
+ encoding: "utf-8",
458
+ timeout: III_INSTALL_SPAWN_TIMEOUT_MS,
459
+ env: opts.env ?? process.env
460
+ });
461
+ const archive = iiiArchivePath(opts.binDir);
462
+ const download = run(iiiDownloadShellCommand(), [
463
+ opts.releaseUrl,
464
+ opts.binDir,
465
+ archive
466
+ ]);
467
+ if (download.status !== 0) return {
468
+ ok: false,
469
+ reason: "download",
470
+ detail: describeInstallFailure(download)
471
+ };
472
+ let actual;
473
+ try {
474
+ actual = sha256File(archive);
475
+ } catch (err) {
476
+ rmSync(archive, { force: true });
477
+ return {
478
+ ok: false,
479
+ reason: "download",
480
+ detail: err instanceof Error ? err.message : String(err)
481
+ };
482
+ }
483
+ if (actual !== expected) {
484
+ rmSync(archive, { force: true });
485
+ return {
486
+ ok: false,
487
+ reason: "checksum",
488
+ detail: `SHA-256 mismatch for ${opts.asset}: expected ${expected}, got ${actual}. The download was deleted and nothing was installed`
489
+ };
490
+ }
491
+ const extract = run(iiiExtractShellCommand(), [
492
+ archive,
493
+ opts.binDir,
494
+ opts.binPath
495
+ ]);
496
+ if (extract.status !== 0) return {
497
+ ok: false,
498
+ reason: "extract",
499
+ detail: describeInstallFailure(extract)
500
+ };
501
+ return { ok: true };
502
+ }
503
+ //#endregion
504
+ //#region src/cli/startup-stderr.ts
505
+ function createStartupStderrCapture(maxBytes = 16 * 1024) {
506
+ const chunks = [];
507
+ let capturedBytes = 0;
508
+ return {
509
+ append(chunk) {
510
+ if (capturedBytes >= maxBytes) return;
511
+ const slice = chunk.subarray(0, maxBytes - capturedBytes);
512
+ chunks.push(slice);
513
+ capturedBytes += slice.length;
514
+ },
515
+ text() {
516
+ return Buffer.concat(chunks).toString("utf8");
517
+ }
518
+ };
519
+ }
520
+ //#endregion
521
+ //#region src/cli/console.ts
522
+ const GLOBAL_VALUE_FLAGS = new Set([
523
+ "--port",
524
+ "--instance",
525
+ "--data-dir",
526
+ "--tools"
527
+ ]);
528
+ const GLOBAL_BOOLEAN_FLAGS = new Set([
529
+ "--verbose",
530
+ "-v",
531
+ "--no-engine"
532
+ ]);
533
+ function defaultConsolePort(viewerPort) {
534
+ return viewerPort + 1;
535
+ }
536
+ function readPort(raw, flag) {
537
+ const port = raw === void 0 ? NaN : Number(raw);
538
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${flag} needs a port between 1 and 65535`);
539
+ return port;
540
+ }
541
+ function parseConsoleArgs(tail, fallbackPort) {
542
+ let consolePort = fallbackPort;
543
+ const extraArgs = [];
544
+ for (let i = 0; i < tail.length; i++) {
545
+ const arg = tail[i];
546
+ if (arg === "--console-port") {
547
+ consolePort = readPort(tail[i + 1], arg);
548
+ i++;
549
+ continue;
550
+ }
551
+ if (arg.startsWith("--console-port=")) {
552
+ consolePort = readPort(arg.slice(15), "--console-port");
553
+ continue;
554
+ }
555
+ if (GLOBAL_VALUE_FLAGS.has(arg)) {
556
+ i++;
557
+ continue;
558
+ }
559
+ if ([...GLOBAL_VALUE_FLAGS].some((flag) => arg.startsWith(`${flag}=`))) continue;
560
+ if (GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
561
+ extraArgs.push(arg);
562
+ }
563
+ return {
564
+ consolePort,
565
+ extraArgs
566
+ };
567
+ }
568
+ function consoleArgs(launch) {
569
+ return [
570
+ "console",
571
+ "--port",
572
+ String(launch.consolePort),
573
+ "--engine-port",
574
+ String(launch.restPort),
575
+ "--ws-port",
576
+ String(launch.streamPort),
577
+ "--bridge-port",
578
+ String(launch.enginePort),
579
+ ...launch.extraArgs
580
+ ];
581
+ }
582
+ //#endregion
583
+ //#region src/cli/process-state.ts
584
+ function processStatIsRunning(stat) {
585
+ return !stat.trimStart().startsWith("Z");
586
+ }
587
+ //#endregion
380
588
  //#region src/cli/splash.ts
381
589
  const IS_COLOR_TTY = !!process.stdout.isTTY && !process.env["NO_COLOR"];
382
590
  function accent(s) {
@@ -436,7 +644,6 @@ const DEFAULTS = {
436
644
  skipSplash: false,
437
645
  skipNpxHint: false,
438
646
  skipGlobalInstall: false,
439
- skipConsoleInstall: false,
440
647
  firstRunAt: null,
441
648
  injectContextChosen: false
442
649
  };
@@ -525,7 +732,7 @@ const PROVIDERS = [
525
732
  },
526
733
  {
527
734
  value: "minimax",
528
- label: "MiniMax — minimax-m1",
735
+ label: "MiniMax — MiniMax-M3",
529
736
  envKey: "MINIMAX_API_KEY"
530
737
  },
531
738
  {
@@ -765,6 +972,86 @@ async function wireSelectedAgents(agents) {
765
972
  p.note(summary.join("\n"), "wire summary");
766
973
  }
767
974
  //#endregion
975
+ //#region src/cli/capture.ts
976
+ function headers(secret) {
977
+ const h = { "Content-Type": "application/json" };
978
+ if (secret) h["Authorization"] = `Bearer ${secret}`;
979
+ return h;
980
+ }
981
+ function classify(status) {
982
+ if (status === 200) return "duplicate";
983
+ if (status >= 200 && status < 300) return "delivered";
984
+ if (status === 408 || status === 429 || status >= 500) return "retry";
985
+ return "rejected";
986
+ }
987
+ async function serverCapture(base, secret) {
988
+ try {
989
+ const res = await fetch(`${base}/agentmemory/capture?limit=20`, {
990
+ headers: headers(secret),
991
+ signal: AbortSignal.timeout(5e3)
992
+ });
993
+ if (!res.ok) return { error: `HTTP ${res.status}` };
994
+ return await res.json();
995
+ } catch (err) {
996
+ return { error: err instanceof Error ? err.message : String(err) };
997
+ }
998
+ }
999
+ async function runCaptureCommand(options) {
1000
+ const log = options.log ?? ((line) => console.log(line));
1001
+ const json = options.args.includes("--json");
1002
+ const drain = options.args.includes("--drain");
1003
+ const output = {};
1004
+ if (drain) output["drain"] = await drainSpool(options.base, async (record) => {
1005
+ const res = await fetch(`${options.base}/agentmemory/observe`, {
1006
+ method: "POST",
1007
+ headers: headers(options.secret),
1008
+ body: JSON.stringify({
1009
+ ...record.body,
1010
+ eventId: record.eventId
1011
+ }),
1012
+ signal: AbortSignal.timeout(1e4)
1013
+ });
1014
+ const text = await res.text().catch(() => "");
1015
+ const outcome = classify(res.status);
1016
+ if (outcome === "delivered" || outcome === "duplicate") {
1017
+ let mark = null;
1018
+ try {
1019
+ mark = parseSentMark(JSON.parse(text));
1020
+ } catch {}
1021
+ if (mark) retainSent(options.base, record.eventId, record.body, mark);
1022
+ }
1023
+ return outcome;
1024
+ });
1025
+ output["spool"] = spoolSummary(options.base);
1026
+ output["server"] = await serverCapture(options.base, options.secret);
1027
+ if (json) {
1028
+ log(JSON.stringify(output, null, 2));
1029
+ return 0;
1030
+ }
1031
+ const spool = output["spool"];
1032
+ if (drain) {
1033
+ const d = output["drain"];
1034
+ log(d.skipped ? `Drain skipped: ${d.skipped === "locked" ? "another drain is running" : "nothing to send"}` : `Drained ${d.claimed}: ${d.delivered} stored, ${d.duplicates} already stored, ${d.rejected} rejected, ${d.expired} expired, ${d.remaining} still waiting${d.error ? ` (${d.error})` : ""}`);
1035
+ }
1036
+ log(`Local spool: ${spool.enabled ? `${spool.records} waiting, ${spool.bytes} bytes` : "off (AGENTMEMORY_CAPTURE_SPOOL=false)"}`);
1037
+ log(` file: ${spool.path}`);
1038
+ log(` limits: ${spool.maxBytes} bytes, ${spool.maxAgeHours} h`);
1039
+ log(` totals: ${spool.stats.spooled} spooled, ${spool.stats.delivered} delivered, ${spool.stats.duplicates} duplicates, ${spool.stats.dropped} dropped, ${spool.stats.expired} expired`);
1040
+ if (spool.stats.lastDrainAt) log(` last drain: ${spool.stats.lastDrainAt}`);
1041
+ if (spool.stats.lastDropAt) log(` last drop: ${spool.stats.lastDropAt} (${spool.stats.lastDropReason ?? "unknown"})`);
1042
+ const server = output["server"];
1043
+ if (server.error) {
1044
+ log(`Server: unreachable at ${options.base} (${server.error})`);
1045
+ return 0;
1046
+ }
1047
+ const inbox = server.capture?.inbox;
1048
+ log(`Server inbox: ${inbox ? `${inbox.pending} pending, ${inbox.retrying} retrying, ${inbox.dead} dead letters` : "unknown"}`);
1049
+ if (inbox?.lastError) log(` last error: ${inbox.lastError}`);
1050
+ for (const item of server.items ?? []) log(` ${item.status.padEnd(8)} ${item.eventId} attempts=${item.attempts} ${item.preview ?? ""}`);
1051
+ if (inbox && inbox.dead > 0) log(`Retry dead letters: curl -X POST ${options.base}/agentmemory/capture/retry ${CURL_AUTH_HEADER} -H "Content-Type: application/json" -d '{"all":true}'`);
1052
+ return 0;
1053
+ }
1054
+ //#endregion
768
1055
  //#region src/cli.ts
769
1056
  const c = {
770
1057
  url: pc.cyan,
@@ -784,11 +1071,14 @@ const IS_WINDOWS = platform() === "win32";
784
1071
  const IS_VERBOSE = args.includes("--verbose") || args.includes("-v") || process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
785
1072
  setBootVerbose(IS_VERBOSE);
786
1073
  const IS_RESET = args.includes("--reset");
1074
+ hydrateProcessEnvFromFile();
787
1075
  if (args.includes("--version") || args.includes("-V")) {
788
1076
  process.stdout.write(`${VERSION}\n`);
789
1077
  process.exit(0);
790
1078
  }
791
- const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || "0.11.2";
1079
+ const IIPINNED_DEFAULT_VERSION = III_PINNED_VERSION;
1080
+ const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || IIPINNED_DEFAULT_VERSION;
1081
+ const IIIENGINE_INSTALL_CMD = `curl -fsSL https://install.iii.dev/iii/main/install.sh | VERSION=${IIPINNED_VERSION} sh`;
792
1082
  function iiiReleaseAsset() {
793
1083
  const p = platform();
794
1084
  const a = process.arch;
@@ -836,6 +1126,8 @@ Commands:
836
1126
  No arg = interactive picker. --all wires every detected agent.
837
1127
  --dry-run shows what would change. --force re-installs.
838
1128
  status Show connection status, memory count, flags, and health
1129
+ console Launch the iii web console for this engine (workers, functions,
1130
+ triggers, queues, traces). --console-port N, default viewer+1
839
1131
  doctor Interactive diagnostic + fixer. [F]ix · [S]kip · [?]more · [Q]uit
840
1132
  --all: apply every fix without prompting (CI)
841
1133
  --dry-run: show what each fix would do, don't execute
@@ -851,6 +1143,9 @@ Commands:
851
1143
  the engine was started natively but state file is missing).
852
1144
  mcp Start standalone MCP shim — opt-in surface for MCP-only clients
853
1145
  (Cursor, Gemini CLI, etc). REST always available at :3111.
1146
+ capture [--drain] Show capture health: the local offline spool and the server
1147
+ inbox (pending, retrying, dead letters). --drain sends
1148
+ spooled observations now. --json for machine output.
854
1149
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
855
1150
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
856
1151
  out-of-range is rejected; for trees >1000 files, batch by subdirectory)
@@ -867,13 +1162,18 @@ Options:
867
1162
  --instance <N> Shortcut for --port (3111 + N*100) to run multiple
868
1163
  daemons side-by-side without env gymnastics.
869
1164
  --instance 1 -> 3211/3212/3213/49234, etc. (max N=50)
1165
+ --data-dir <path> Store iii-engine state outside the current repo
870
1166
 
871
1167
  Environment:
872
1168
  AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
873
1169
  Honored by status, doctor, and MCP shim commands.
1170
+ AGENTMEMORY_DATA_DIR State directory fallback when --data-dir is not set.
874
1171
  AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
875
1172
  native iii-engine binary on first run.
876
1173
  AGENTMEMORY_III_VERSION Override pinned iii-engine version (default ${IIPINNED_VERSION}).
1174
+ AGENTMEMORY_III_CONFIG Engine config to start from (default: ./iii-config.yaml,
1175
+ then ~/.agentmemory/iii-config.yaml, then the bundled
1176
+ loopback-only config).
877
1177
  AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS
878
1178
  Window (seconds) for the smart-search follow-up diagnostic
879
1179
  (default 30). Long values overcount, short values undercount.
@@ -895,19 +1195,62 @@ if (toolsIdx !== -1 && args[toolsIdx + 1]) {
895
1195
  if (toolsMode !== "all" && toolsMode !== "core") p.log.warn(`Unknown --tools value "${toolsMode}" (valid: all, core); falling back to all.`);
896
1196
  process.env["AGENTMEMORY_TOOLS"] = toolsMode;
897
1197
  }
898
- const portIdx = args.indexOf("--port");
899
- if (portIdx !== -1 && args[portIdx + 1]) process.env["III_REST_PORT"] = args[portIdx + 1];
900
- const instanceIdx = args.indexOf("--instance");
901
- if (instanceIdx !== -1 && args[instanceIdx + 1]) {
902
- const n = parseInt(args[instanceIdx + 1] || "", 10);
903
- if (Number.isFinite(n) && n >= 0 && n <= 50) {
904
- const base = 3111 + n * 100;
905
- if (!process.env["III_REST_PORT"]) process.env["III_REST_PORT"] = String(base);
1198
+ const URL_CLIENT_COMMANDS = new Set([
1199
+ "status",
1200
+ "doctor",
1201
+ "mcp",
1202
+ "capture"
1203
+ ]);
1204
+ let hasExplicitLocalPortOverride = false;
1205
+ let selectedInstance = 0;
1206
+ function cliArgValue(name) {
1207
+ const inline = args.find((arg) => arg.startsWith(`${name}=`));
1208
+ if (inline) return inline.slice(name.length + 1);
1209
+ const index = args.indexOf(name);
1210
+ return index === -1 ? void 0 : args[index + 1];
1211
+ }
1212
+ const portValue = cliArgValue("--port");
1213
+ if (portValue) {
1214
+ process.env["III_REST_PORT"] = portValue;
1215
+ hasExplicitLocalPortOverride = true;
1216
+ }
1217
+ const instanceValue = cliArgValue("--instance");
1218
+ if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance="))) {
1219
+ if (instanceValue === void 0 || !/^\d+$/.test(instanceValue)) {
1220
+ p.log.error("--instance must be an integer between 0 and 50.");
1221
+ process.exit(1);
906
1222
  }
1223
+ const n = Number(instanceValue);
1224
+ if (!Number.isInteger(n) || n > 50) {
1225
+ p.log.error("--instance must be an integer between 0 and 50.");
1226
+ process.exit(1);
1227
+ }
1228
+ selectedInstance = n;
1229
+ const base = 3111 + n * 100;
1230
+ if (!hasExplicitLocalPortOverride) process.env["III_REST_PORT"] = String(base);
1231
+ hasExplicitLocalPortOverride = true;
1232
+ }
1233
+ const restPort = parseInt(process.env["III_REST_PORT"] || "3111", 10);
1234
+ if (Number.isFinite(restPort) && restPort > 0) {
1235
+ process.env["III_STREAM_PORT"] ??= process.env["III_STREAMS_PORT"] ?? String(restPort + 1);
1236
+ process.env["III_VIEWER_PORT"] ??= String(restPort + 2);
1237
+ const configuredEngineUrl = process.env["III_ENGINE_URL"];
1238
+ if (configuredEngineUrl) try {
1239
+ const parsedEnginePort = new URL(configuredEngineUrl).port;
1240
+ if (parsedEnginePort) process.env["III_ENGINE_PORT"] = parsedEnginePort;
1241
+ } catch {}
1242
+ else process.env["III_ENGINE_PORT"] ??= String(restPort + 46023);
1243
+ process.env["AGENTMEMORY_METRICS_PORT"] ??= String(restPort + 6353);
907
1244
  }
1245
+ const dataDirResolution = resolveDataDir({ args });
1246
+ process.env["AGENTMEMORY_DATA_DIR"] = dataDirResolution.dataDir;
1247
+ process.env["AGENTMEMORY_RUNTIME_DIR"] = selectedInstance > 0 ? dataDirResolution.dataDir : join(homedir(), ".agentmemory");
908
1248
  const skipEngine = args.includes("--no-engine");
1249
+ function shouldUseAgentmemoryUrl() {
1250
+ return !hasExplicitLocalPortOverride && URL_CLIENT_COMMANDS.has(args[0] ?? "");
1251
+ }
909
1252
  function getRestPort() {
910
- const url = process.env["AGENTMEMORY_URL"];
1253
+ const url = shouldUseAgentmemoryUrl() ? process.env["AGENTMEMORY_URL"] : void 0;
911
1254
  if (url) try {
912
1255
  const parsed = new URL(url).port;
913
1256
  if (parsed) return parseInt(parsed, 10);
@@ -915,10 +1258,13 @@ function getRestPort() {
915
1258
  return parseInt(process.env["III_REST_PORT"] || "3111", 10) || 3111;
916
1259
  }
917
1260
  function getBaseUrl() {
918
- const url = process.env["AGENTMEMORY_URL"];
1261
+ const url = shouldUseAgentmemoryUrl() ? process.env["AGENTMEMORY_URL"] : void 0;
919
1262
  if (url) return url.replace(/\/+$/, "");
920
1263
  return `http://localhost:${getRestPort()}`;
921
1264
  }
1265
+ function getConfiguredViewerPort() {
1266
+ return parseInt(process.env["III_VIEWER_PORT"] || "", 10) || getRestPort() + 2;
1267
+ }
922
1268
  let discoveredViewerPort = null;
923
1269
  async function discoverViewerPort() {
924
1270
  if (discoveredViewerPort !== null) return;
@@ -941,23 +1287,23 @@ function getViewerUrl() {
941
1287
  }
942
1288
  try {
943
1289
  const u = new URL(getBaseUrl());
944
- const vPort = parseInt(process.env["III_VIEWER_PORT"] || "", 10) || (parseInt(u.port || "3111", 10) || 3111) + 2;
1290
+ const vPort = getConfiguredViewerPort();
945
1291
  return `${u.protocol}//${u.hostname}:${vPort}`;
946
1292
  } catch {
947
- return `http://localhost:${parseInt(process.env["III_VIEWER_PORT"] || "", 10) || getRestPort() + 2}`;
1293
+ return `http://localhost:${getConfiguredViewerPort()}`;
948
1294
  }
949
1295
  }
950
1296
  function getStreamPort() {
951
1297
  return parseInt(process.env["III_STREAM_PORT"] || "", 10) || parseInt(process.env["III_STREAMS_PORT"] || "", 10) || getRestPort() + 1;
952
1298
  }
953
1299
  function getEnginePort() {
954
- const explicit = parseInt(process.env["III_ENGINE_PORT"] || "", 10);
955
- if (explicit) return explicit;
956
1300
  const url = process.env["III_ENGINE_URL"];
957
1301
  if (url) try {
958
1302
  const parsed = new URL(url).port;
959
1303
  if (parsed) return parseInt(parsed, 10);
960
1304
  } catch {}
1305
+ const explicit = parseInt(process.env["III_ENGINE_PORT"] || "", 10);
1306
+ if (explicit) return explicit;
961
1307
  return getRestPort() + 46023;
962
1308
  }
963
1309
  async function isEngineRunning() {
@@ -996,9 +1342,21 @@ function findIiiConfig() {
996
1342
  join(__dirname, "iii-config.yaml"),
997
1343
  join(__dirname, "..", "iii-config.yaml")
998
1344
  ];
999
- for (const c of candidates) if (existsSync(c)) return c;
1345
+ for (const c of candidates) if (existsSync(c)) return resolve(c);
1000
1346
  return "";
1001
1347
  }
1348
+ function warnIfRelocatedDataDir() {
1349
+ if (!dataDirResolution.relocatedFrom) return;
1350
+ try {
1351
+ mkdirSync(dataDirResolution.dataDir, { recursive: true });
1352
+ const marker = join(dataDirResolution.dataDir, ".cwd-relocation-warning");
1353
+ if (existsSync(marker)) return;
1354
+ p.log.warn(`Default data dir ${dataDirResolution.relocatedFrom} is inside a git worktree; using ${dataDirResolution.dataDir} instead.`);
1355
+ writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
1356
+ } catch {
1357
+ p.log.warn(`Default data dir ${dataDirResolution.relocatedFrom} is inside a git worktree; using ${dataDirResolution.dataDir} instead.`);
1358
+ }
1359
+ }
1002
1360
  function whichBinary(name) {
1003
1361
  const cmd = IS_WINDOWS ? "where" : "which";
1004
1362
  try {
@@ -1068,10 +1426,10 @@ function resolveCompatibleIii(iiiBinPath) {
1068
1426
  return null;
1069
1427
  }
1070
1428
  function enginePidfilePath() {
1071
- return join(homedir(), ".agentmemory", "iii.pid");
1429
+ return runtimeMetadataPath("iii.pid");
1072
1430
  }
1073
1431
  function engineStatePath() {
1074
- return join(homedir(), ".agentmemory", "engine-state.json");
1432
+ return runtimeMetadataPath("engine-state.json");
1075
1433
  }
1076
1434
  function writeEnginePidfile(pid) {
1077
1435
  try {
@@ -1097,7 +1455,7 @@ function clearEnginePidfile() {
1097
1455
  } catch {}
1098
1456
  }
1099
1457
  function workerPidfilePath() {
1100
- return join(homedir(), ".agentmemory", "worker.pid");
1458
+ return runtimeMetadataPath("worker.pid");
1101
1459
  }
1102
1460
  function readWorkerPidfile() {
1103
1461
  try {
@@ -1117,7 +1475,15 @@ function writeEngineState(state) {
1117
1475
  try {
1118
1476
  const statePath = engineStatePath();
1119
1477
  mkdirSync(dirname(statePath), { recursive: true });
1120
- writeFileSync(statePath, `${JSON.stringify(state)}\n`, { encoding: "utf-8" });
1478
+ const normalized = state.kind === "docker" && state.schemaVersion === 2 ? {
1479
+ ...state,
1480
+ restPort: state.restPort ?? getRestPort(),
1481
+ dataDir: state.dataDir ?? dataDirResolution.dataDir
1482
+ } : {
1483
+ ...state,
1484
+ restPort: state.restPort ?? getRestPort()
1485
+ };
1486
+ writeFileSync(statePath, `${JSON.stringify(normalized)}\n`, { encoding: "utf-8" });
1121
1487
  } catch (err) {
1122
1488
  vlog(`writeEngineState: ${err instanceof Error ? err.message : String(err)}`);
1123
1489
  }
@@ -1137,6 +1503,44 @@ function clearEngineState() {
1137
1503
  unlinkSync(engineStatePath());
1138
1504
  } catch {}
1139
1505
  }
1506
+ function engineStateRestPort(state) {
1507
+ if (state.restPort && Number.isFinite(state.restPort)) return state.restPort;
1508
+ if (state.kind === "docker") {
1509
+ const projectPort = state.projectName?.match(/^agentmemory-(\d+)$/)?.[1];
1510
+ if (projectPort) return parseInt(projectPort, 10);
1511
+ return 3111;
1512
+ }
1513
+ try {
1514
+ const configuredPort = (readFileSync(state.configPath, "utf-8").match(/- name:\s*(?:iii-)?http([\s\S]*?)(?=\n\s*- name:|$)/)?.[1])?.match(/\n\s*port:\s*(\d+)/)?.[1];
1515
+ if (configuredPort) return parseInt(configuredPort, 10);
1516
+ } catch {}
1517
+ return 3111;
1518
+ }
1519
+ async function startWorkerForEngineState() {
1520
+ const workerPid = readWorkerPidfile();
1521
+ if (workerPid && pidAlive(workerPid)) return;
1522
+ if (configuredEngineMayStartWorker() && await waitForConfiguredWorker(5e3)) return;
1523
+ await import("./src-Jsq9LCeH.mjs");
1524
+ }
1525
+ function configuredEngineMayStartWorker() {
1526
+ const state = readEngineState();
1527
+ if (state?.kind !== "native") return false;
1528
+ try {
1529
+ return readFileSync(state.configPath, "utf-8").includes("- name: iii-exec");
1530
+ } catch {
1531
+ return false;
1532
+ }
1533
+ }
1534
+ async function waitForConfiguredWorker(timeoutMs) {
1535
+ const deadline = Date.now() + timeoutMs;
1536
+ while (Date.now() < deadline) {
1537
+ const workerPid = readWorkerPidfile();
1538
+ if (workerPid && pidAlive(workerPid)) return true;
1539
+ if (await isAgentmemoryReady()) return true;
1540
+ await new Promise((resolve) => setTimeout(resolve, 100));
1541
+ }
1542
+ return false;
1543
+ }
1140
1544
  function discoverComposeFile() {
1141
1545
  return [
1142
1546
  join(__dirname, "..", "docker-compose.yml"),
@@ -1144,6 +1548,251 @@ function discoverComposeFile() {
1144
1548
  join(process.cwd(), "docker-compose.yml")
1145
1549
  ].find((c) => existsSync(c)) ?? null;
1146
1550
  }
1551
+ function sameDockerMountSource(type, left, right) {
1552
+ return type === "bind" ? resolve(left) === resolve(right) : left === right;
1553
+ }
1554
+ function inspectDockerContainer(dockerBin, containerId, state, ownerPort) {
1555
+ const result = spawnSync(dockerBin, ["inspect", containerId], {
1556
+ encoding: "utf-8",
1557
+ stdio: [
1558
+ "ignore",
1559
+ "pipe",
1560
+ "ignore"
1561
+ ],
1562
+ maxBuffer: 1024 * 1024
1563
+ });
1564
+ if (result.status !== 0) return {
1565
+ status: "unavailable",
1566
+ reason: `docker inspect failed for ${containerId}`
1567
+ };
1568
+ let record;
1569
+ try {
1570
+ record = JSON.parse(result.stdout)[0] ?? {};
1571
+ } catch {
1572
+ return {
1573
+ status: "unavailable",
1574
+ reason: `docker inspect returned invalid JSON for ${containerId}`
1575
+ };
1576
+ }
1577
+ const image = record.Config?.Image ?? "";
1578
+ const expectedImage = `iiidev/iii:${state.engineVersion ?? IIPINNED_VERSION}`;
1579
+ if (image !== expectedImage) return {
1580
+ status: "unavailable",
1581
+ reason: `saved container uses ${image || "an unknown image"}, expected ${expectedImage}`
1582
+ };
1583
+ const labels = record.Config?.Labels ?? {};
1584
+ const projectName = labels["com.docker.compose.project"] ?? "";
1585
+ if (labels["com.docker.compose.service"] !== "iii-engine" || !projectName) return {
1586
+ status: "unavailable",
1587
+ reason: "saved container is not the iii-engine service of a Compose project"
1588
+ };
1589
+ if (state.projectName && state.projectName !== projectName) return {
1590
+ status: "unavailable",
1591
+ reason: `saved Docker project ${state.projectName} does not match container project ${projectName}`
1592
+ };
1593
+ if (state.schemaVersion === 2 && projectName !== dockerProjectName(ownerPort)) return {
1594
+ status: "unavailable",
1595
+ reason: `Docker project ${projectName} does not match REST port ${ownerPort}`
1596
+ };
1597
+ if (!(record.HostConfig?.PortBindings?.[`${ownerPort}/tcp`] ?? []).some((binding) => binding.HostPort === String(ownerPort))) return {
1598
+ status: "unavailable",
1599
+ reason: `saved container does not publish REST port ${ownerPort}`
1600
+ };
1601
+ const dataMount = record.Mounts?.find((mount) => mount.Destination === "/data");
1602
+ const dataMountType = dataMount?.Type;
1603
+ const dataMountSource = dataMountType === "volume" ? dataMount?.Name ?? dataMount?.Source ?? "" : dataMount?.Source ?? "";
1604
+ if (dataMountType !== "bind" && dataMountType !== "volume" || !dataMountSource) return {
1605
+ status: "unavailable",
1606
+ reason: "saved container has no verifiable /data bind or volume mount"
1607
+ };
1608
+ if (state.dataMountType && state.dataMountType !== dataMountType) return {
1609
+ status: "unavailable",
1610
+ reason: `saved /data mount type ${state.dataMountType} changed to ${dataMountType}`
1611
+ };
1612
+ if (state.dataMountSource && !sameDockerMountSource(dataMountType, state.dataMountSource, dataMountSource)) return {
1613
+ status: "unavailable",
1614
+ reason: "saved container /data mount source changed"
1615
+ };
1616
+ if (!state.dataMountSource) {
1617
+ if (dataMountType === "bind" && resolve(dataMountSource) !== resolve(dataDirResolution.dataDir)) return {
1618
+ status: "unavailable",
1619
+ reason: `container /data bind ${dataMountSource} does not match ${dataDirResolution.dataDir}`
1620
+ };
1621
+ if (dataMountType === "volume" && dataDirResolution.source !== "default") return {
1622
+ status: "unavailable",
1623
+ reason: "legacy container uses a named volume but this invocation selects an explicit data directory"
1624
+ };
1625
+ }
1626
+ if (state.dataDir && resolve(state.dataDir) !== resolve(dataDirResolution.dataDir)) return {
1627
+ status: "unavailable",
1628
+ reason: `saved data directory ${state.dataDir} does not match ${dataDirResolution.dataDir}`
1629
+ };
1630
+ return {
1631
+ status: record.State?.Running ? "running" : "stopped",
1632
+ composeFile: existsSync(state.composeFile) ? state.composeFile : discoverComposeFile() ?? state.composeFile,
1633
+ projectName,
1634
+ containerId: record.Id ?? containerId,
1635
+ dataMountType,
1636
+ dataMountSource,
1637
+ preserveContainer: state.preserveContainer === true || state.schemaVersion !== 2 && dataMountType === "volume"
1638
+ };
1639
+ }
1640
+ function inspectOwnedDockerEngine(state) {
1641
+ if (state.kind !== "docker") return {
1642
+ status: "unavailable",
1643
+ reason: "engine state is not Docker"
1644
+ };
1645
+ const ownerPort = engineStateRestPort(state);
1646
+ if (state.schemaVersion === 2 && state.projectName !== dockerProjectName(ownerPort)) return {
1647
+ status: "unavailable",
1648
+ reason: `saved Docker project ${state.projectName} does not match REST port ${ownerPort}`
1649
+ };
1650
+ const dockerBin = whichBinary("docker");
1651
+ if (!dockerBin) return {
1652
+ status: "unavailable",
1653
+ reason: "docker is not on PATH"
1654
+ };
1655
+ const discoveryFailures = [];
1656
+ if (state.containerId) {
1657
+ const direct = inspectDockerContainer(dockerBin, state.containerId, state, ownerPort);
1658
+ if (direct.status !== "unavailable") return direct;
1659
+ if (state.preserveContainer) return direct;
1660
+ discoveryFailures.push(direct.reason);
1661
+ }
1662
+ const composeFile = existsSync(state.composeFile) ? state.composeFile : discoverComposeFile() ?? state.composeFile;
1663
+ const candidateIds = /* @__PURE__ */ new Set();
1664
+ try {
1665
+ if (existsSync(composeFile)) {
1666
+ const composeText = readFileSync(composeFile, "utf-8");
1667
+ if (!/^\s+iii-engine:/m.test(composeText)) return {
1668
+ status: "unavailable",
1669
+ reason: `${composeFile} does not define iii-engine`
1670
+ };
1671
+ const psResult = spawnSync(dockerBin, dockerComposeArgs(composeFile, state.projectName, [
1672
+ "ps",
1673
+ "-q",
1674
+ "--all",
1675
+ "iii-engine"
1676
+ ]), {
1677
+ encoding: "utf-8",
1678
+ stdio: [
1679
+ "ignore",
1680
+ "pipe",
1681
+ "ignore"
1682
+ ]
1683
+ });
1684
+ if (psResult.status === 0) {
1685
+ for (const id of (psResult.stdout ?? "").trim().split(/\s+/)) if (id) candidateIds.add(id);
1686
+ } else discoveryFailures.push(`docker compose ps failed for project ${state.projectName ?? "<default>"}`);
1687
+ }
1688
+ const scanResult = spawnSync(dockerBin, [
1689
+ "ps",
1690
+ "-aq",
1691
+ "--filter",
1692
+ "label=com.docker.compose.service=iii-engine",
1693
+ "--filter",
1694
+ `ancestor=iiidev/iii:${state.engineVersion ?? IIPINNED_VERSION}`,
1695
+ ...state.projectName ? ["--filter", `label=com.docker.compose.project=${state.projectName}`] : []
1696
+ ], {
1697
+ encoding: "utf-8",
1698
+ stdio: [
1699
+ "ignore",
1700
+ "pipe",
1701
+ "ignore"
1702
+ ]
1703
+ });
1704
+ if (scanResult.status === 0) {
1705
+ for (const id of (scanResult.stdout ?? "").trim().split(/\s+/)) if (id) candidateIds.add(id);
1706
+ } else discoveryFailures.push("docker container scan failed");
1707
+ const matches = [];
1708
+ for (const containerId of candidateIds) {
1709
+ const inspected = inspectDockerContainer(dockerBin, containerId, state, ownerPort);
1710
+ if (inspected.status === "running" || inspected.status === "stopped") {
1711
+ if (!matches.some((match) => (match.status === "running" || match.status === "stopped") && match.containerId === inspected.containerId)) matches.push(inspected);
1712
+ } else if (inspected.status === "unavailable") discoveryFailures.push(inspected.reason);
1713
+ }
1714
+ if (matches.length === 1) return matches[0];
1715
+ if (matches.length > 1) return {
1716
+ status: "unavailable",
1717
+ reason: `multiple Docker iii-engine containers match REST port ${ownerPort}`
1718
+ };
1719
+ if (discoveryFailures.length > 0) return {
1720
+ status: "unavailable",
1721
+ reason: [...new Set(discoveryFailures)].join("; ")
1722
+ };
1723
+ if (state.schemaVersion !== 2 || state.preserveContainer) return {
1724
+ status: "unavailable",
1725
+ reason: `legacy Docker ownership exists for REST port ${ownerPort}, but no unique container can be verified`
1726
+ };
1727
+ return {
1728
+ status: "missing",
1729
+ composeFile,
1730
+ projectName: state.projectName
1731
+ };
1732
+ } catch (err) {
1733
+ return {
1734
+ status: "unavailable",
1735
+ reason: err instanceof Error ? err.message : String(err)
1736
+ };
1737
+ }
1738
+ }
1739
+ function persistDockerInspection(state, inspection) {
1740
+ if (inspection.status !== "running" && inspection.status !== "stopped") return state;
1741
+ const resolved = {
1742
+ ...state,
1743
+ composeFile: inspection.composeFile,
1744
+ projectName: inspection.projectName,
1745
+ containerId: inspection.containerId,
1746
+ dataMountType: inspection.dataMountType,
1747
+ dataMountSource: inspection.dataMountSource,
1748
+ preserveContainer: inspection.preserveContainer
1749
+ };
1750
+ writeEngineState(resolved);
1751
+ return resolved;
1752
+ }
1753
+ async function assertRuntimePortOwnership() {
1754
+ const state = readEngineState();
1755
+ if (!state) return;
1756
+ const ownerPort = engineStateRestPort(state);
1757
+ const requestedPort = getRestPort();
1758
+ if (ownerPort === requestedPort) return;
1759
+ if (state.kind === "docker") {
1760
+ const inspection = inspectOwnedDockerEngine(state);
1761
+ if (inspection.status === "unavailable") {
1762
+ p.log.error(`The data directory contains Docker ownership for REST port ${ownerPort}, but it cannot be verified: ${inspection.reason}. Refusing to overwrite its lifecycle state.`);
1763
+ process.exit(1);
1764
+ }
1765
+ if (inspection.status === "running" || inspection.status === "stopped") {
1766
+ persistDockerInspection(state, inspection);
1767
+ p.log.error(`The canonical lifecycle state is owned by a ${inspection.status} Docker engine on REST port ${ownerPort}. Refusing to reuse it for port ${requestedPort}. Use the original port, or use --instance <N> for an isolated daemon.`);
1768
+ process.exit(1);
1769
+ }
1770
+ p.log.error(`The canonical lifecycle state still records a Docker engine for REST port ${ownerPort}, but its container is missing. Refusing to overwrite that ownership from port ${requestedPort}. Re-run \`agentmemory stop\` with the original port to reconcile it.`);
1771
+ process.exit(1);
1772
+ }
1773
+ const enginePid = readEnginePidfile();
1774
+ const workerPid = readWorkerPidfile();
1775
+ let active = Boolean(enginePid && pidAlive(enginePid) && !isForeignPortHolder(pidCommand(enginePid)) || workerPid && pidAlive(workerPid));
1776
+ if (!active) try {
1777
+ await fetch(`http://127.0.0.1:${ownerPort}/`, { signal: AbortSignal.timeout(750) });
1778
+ active = true;
1779
+ } catch {}
1780
+ if (active) {
1781
+ p.log.error(`The canonical lifecycle state is owned by a live agentmemory engine on REST port ${ownerPort}. Refusing to reuse it for port ${requestedPort}. Use the original port, or use --instance <N> for an isolated daemon.`);
1782
+ process.exit(1);
1783
+ }
1784
+ clearEnginePidfile();
1785
+ clearWorkerPidfile();
1786
+ clearEngineState();
1787
+ }
1788
+ function configureDockerHostUser() {
1789
+ if (typeof process.getuid !== "function" || typeof process.getgid !== "function") return;
1790
+ const hostUid = String(process.getuid());
1791
+ const hostGid = String(process.getgid());
1792
+ process.env["AGENTMEMORY_DOCKER_UID"] ??= hostUid;
1793
+ process.env["AGENTMEMORY_DOCKER_GID"] ??= hostGid;
1794
+ if (process.env["AGENTMEMORY_DOCKER_UID"] === hostUid && process.env["AGENTMEMORY_DOCKER_GID"] === hostGid) process.env["AGENTMEMORY_DOCKER_SKIP_CHOWN"] ??= "1";
1795
+ }
1147
1796
  function isInvokedViaNpx() {
1148
1797
  if (process.env["npm_lifecycle_event"] === "npx") return true;
1149
1798
  if ((process.argv[1] ?? "").includes("_npx")) return true;
@@ -1175,62 +1824,25 @@ async function maybeOfferGlobalInstall() {
1175
1824
  if (runCommand(npmBin, [
1176
1825
  "install",
1177
1826
  "-g",
1178
- `@agentmemory/agentmemory@0.9.28`
1179
- ], { label: `Installing @agentmemory/agentmemory@0.9.28 globally` })) {
1827
+ `@agentmemory/agentmemory@0.9.30`
1828
+ ], { label: `Installing @agentmemory/agentmemory@0.9.30 globally` })) {
1180
1829
  p.log.success("Installed globally. `agentmemory stop` etc. will now work in new shells.");
1181
1830
  writePrefs({ skipGlobalInstall: true });
1182
1831
  } else p.log.warn("Global install failed. Try manually: npm install -g @agentmemory/agentmemory");
1183
1832
  }
1184
- function detectIiiConsole() {
1185
- const onPath = whichBinary("iii-console");
1186
- if (onPath) return {
1187
- kind: "installed",
1188
- binPath: onPath
1189
- };
1190
- const fallback = IS_WINDOWS ? join(process.env["USERPROFILE"] ?? "", ".local", "bin", "iii-console.exe") : join(homedir(), ".local", "bin", "iii-console");
1191
- if (fallback && existsSync(fallback)) return {
1192
- kind: "installed",
1193
- binPath: fallback
1194
- };
1195
- return { kind: "missing" };
1196
- }
1197
- const III_CONSOLE_INSTALL_CMD = `curl -fsSL https://install.iii.dev/iii/main/install.sh | VERSION=${IIPINNED_VERSION} sh`;
1198
- function iiiConsoleInstallHint() {
1199
- if (!IS_WINDOWS) return III_CONSOLE_INSTALL_CMD;
1200
- return `# PowerShell:\n $env:VERSION = "${IIPINNED_VERSION}"\n iwr -useb https://install.iii.dev/iii/main/install.sh -OutFile install.sh\n bash install.sh # WSL or Git Bash required\n# Or grab the pinned release directly:\n https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
1201
- }
1202
- async function ensureIiiConsole() {
1203
- const state = detectIiiConsole();
1204
- if (state.kind === "installed") return state;
1205
- if (!process.stdin.isTTY || process.env["CI"]) return state;
1206
- if (readPrefs().skipConsoleInstall) return state;
1207
- const answer = await p.confirm({
1208
- message: "iii console gives engine-level visibility (workers, functions, queues, traces). Install now?",
1209
- initialValue: true
1210
- });
1211
- if (p.isCancel(answer)) return state;
1212
- if (answer === false) {
1213
- writePrefs({ skipConsoleInstall: true });
1214
- return state;
1215
- }
1216
- const shBin = whichBinary("sh");
1217
- const curlBin = whichBinary("curl");
1218
- if (!shBin || !curlBin) {
1219
- p.log.warn(`curl or sh not found. Install manually:\n ${iiiConsoleInstallHint()}`);
1220
- return state;
1221
- }
1222
- if (!runCommand(shBin, ["-c", III_CONSOLE_INSTALL_CMD], { label: "Installing iii console" })) {
1223
- p.log.warn(`iii console install failed. Re-run manually:\n ${iiiConsoleInstallHint()}`);
1224
- return state;
1225
- }
1226
- return detectIiiConsole();
1227
- }
1228
1833
  function adoptRunningEngine() {
1229
1834
  try {
1230
1835
  const existingState = readEngineState();
1231
1836
  const existingPid = readEnginePidfile();
1232
1837
  if (existingState && existingPid) return;
1233
1838
  const enginePid = findEnginePidsByPort(getRestPort())[0];
1839
+ if (enginePid) {
1840
+ const comm = pidCommand(enginePid);
1841
+ if (isForeignPortHolder(comm)) {
1842
+ vlog(`adoptRunningEngine: refusing to adopt pid ${enginePid} (${comm}) — not the iii engine binary`);
1843
+ return;
1844
+ }
1845
+ }
1234
1846
  if (enginePid && !existingPid) writeEnginePidfile(enginePid);
1235
1847
  if (!existingState) writeEngineState({
1236
1848
  kind: "native",
@@ -1254,7 +1866,7 @@ async function runIiiInstaller() {
1254
1866
  };
1255
1867
  }
1256
1868
  if (IS_WINDOWS || isZipAsset) {
1257
- 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}`);
1869
+ p.log.info(`Auto-install unavailable on ${platform()} — ${asset} isn't tar-compatible. Install manually:\n 1. Download ${releaseUrl}\n Its SHA-256 must be ${expectedIiiSha256(IIPINNED_VERSION, asset ?? "") ?? `the value in ${releaseUrl.replace(/\.zip$/, ".sha256")}`}\n 2. Extract iii.exe and place it on PATH (e.g. %USERPROFILE%\\.local\\bin)\nOr use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`);
1258
1870
  return {
1259
1871
  ok: false,
1260
1872
  binPath: null
@@ -1262,8 +1874,14 @@ async function runIiiInstaller() {
1262
1874
  }
1263
1875
  const shBin = whichBinary("sh");
1264
1876
  const curlBin = whichBinary("curl");
1265
- if (!shBin || !curlBin) {
1266
- p.log.warn("curl or sh not found. Cannot auto-install iii-engine.");
1877
+ const tarBin = whichBinary("tar");
1878
+ if (!shBin || !curlBin || !tarBin) {
1879
+ const missing = [
1880
+ !shBin && "sh",
1881
+ !curlBin && "curl",
1882
+ !tarBin && "tar"
1883
+ ].filter(Boolean).join(", ");
1884
+ p.log.warn(`${missing} not found. Cannot auto-install iii-engine.`);
1267
1885
  return {
1268
1886
  ok: false,
1269
1887
  binPath: null
@@ -1271,28 +1889,68 @@ async function runIiiInstaller() {
1271
1889
  }
1272
1890
  const binDir = agentmemoryBinDir();
1273
1891
  const binPath = privateIiiPath();
1274
- if (!runCommand(shBin, ["-c", [
1275
- `mkdir -p "${binDir}"`,
1276
- `curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
1277
- `chmod +x "${binPath}"`
1278
- ].join(" && ")], {
1279
- label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
1280
- optional: true
1281
- })) {
1282
- 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}.`);
1892
+ const label = `Installing iii-engine v${IIPINNED_VERSION} (pinned)`;
1893
+ const spinner = p.spinner();
1894
+ spinner.start(label);
1895
+ const outcome = installIiiArchive({
1896
+ sh: shBin,
1897
+ releaseUrl,
1898
+ version: IIPINNED_VERSION,
1899
+ asset: asset ?? "",
1900
+ binDir,
1901
+ binPath
1902
+ });
1903
+ if (!outcome.ok) {
1904
+ spinner.stop(`${label} ${pc.red("✗")}`);
1905
+ const sha256 = expectedIiiSha256(IIPINNED_VERSION, asset ?? "");
1906
+ const heading = outcome.reason === "checksum" ? "iii-engine download failed verification" : outcome.reason === "no-checksum" ? "iii-engine auto-install skipped" : "iii-engine download failed";
1907
+ const checksumHint = sha256 ? "" : `Compare the archive against ${releaseUrl.replace(/\.(tar\.gz|zip)$/, ".sha256")} before extracting.\n`;
1908
+ p.log.warn(`${heading}: ${outcome.detail.slice(0, 300)}\nInstall it manually, then re-run agentmemory:\n ${iiiManualInstallCommand(releaseUrl, binDir, binPath, sha256)}\n` + checksumHint + `Or use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`);
1283
1909
  return {
1284
1910
  ok: false,
1285
1911
  binPath: null
1286
1912
  };
1287
1913
  }
1914
+ spinner.stop(`${label} ${pc.green("✓")}`);
1288
1915
  return {
1289
1916
  ok: true,
1290
1917
  binPath
1291
1918
  };
1292
1919
  }
1293
1920
  let startupFailure = null;
1294
- function spawnEngineBackground(bin, spawnArgs, label) {
1295
- vlog(`spawn: ${bin} ${spawnArgs.join(" ")}`);
1921
+ let activeStartupStderr = createStartupStderrCapture();
1922
+ function printCapturedStartupStderr() {
1923
+ const stderr = redactCredentialUrls(activeStartupStderr.text().trim());
1924
+ if (IS_VERBOSE && stderr) p.note(stderr, "engine stderr");
1925
+ }
1926
+ function printDockerStartupLogs() {
1927
+ const state = readEngineState();
1928
+ if (state?.kind !== "docker") return;
1929
+ const inspection = inspectOwnedDockerEngine(state);
1930
+ if (inspection.status !== "running" && inspection.status !== "stopped") return;
1931
+ const dockerBin = whichBinary("docker");
1932
+ if (!dockerBin) return;
1933
+ const result = spawnSync(dockerBin, [
1934
+ "logs",
1935
+ "--tail",
1936
+ "80",
1937
+ inspection.containerId
1938
+ ], {
1939
+ encoding: "utf-8",
1940
+ stdio: [
1941
+ "ignore",
1942
+ "pipe",
1943
+ "pipe"
1944
+ ],
1945
+ timeout: 5e3,
1946
+ maxBuffer: 64 * 1024
1947
+ });
1948
+ const output = redactCredentialUrls(`${result.stdout ?? ""}${result.stderr ?? ""}`.trim());
1949
+ if (output) p.note(output.slice(-16 * 1024), "docker engine logs");
1950
+ }
1951
+ function spawnEngineBackground(bin, spawnArgs, label, cwd) {
1952
+ vlog(`spawn: ${bin} ${spawnArgs.join(" ")}${cwd ? ` (cwd: ${cwd})` : ""}`);
1953
+ activeStartupStderr = createStartupStderrCapture();
1296
1954
  const child = spawn(bin, spawnArgs, {
1297
1955
  detached: true,
1298
1956
  stdio: [
@@ -1300,22 +1958,18 @@ function spawnEngineBackground(bin, spawnArgs, label) {
1300
1958
  "ignore",
1301
1959
  "pipe"
1302
1960
  ],
1303
- windowsHide: true
1961
+ windowsHide: true,
1962
+ env: engineChildEnv(process.env),
1963
+ ...cwd ? { cwd } : {}
1304
1964
  });
1305
1965
  const isDocker = label.includes("Docker");
1306
1966
  if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
1307
- const stderrChunks = [];
1308
- let stderrBytes = 0;
1309
- const MAX_STDERR_CAPTURE = 16 * 1024;
1310
1967
  child.stderr?.on("data", (chunk) => {
1311
- if (stderrBytes >= MAX_STDERR_CAPTURE) return;
1312
- const slice = chunk.subarray(0, MAX_STDERR_CAPTURE - stderrBytes);
1313
- stderrChunks.push(slice);
1314
- stderrBytes += slice.length;
1968
+ activeStartupStderr.append(chunk);
1315
1969
  });
1316
1970
  child.on("exit", (code, signal) => {
1317
1971
  if (code !== null && code !== 0 || code === null && signal !== null) {
1318
- const stderr = Buffer.concat(stderrChunks).toString("utf-8");
1972
+ const stderr = redactCredentialUrls(activeStartupStderr.text());
1319
1973
  startupFailure = {
1320
1974
  kind: isDocker ? "docker-crashed" : "engine-crashed",
1321
1975
  stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
@@ -1323,22 +1977,100 @@ function spawnEngineBackground(bin, spawnArgs, label) {
1323
1977
  };
1324
1978
  vlog(`engine exited early: code=${code} signal=${signal}`);
1325
1979
  if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
1326
- if (!isDocker) clearEnginePidfile();
1327
- clearEngineState();
1980
+ if (!isDocker) {
1981
+ clearEnginePidfile();
1982
+ clearEngineState();
1983
+ }
1328
1984
  }
1329
1985
  });
1330
1986
  child.unref();
1331
1987
  return child;
1332
1988
  }
1989
+ function prepareEngineLaunch(configPath) {
1990
+ const home = homedir();
1991
+ const bundledConfig = isBundledConfig(configPath, __dirname);
1992
+ const cwd = resolveEngineCwd(configPath, process.cwd(), home, bundledConfig);
1993
+ const stateBackendKind = getStateBackend();
1994
+ try {
1995
+ mkdirSync(cwd, { recursive: true });
1996
+ } catch (err) {
1997
+ const failure = resolveLaunchRenderFailure(stateBackendKind, err);
1998
+ if (failure.fatal) {
1999
+ p.log.error(failure.message);
2000
+ process.exit(1);
2001
+ }
2002
+ return {
2003
+ configPath,
2004
+ cwd: process.cwd()
2005
+ };
2006
+ }
2007
+ try {
2008
+ const rawConfig = readFileSync(configPath, "utf-8");
2009
+ const options = {
2010
+ dataDir: dataDirResolution.dataDir,
2011
+ ports: {
2012
+ restPort: getRestPort(),
2013
+ streamPort: getStreamPort(),
2014
+ viewerPort: getConfiguredViewerPort(),
2015
+ enginePort: getEnginePort()
2016
+ },
2017
+ stateBackend: {
2018
+ kind: stateBackendKind,
2019
+ redisUrl: getRedisUrl()
2020
+ },
2021
+ saveIntervalMs: getStateSaveIntervalMs()
2022
+ };
2023
+ const rewritten = bundledConfig ? rewriteBundledConfig(rawConfig, home, process.execPath, join(__dirname, "index.mjs"), options) : renderEngineConfig(rawConfig, options);
2024
+ const runtimePath = runtimeConfigPath(dataDirResolution.dataDir);
2025
+ mkdirSync(dirname(runtimePath), { recursive: true });
2026
+ writeFileSync(runtimePath, rewritten, "utf-8");
2027
+ try {
2028
+ const cleared = clearPersistedBuiltinConfig(cwd, runtimePath, rewritten);
2029
+ if (cleared.length > 0) vlog(`cleared ${cleared.length} persisted builtin config entries so the rendered runtime config seeds the engine again`);
2030
+ } catch (err) {
2031
+ p.log.error(`${String(err instanceof Error ? err.message : err)}. The engine would ignore the rendered runtime config and keep its previously persisted ports and timeouts. Fix the permissions on that file and run agentmemory start again.`);
2032
+ process.exit(1);
2033
+ }
2034
+ if (selectedInstance === 0 && dataDirResolution.source === "default") {
2035
+ for (const m of legacyDataMigrations(process.cwd(), home, dataDirResolution.dataDir)) if (existsSync(m.from) && !existsSync(m.to)) try {
2036
+ mkdirSync(dirname(m.to), { recursive: true });
2037
+ cpSync(m.from, m.to, { recursive: true });
2038
+ p.log.info(`Copied existing data: ${m.from} -> ${m.to}`);
2039
+ } catch (err) {
2040
+ vlog(`data copy failed for ${m.from}: ${String(err)}`);
2041
+ }
2042
+ }
2043
+ return {
2044
+ configPath: runtimePath,
2045
+ cwd
2046
+ };
2047
+ } catch (err) {
2048
+ const failure = resolveLaunchRenderFailure(stateBackendKind, err);
2049
+ if (failure.fatal) {
2050
+ p.log.error(failure.message);
2051
+ process.exit(1);
2052
+ }
2053
+ vlog(`runtime config generation failed, using bundled config verbatim: ${String(err)}`);
2054
+ return {
2055
+ configPath,
2056
+ cwd
2057
+ };
2058
+ }
2059
+ }
1333
2060
  function startIiiBin(iiiBin, configPath) {
1334
2061
  const s = p.spinner();
1335
2062
  s.start(`Starting iii-engine: ${iiiBin}`);
2063
+ const launch = prepareEngineLaunch(configPath);
1336
2064
  writeEngineState({
1337
2065
  kind: "native",
1338
- configPath,
2066
+ configPath: launch.configPath,
1339
2067
  binPath: iiiBin
1340
2068
  });
1341
- spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
2069
+ spawnEngineBackground(iiiBin, [
2070
+ "--config",
2071
+ launch.configPath,
2072
+ "--no-update-check"
2073
+ ], "iii-engine", launch.cwd);
1342
2074
  s.stop(c.ok("iii-engine process started"));
1343
2075
  return true;
1344
2076
  }
@@ -1350,14 +2082,52 @@ function pickCompatibleIii(candidates) {
1350
2082
  }
1351
2083
  return null;
1352
2084
  }
2085
+ function warnIfDockerIgnoresStateBackend(stateBackendKind) {
2086
+ if (stateBackendKind !== "redis") return;
2087
+ p.log.warn("AGENTMEMORY_STATE_BACKEND=redis is not applied on the Docker path: docker-compose.yml mounts iii-config.docker.yaml read-only, so this start will not render it. Edit iii-config.docker.yaml by hand to point iii-state/iii-stream at redis (see README \"Storage backend\"), or it keeps using the file store. docker-compose.yml passes AGENTMEMORY_REDIS_URL into the engine container, so redis_url: '${AGENTMEMORY_REDIS_URL}' works in that file.");
2088
+ }
1353
2089
  async function startEngine() {
2090
+ let stateBackendKind;
2091
+ try {
2092
+ stateBackendKind = getStateBackend();
2093
+ } catch (err) {
2094
+ p.log.error(err instanceof Error ? err.message : String(err));
2095
+ process.exit(1);
2096
+ }
2097
+ if (stateBackendKind === "redis" && !getRedisUrl()) {
2098
+ p.log.error("AGENTMEMORY_STATE_BACKEND=redis requires AGENTMEMORY_REDIS_URL to be set (e.g. redis://localhost:6379). Set both in ~/.agentmemory/.env, or unset AGENTMEMORY_STATE_BACKEND to keep the default file store.");
2099
+ process.exit(1);
2100
+ }
2101
+ await assertRuntimePortOwnership();
2102
+ const persistedState = readEngineState();
2103
+ if (persistedState?.kind === "docker") {
2104
+ warnIfDockerIgnoresStateBackend(stateBackendKind);
2105
+ const inspection = inspectOwnedDockerEngine(persistedState);
2106
+ if (inspection.status === "unavailable" || inspection.status === "missing") {
2107
+ const detail = inspection.status === "unavailable" ? inspection.reason : "container is missing";
2108
+ p.log.error(`A saved Docker engine owns this lifecycle state, but it cannot be resumed safely (${detail}). Run \`agentmemory stop\` to reconcile that project before starting another engine.`);
2109
+ return false;
2110
+ }
2111
+ persistDockerInspection(persistedState, inspection);
2112
+ if (inspection.status === "running") return true;
2113
+ const dockerBin = whichBinary("docker");
2114
+ return Boolean(dockerBin && runCommand(dockerBin, ["start", inspection.containerId], { label: `Restarting owned Docker container ${inspection.containerId}` }));
2115
+ }
1354
2116
  const configPath = findIiiConfig();
2117
+ warnIfRelocatedDataDir();
1355
2118
  const pathIii = whichBinary("iii");
1356
2119
  vlog(`iii binary: ${pathIii ?? "(not on PATH)"}, config: ${configPath || "(not found)"}`);
2120
+ const dockerBin = whichBinary("docker");
2121
+ const composeFile = [
2122
+ join(__dirname, "..", "docker-compose.yml"),
2123
+ join(__dirname, "docker-compose.yml"),
2124
+ join(process.cwd(), "docker-compose.yml")
2125
+ ].find((c) => existsSync(c));
2126
+ const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
1357
2127
  const fallbacks = fallbackIiiPaths().filter((p) => existsSync(p));
1358
2128
  for (const f of fallbacks) vlog(`fallback iii at ${f} reports version: ${iiiBinVersion(f) ?? "unknown"}`);
1359
2129
  let iiiBin = pickCompatibleIii([pathIii, ...fallbacks]);
1360
- if (iiiBin && configPath) {
2130
+ if (iiiBin && configPath && !(dockerOptIn && dockerBin && composeFile)) {
1361
2131
  if (iiiBin !== pathIii) {
1362
2132
  p.log.info(`Using iii at: ${c.dim(iiiBin)} (v${c.accent(IIPINNED_VERSION)})`);
1363
2133
  process.env["PATH"] = `${dirname(iiiBin)}${delimiter}${process.env["PATH"] ?? ""}`;
@@ -1369,15 +2139,9 @@ async function startEngine() {
1369
2139
  startupFailure = { kind: "no-engine" };
1370
2140
  return false;
1371
2141
  }
1372
- const dockerBin = whichBinary("docker");
1373
2142
  vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
1374
- const composeFile = [
1375
- join(__dirname, "..", "docker-compose.yml"),
1376
- join(__dirname, "docker-compose.yml"),
1377
- join(process.cwd(), "docker-compose.yml")
1378
- ].find((c) => existsSync(c));
2143
+ mkdirSync(dataDirResolution.dataDir, { recursive: true });
1379
2144
  vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
1380
- const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
1381
2145
  const interactive = !!process.stdin.isTTY && !process.env["CI"];
1382
2146
  let choice;
1383
2147
  const pathIiiMismatch = pathIii !== null && resolveCompatibleIii(pathIii) === null;
@@ -1443,19 +2207,22 @@ async function startEngine() {
1443
2207
  }
1444
2208
  }
1445
2209
  if (choice === "docker" && dockerBin && composeFile) {
2210
+ warnIfDockerIgnoresStateBackend(stateBackendKind);
1446
2211
  const s = p.spinner();
1447
2212
  s.start("Starting iii-engine via Docker...");
2213
+ configureDockerHostUser();
2214
+ const projectName = dockerProjectName(getRestPort());
1448
2215
  writeEngineState({
1449
2216
  kind: "docker",
1450
- composeFile
1451
- });
1452
- spawnEngineBackground(dockerBin, [
1453
- "compose",
1454
- "-f",
2217
+ schemaVersion: 2,
1455
2218
  composeFile,
1456
- "up",
1457
- "-d"
1458
- ], "iii-engine via Docker");
2219
+ projectName,
2220
+ engineVersion: IIPINNED_VERSION,
2221
+ dataMountType: "bind",
2222
+ dataMountSource: dataDirResolution.dataDir,
2223
+ preserveContainer: false
2224
+ });
2225
+ spawnEngineBackground(dockerBin, dockerComposeArgs(composeFile, projectName, ["up", "-d"]), "iii-engine via Docker");
1459
2226
  s.stop("Docker compose started");
1460
2227
  return true;
1461
2228
  }
@@ -1471,25 +2238,65 @@ async function waitForEngine(timeoutMs) {
1471
2238
  }
1472
2239
  return false;
1473
2240
  }
2241
+ async function reconcilePersistedDockerEngine() {
2242
+ const state = readEngineState();
2243
+ if (state?.kind !== "docker") return false;
2244
+ const inspection = inspectOwnedDockerEngine(state);
2245
+ if (inspection.status === "unavailable") {
2246
+ p.log.error(`The saved Docker engine cannot be verified: ${inspection.reason}. Refusing to overwrite its lifecycle state. Run \`agentmemory stop\` after restoring Docker/compose access.`);
2247
+ process.exit(1);
2248
+ }
2249
+ if (inspection.status === "missing") {
2250
+ p.log.error("The saved Docker project no longer has an iii-engine container. Run `agentmemory stop` to remove its stale project state, then start agentmemory again.");
2251
+ process.exit(1);
2252
+ }
2253
+ persistDockerInspection(state, inspection);
2254
+ if (inspection.status === "stopped") {
2255
+ const dockerBin = whichBinary("docker");
2256
+ if (!dockerBin || !runCommand(dockerBin, ["start", inspection.containerId], { label: `Restarting owned Docker container ${inspection.containerId}` })) {
2257
+ p.log.error("The owned Docker container could not be restarted; lifecycle state was preserved.");
2258
+ process.exit(1);
2259
+ }
2260
+ }
2261
+ if (!await waitForEngine(5e3)) {
2262
+ printDockerStartupLogs();
2263
+ p.log.error(`The owned Docker container is running, but its REST API on port ${getRestPort()} is unhealthy. The saved project was preserved; inspect the Docker logs above or run \`agentmemory stop\`.`);
2264
+ process.exit(1);
2265
+ }
2266
+ await startWorkerForEngineState();
2267
+ if (!await waitForAgentmemoryReady(15e3)) {
2268
+ p.log.error("agentmemory worker did not become ready within 15s.");
2269
+ process.exit(1);
2270
+ }
2271
+ await maybeOfferGlobalInstall();
2272
+ printReadyHint();
2273
+ return true;
2274
+ }
1474
2275
  function installInstructions() {
1475
2276
  const releaseUrl = iiiReleaseUrl();
1476
- if (IS_WINDOWS) return [
1477
- `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
1478
- "",
1479
- " A) Download the prebuilt Windows binary:",
1480
- ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
1481
- ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
1482
- " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
1483
- " 4. Re-run: npx @agentmemory/agentmemory",
1484
- "",
1485
- ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
1486
- " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
1487
- "",
1488
- "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
1489
- "",
1490
- "Docs: https://iii.dev/docs"
1491
- ];
1492
- const linuxInstall = releaseUrl ? ` A) mkdir -p ~/.agentmemory/bin && curl -fsSL "${releaseUrl}" | tar -xz -C ~/.agentmemory/bin && chmod +x ~/.agentmemory/bin/iii` : ` A) Manual download: https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
2277
+ if (IS_WINDOWS) {
2278
+ const asset = iiiReleaseAsset() ?? "";
2279
+ const sha256 = expectedIiiSha256(IIPINNED_VERSION, asset);
2280
+ const verify = sha256 ? ` Verify before extracting: (Get-FileHash .\\${asset}).Hash must be ${sha256.toUpperCase()}` : ` Verify before extracting against the matching .sha256 file on the release page`;
2281
+ return [
2282
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
2283
+ "",
2284
+ " A) Download the prebuilt Windows binary:",
2285
+ ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
2286
+ ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
2287
+ verify,
2288
+ " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
2289
+ " 4. Re-run: npx @agentmemory/agentmemory",
2290
+ "",
2291
+ ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
2292
+ " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
2293
+ "",
2294
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
2295
+ "",
2296
+ "Docs: https://iii.dev/docs"
2297
+ ];
2298
+ }
2299
+ const linuxInstall = releaseUrl ? ` A) ${iiiManualInstallCommand(releaseUrl, agentmemoryBinDir(), privateIiiPath(), expectedIiiSha256(IIPINNED_VERSION, iiiReleaseAsset() ?? ""))}` : ` A) Manual download: https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
1493
2300
  return [
1494
2301
  `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
1495
2302
  "",
@@ -1516,7 +2323,8 @@ async function waitForAgentmemoryReady(timeoutMs) {
1516
2323
  return false;
1517
2324
  }
1518
2325
  function getEngineHost() {
1519
- for (const envKey of ["III_ENGINE_URL", "AGENTMEMORY_URL"]) {
2326
+ const envKeys = shouldUseAgentmemoryUrl() ? ["III_ENGINE_URL", "AGENTMEMORY_URL"] : ["III_ENGINE_URL"];
2327
+ for (const envKey of envKeys) {
1520
2328
  const raw = process.env[envKey];
1521
2329
  if (!raw) continue;
1522
2330
  try {
@@ -1526,13 +2334,16 @@ function getEngineHost() {
1526
2334
  }
1527
2335
  return "localhost";
1528
2336
  }
1529
- function printReadyHint(consoleState) {
2337
+ function printReadyHint() {
1530
2338
  const restUrl = getBaseUrl();
1531
2339
  const viewerUrl = getViewerUrl();
1532
2340
  const engineHost = getEngineHost();
1533
2341
  const streamUrl = `ws://${engineHost}:${getStreamPort()}`;
1534
2342
  const engineUrl = `ws://${engineHost}:${getEnginePort()}`;
1535
- const consoleLine = consoleState.kind === "installed" ? `${c.label("iii console")} ${c.dim(consoleState.binPath)} ${c.dim(`(run: ${consoleState.binPath} -p <port>)`)}` : `${c.label("iii console")} ${c.dim(`(install: ${iiiConsoleInstallHint()})`)}`;
2343
+ const invocation = isInvokedViaNpx() ? "npx @agentmemory/agentmemory" : "agentmemory";
2344
+ const consolePort = defaultConsolePort(getConfiguredViewerPort());
2345
+ const portFlags = portFlagSuffix(getRestPort(), selectedInstance);
2346
+ const consoleLine = `${c.label("iii console")} ${c.cmd(`${invocation} console${portFlags}`)} ${c.dim(`(serves on :${consolePort}; first run downloads the console next to the pinned iii)`)}`;
1536
2347
  const lines = [
1537
2348
  `${c.label("REST API")} ${c.url(restUrl)}`,
1538
2349
  `${c.label("Viewer")} ${c.url(viewerUrl)}`,
@@ -1541,10 +2352,17 @@ function printReadyHint(consoleState) {
1541
2352
  consoleLine
1542
2353
  ];
1543
2354
  p.note(lines.join("\n"), `agentmemory v${c.accent(VERSION)}`);
1544
- const demoCommand = isInvokedViaNpx() ? "npx @agentmemory/agentmemory demo" : "agentmemory demo";
2355
+ const demoCommand = `${invocation} demo${portFlags}`;
1545
2356
  process.stdout.write(`\n${c.dim("Try:")} ${c.cmd(demoCommand)}\n`);
1546
2357
  }
1547
2358
  async function main() {
2359
+ await assertRuntimePortOwnership();
2360
+ try {
2361
+ if ((await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(1500) })).ok) {
2362
+ p.log.error(`agentmemory is already running on port ${getRestPort()}. Starting a second instance here would corrupt the running daemon's REST routing. Use the REST API (or the MCP tools) against the running instance, run a different --instance, or stop it first with \`agentmemory stop\`.`);
2363
+ process.exit(1);
2364
+ }
2365
+ } catch {}
1548
2366
  if (IS_RESET) resetPrefs();
1549
2367
  const firstRun = isFirstRun();
1550
2368
  const prefs = readPrefs();
@@ -1552,14 +2370,14 @@ async function main() {
1552
2370
  if (firstRun || IS_RESET) await runOnboarding();
1553
2371
  if (skipEngine) {
1554
2372
  if (IS_VERBOSE) p.log.info("Skipping engine check (--no-engine)");
1555
- await import("./src-CzgoepGU.mjs");
2373
+ await import("./src-Jsq9LCeH.mjs");
1556
2374
  if (await waitForAgentmemoryReady(15e3)) {
1557
- const consoleState = await ensureIiiConsole();
1558
2375
  await maybeOfferGlobalInstall();
1559
- printReadyHint(consoleState);
2376
+ printReadyHint();
1560
2377
  }
1561
2378
  return;
1562
2379
  }
2380
+ if (await reconcilePersistedDockerEngine()) return;
1563
2381
  if (await isEngineRunning()) {
1564
2382
  if (IS_VERBOSE) p.log.success("iii-engine is running");
1565
2383
  const persisted = readEngineState();
@@ -1567,12 +2385,13 @@ async function main() {
1567
2385
  const detected = attachedBin ? iiiBinVersion(attachedBin) : null;
1568
2386
  if (detected === IIPINNED_VERSION) {
1569
2387
  adoptRunningEngine();
1570
- await import("./src-CzgoepGU.mjs");
1571
- if (await waitForAgentmemoryReady(15e3)) {
1572
- const consoleState = await ensureIiiConsole();
1573
- await maybeOfferGlobalInstall();
1574
- printReadyHint(consoleState);
2388
+ await startWorkerForEngineState();
2389
+ if (!await waitForAgentmemoryReady(15e3)) {
2390
+ p.log.error("agentmemory worker did not become ready within 15s.");
2391
+ process.exit(1);
1575
2392
  }
2393
+ await maybeOfferGlobalInstall();
2394
+ printReadyHint();
1576
2395
  return;
1577
2396
  }
1578
2397
  const detectedLabel = detected ? `v${detected}` : "an unverified version";
@@ -1592,7 +2411,7 @@ async function main() {
1592
2411
  ` ${c.cmd(base)}`,
1593
2412
  "",
1594
2413
  c.dim(`Step 2 needs no manual install. To install iii v${IIPINNED_VERSION} yourself (replaces your global iii), curl:`),
1595
- ` ${c.cmd(III_CONSOLE_INSTALL_CMD)}`,
2414
+ ` ${c.cmd(IIIENGINE_INSTALL_CMD)}`,
1596
2415
  ` ${c.dim("or download the release:")} ${c.url(`https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`)}`
1597
2416
  ].join("\n"), "engine conflict");
1598
2417
  process.exit(1);
@@ -1609,6 +2428,7 @@ async function main() {
1609
2428
  if (!await waitForEngine(15e3)) {
1610
2429
  const port = getRestPort();
1611
2430
  s.stop("iii-engine did not become ready within 15s");
2431
+ printDockerStartupLogs();
1612
2432
  if (startupFailure?.kind === "engine-crashed" || startupFailure?.kind === "docker-crashed") {
1613
2433
  p.log.error("The iii-engine process crashed on startup.");
1614
2434
  if (startupFailure.binary) p.log.info(`Binary: ${startupFailure.binary}`);
@@ -1625,6 +2445,7 @@ async function main() {
1625
2445
  ].join("\n"), "Troubleshooting");
1626
2446
  } else {
1627
2447
  p.log.error("The engine process started but the REST API never responded.");
2448
+ printCapturedStartupStderr();
1628
2449
  p.note([
1629
2450
  `Check whether port ${port} is already bound by another process:`,
1630
2451
  portInUseDiagnostic(port),
@@ -1638,19 +2459,18 @@ async function main() {
1638
2459
  process.exit(1);
1639
2460
  }
1640
2461
  s.stop(c.ok("iii-engine is ready"));
1641
- await import("./src-CzgoepGU.mjs");
1642
- if (await waitForAgentmemoryReady(15e3)) {
1643
- const consoleState = await ensureIiiConsole();
1644
- await maybeOfferGlobalInstall();
1645
- printReadyHint(consoleState);
2462
+ await startWorkerForEngineState();
2463
+ if (!await waitForAgentmemoryReady(15e3)) {
2464
+ p.log.error("agentmemory worker did not become ready within 15s.");
2465
+ process.exit(1);
1646
2466
  }
2467
+ await maybeOfferGlobalInstall();
2468
+ printReadyHint();
1647
2469
  writePrefs({ skipSplash: true });
1648
2470
  }
1649
2471
  async function apiFetch(base, path, timeoutMs = 5e3) {
1650
2472
  try {
1651
- const headers = {};
1652
- const secret = process.env["AGENTMEMORY_SECRET"];
1653
- if (secret) headers["Authorization"] = `Bearer ${secret}`;
2473
+ const headers = bearerHeaders(base);
1654
2474
  return await (await fetch(`${base}/agentmemory/${path}`, {
1655
2475
  signal: AbortSignal.timeout(timeoutMs),
1656
2476
  headers
@@ -1939,7 +2759,7 @@ async function passiveServerChecks() {
1939
2759
  }, {
1940
2760
  name: "Embedding provider",
1941
2761
  ok: hasEmbed,
1942
- hint: hasEmbed ? void 0 : "Running BM25-only. Add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST"
2762
+ hint: hasEmbed ? void 0 : "Running BM25-only. Set EMBEDDING_PROVIDER=local for on-device semantic search, or add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST"
1943
2763
  });
1944
2764
  for (const f of flags?.flags || []) checks.push({
1945
2765
  name: f.label,
@@ -2153,7 +2973,10 @@ async function postJson(url, body, timeoutMs = 5e3) {
2153
2973
  try {
2154
2974
  const res = await fetch(url, {
2155
2975
  method: "POST",
2156
- headers: { "Content-Type": "application/json" },
2976
+ headers: {
2977
+ "Content-Type": "application/json",
2978
+ ...bearerHeaders(url)
2979
+ },
2157
2980
  body: JSON.stringify(body),
2158
2981
  signal: AbortSignal.timeout(timeoutMs)
2159
2982
  });
@@ -2166,7 +2989,10 @@ async function postJson(url, body, timeoutMs = 5e3) {
2166
2989
  async function postJsonStrict(url, body, timeoutMs = 5e3) {
2167
2990
  const res = await fetch(url, {
2168
2991
  method: "POST",
2169
- headers: { "Content-Type": "application/json" },
2992
+ headers: {
2993
+ "Content-Type": "application/json",
2994
+ ...bearerHeaders(url)
2995
+ },
2170
2996
  body: JSON.stringify(body),
2171
2997
  signal: AbortSignal.timeout(timeoutMs)
2172
2998
  });
@@ -2201,7 +3027,10 @@ async function seedDemoSession(base, project, session) {
2201
3027
  try {
2202
3028
  const res = await fetch(url, {
2203
3029
  method: "POST",
2204
- headers: { "Content-Type": "application/json" },
3030
+ headers: {
3031
+ "Content-Type": "application/json",
3032
+ ...bearerHeaders(url)
3033
+ },
2205
3034
  body: JSON.stringify(payload),
2206
3035
  signal: AbortSignal.timeout(5e3)
2207
3036
  });
@@ -2273,6 +3102,7 @@ async function runInit() {
2273
3102
  p.outro(`Edit ${target} and you're set.`);
2274
3103
  }
2275
3104
  async function startServerForDemo() {
3105
+ await assertRuntimePortOwnership();
2276
3106
  if (await isAgentmemoryReady()) return async () => {};
2277
3107
  const startedEngine = !await isEngineRunning();
2278
3108
  if (startedEngine) {
@@ -2282,11 +3112,13 @@ async function startServerForDemo() {
2282
3112
  process.exit(1);
2283
3113
  }
2284
3114
  if (!await waitForEngine(15e3)) {
3115
+ printCapturedStartupStderr();
3116
+ printDockerStartupLogs();
2285
3117
  p.log.error("iii-engine did not become ready within 15s.");
2286
3118
  process.exit(1);
2287
3119
  }
2288
3120
  }
2289
- await import("./src-CzgoepGU.mjs");
3121
+ await startWorkerForEngineState();
2290
3122
  if (!await waitForAgentmemoryReady(15e3)) {
2291
3123
  p.log.error("agentmemory worker did not become ready within 15s.");
2292
3124
  process.exit(1);
@@ -2296,7 +3128,7 @@ async function startServerForDemo() {
2296
3128
  const port = getRestPort();
2297
3129
  const state = readEngineState();
2298
3130
  if (state?.kind === "docker") {
2299
- await stopDockerEngine(state.composeFile, port).catch(() => {});
3131
+ await stopDockerEngine(state, port).catch(() => {});
2300
3132
  return;
2301
3133
  }
2302
3134
  const pids = new Set(findEnginePidsByPort(port));
@@ -2346,6 +3178,7 @@ async function runDemoBody(base) {
2346
3178
  const results = [];
2347
3179
  for (const query of queries) results.push(await runDemoSearch(base, query));
2348
3180
  sQuery.stop("Search complete");
3181
+ const semanticHits = results.find((r) => r.query === "database performance optimization")?.hits ?? 0;
2349
3182
  const lines = [
2350
3183
  `Project: ${demoProject}`,
2351
3184
  `Sessions: ${sessions.length} seeded (${totalObs} observations)`,
@@ -2353,8 +3186,11 @@ async function runDemoBody(base) {
2353
3186
  c.label("Search results:"),
2354
3187
  ...results.flatMap((r) => [` ${c.label(`"${r.query}"`)}`, ` ${c.dim("→")} ${c.ok(`${r.hits} hit(s)`)}, top: ${r.topTitle.slice(0, 60)}`]),
2355
3188
  "",
2356
- c.accent(`Notice: searching "database performance optimization"`),
2357
- c.accent(`found the N+1 query fix — keyword matching can't do that.`),
3189
+ ...semanticHits > 0 ? [c.accent(`Notice: searching "database performance optimization"`), c.accent(`found the N+1 query fix — keyword matching can't do that.`)] : [
3190
+ c.dim(`Note: "database performance optimization" found nothing —`),
3191
+ c.dim(`set EMBEDDING_PROVIDER=local for on-device semantic recall,`),
3192
+ c.dim(`or add an embedding API key in ~/.agentmemory/.env.`)
3193
+ ],
2358
3194
  "",
2359
3195
  `Viewer: ${c.url(getViewerUrl())}`,
2360
3196
  `Clean up with: ${c.dim(`curl -X DELETE "${base}/agentmemory/sessions?project=${demoProject}"`)}`
@@ -2403,14 +3239,22 @@ async function runUpgrade() {
2403
3239
  };
2404
3240
  if (hasPackageJson) if (!!pnpmBin && hasPnpmLock && pnpmBin) {
2405
3241
  requireSuccess(runCommand(pnpmBin, ["install"], { label: "Refreshing dependencies (pnpm install)" }), "pnpm install");
2406
- runCommand(pnpmBin, ["up", "iii-sdk@0.11.2"], {
2407
- label: "Pinning iii-sdk@0.11.2",
3242
+ runCommand(pnpmBin, [
3243
+ "up",
3244
+ `iii-sdk@${IIPINNED_DEFAULT_VERSION}`,
3245
+ `@iii-dev/helpers@${IIPINNED_DEFAULT_VERSION}`
3246
+ ], {
3247
+ label: `Pinning iii-sdk and @iii-dev/helpers at ${IIPINNED_DEFAULT_VERSION}`,
2408
3248
  optional: true
2409
3249
  });
2410
3250
  } else if (npmBin) {
2411
3251
  requireSuccess(runCommand(npmBin, ["install"], { label: "Refreshing dependencies (npm install)" }), "npm install");
2412
- runCommand(npmBin, ["install", "iii-sdk@0.11.2"], {
2413
- label: "Pinning iii-sdk@0.11.2",
3252
+ runCommand(npmBin, [
3253
+ "install",
3254
+ `iii-sdk@${IIPINNED_DEFAULT_VERSION}`,
3255
+ `@iii-dev/helpers@${IIPINNED_DEFAULT_VERSION}`
3256
+ ], {
3257
+ label: `Pinning iii-sdk and @iii-dev/helpers at ${IIPINNED_DEFAULT_VERSION}`,
2414
3258
  optional: true
2415
3259
  });
2416
3260
  } else p.log.warn("No package manager found (pnpm/npm). Skipping JS dependency upgrade.");
@@ -2442,6 +3286,22 @@ async function runUpgrade() {
2442
3286
  function pidAlive(pid) {
2443
3287
  try {
2444
3288
  process.kill(pid, 0);
3289
+ if (!IS_WINDOWS) try {
3290
+ const stat = execFileSync("ps", [
3291
+ "-p",
3292
+ String(pid),
3293
+ "-o",
3294
+ "stat="
3295
+ ], {
3296
+ encoding: "utf-8",
3297
+ stdio: [
3298
+ "ignore",
3299
+ "pipe",
3300
+ "ignore"
3301
+ ]
3302
+ });
3303
+ if (stat.trim() && !processStatIsRunning(stat)) return false;
3304
+ } catch {}
2445
3305
  return true;
2446
3306
  } catch (err) {
2447
3307
  return err?.code === "EPERM";
@@ -2476,6 +3336,95 @@ async function signalAndWait(pid, initialSignal, timeoutMs) {
2476
3336
  await new Promise((r) => setTimeout(r, 200));
2477
3337
  return !pidAlive(pid);
2478
3338
  }
3339
+ async function waitForEngineFlush() {
3340
+ let stateBackend = "file";
3341
+ try {
3342
+ stateBackend = getStateBackend();
3343
+ } catch {}
3344
+ const runtimePath = runtimeConfigPath(dataDirResolution.dataDir);
3345
+ const configTexts = [];
3346
+ for (const path of engineStateConfigPaths(join(homedir(), ".agentmemory"), runtimePath)) try {
3347
+ configTexts.push(readFileSync(path, "utf-8"));
3348
+ } catch {}
3349
+ const waitMs = engineFlushWaitMs(stateBackend, configTexts);
3350
+ if (waitMs <= 0) return;
3351
+ const s = p.spinner();
3352
+ s.start(`Waiting ${(waitMs / 1e3).toFixed(1)}s for iii-engine to write state to disk...`);
3353
+ await new Promise((r) => setTimeout(r, waitMs));
3354
+ s.stop("iii-engine state flush window passed");
3355
+ }
3356
+ async function stopWorkerPid(pid, graceMs) {
3357
+ const s = p.spinner();
3358
+ s.start(`Stopping agentmemory worker (pid ${pid})... [flushing state]`);
3359
+ const ok = await signalAndWait(pid, "SIGTERM", graceMs);
3360
+ s.stop(ok ? `Stopped worker pid ${pid}` : `Failed to stop worker pid ${pid}`);
3361
+ return ok;
3362
+ }
3363
+ async function stopNativeEngineForRemoval() {
3364
+ const port = getRestPort();
3365
+ const workerPid = readWorkerPidfile();
3366
+ const enginePids = new Set(findEnginePidsByPort(port));
3367
+ const pidfilePid = readEnginePidfile();
3368
+ if (pidfilePid) enginePids.add(pidfilePid);
3369
+ if (workerPid) enginePids.delete(workerPid);
3370
+ const foreign = [...enginePids].map((pid) => ({
3371
+ pid,
3372
+ comm: pidCommand(pid)
3373
+ })).filter(({ comm }) => isForeignPortHolder(comm));
3374
+ if (foreign.length > 0) return {
3375
+ ok: false,
3376
+ reason: `refusing to signal non-iii process(es): ${foreign.map(({ pid, comm }) => `${pid} (${comm})`).join(", ")}`
3377
+ };
3378
+ if (workerPid) {
3379
+ if (!await stopWorkerPid(workerPid, 5e3)) return {
3380
+ ok: false,
3381
+ reason: `worker pid ${workerPid} could not be stopped; no engine or files were removed`
3382
+ };
3383
+ clearWorkerPidfile();
3384
+ }
3385
+ for (const pid of enginePids) {
3386
+ const s = p.spinner();
3387
+ s.start(`Stopping iii-engine (pid ${pid})...`);
3388
+ const ok = await signalAndWait(pid, "SIGTERM", 3e3);
3389
+ s.stop(ok ? `Stopped pid ${pid}` : `Failed to stop pid ${pid}`);
3390
+ if (!ok) return {
3391
+ ok: false,
3392
+ reason: `engine pid ${pid} survived shutdown; lifecycle state and files were preserved`
3393
+ };
3394
+ }
3395
+ clearEnginePidfile();
3396
+ clearEngineState();
3397
+ clearWorkerPidfile();
3398
+ return {
3399
+ ok: true,
3400
+ stoppedEnginePids: enginePids.size
3401
+ };
3402
+ }
3403
+ function pidCommand(pid) {
3404
+ if (IS_WINDOWS) return "";
3405
+ try {
3406
+ return execFileSync("ps", [
3407
+ "-p",
3408
+ String(pid),
3409
+ "-o",
3410
+ "comm="
3411
+ ], {
3412
+ encoding: "utf-8",
3413
+ stdio: [
3414
+ "ignore",
3415
+ "pipe",
3416
+ "ignore"
3417
+ ]
3418
+ }).trim();
3419
+ } catch {
3420
+ return "";
3421
+ }
3422
+ }
3423
+ function isForeignPortHolder(comm) {
3424
+ if (!comm) return false;
3425
+ const base = comm.split("/").pop() || comm;
3426
+ return base !== "iii" && !base.startsWith("iii-");
3427
+ }
2479
3428
  function findEnginePidsByPort(port) {
2480
3429
  if (IS_WINDOWS) return [];
2481
3430
  const lsof = whichBinary("lsof");
@@ -2500,47 +3449,80 @@ function findEnginePidsByPort(port) {
2500
3449
  return [];
2501
3450
  }
2502
3451
  }
2503
- async function stopDockerEngine(composeFile, port) {
3452
+ async function stopDockerEngine(state, port, suppressOutro = false) {
2504
3453
  const dockerBin = whichBinary("docker");
2505
3454
  if (!dockerBin) {
2506
- 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`);
3455
+ p.log.error("Engine was started via Docker, but `docker` is no longer on PATH. Lifecycle state was preserved.");
2507
3456
  process.exit(1);
2508
3457
  }
2509
- if (!existsSync(composeFile)) {
2510
- 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)`);
3458
+ const inspection = inspectOwnedDockerEngine(state);
3459
+ if (inspection.status === "unavailable") {
3460
+ p.log.error(`The saved Docker engine cannot be verified: ${inspection.reason}. Refusing to stop or clear ownership state.`);
3461
+ process.exit(1);
3462
+ }
3463
+ const resolvedState = persistDockerInspection(state, inspection);
3464
+ const workerPid = readWorkerPidfile();
3465
+ if (workerPid && !await stopWorkerPid(workerPid, 5e3)) {
3466
+ p.log.error("The agentmemory worker could not be stopped; Docker ownership state was preserved.");
2511
3467
  process.exit(1);
2512
3468
  }
2513
- const ok = runCommand(dockerBin, [
2514
- "compose",
2515
- "-f",
2516
- composeFile,
2517
- "down"
2518
- ], { label: `docker compose -f ${composeFile} down` });
2519
- clearEnginePidfile();
2520
- clearEngineState();
2521
3469
  clearWorkerPidfile();
2522
- if (!ok) {
2523
- p.log.error(`docker compose down failed. The engine may still be running on :${port}. Inspect with:\n docker compose -f ${composeFile} ps`);
3470
+ if (inspection.status === "running" || inspection.status === "stopped") {
3471
+ if (inspection.status === "running" && !runCommand(dockerBin, [
3472
+ "stop",
3473
+ "--time",
3474
+ "10",
3475
+ inspection.containerId
3476
+ ], { label: `Stopping preserved Docker container ${inspection.containerId}` })) {
3477
+ p.log.error("The Docker container could not be stopped; ownership state was preserved.");
3478
+ process.exit(1);
3479
+ }
3480
+ clearEnginePidfile();
3481
+ writeEngineState(resolvedState);
3482
+ if (!suppressOutro) p.outro("Stopped. The validated Docker container and data mount were preserved for a lossless restart.");
3483
+ return;
3484
+ }
3485
+ const projectName = inspection.projectName ?? resolvedState.projectName;
3486
+ if (!projectName) {
3487
+ p.log.error("No verified Docker Compose project name is available. Refusing an unscoped stop.");
2524
3488
  process.exit(1);
2525
3489
  }
2526
- p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
3490
+ const effectiveComposeFile = existsSync(inspection.composeFile) ? inspection.composeFile : discoverComposeFile();
3491
+ if (!effectiveComposeFile) {
3492
+ p.log.error("No agentmemory compose file is available. Docker ownership state was preserved.");
3493
+ process.exit(1);
3494
+ }
3495
+ const composeText = readFileSync(effectiveComposeFile, "utf-8");
3496
+ const ownServices = ["iii-engine", "iii-init"].filter((service) => new RegExp(`^\\s+${service}:`, "m").test(composeText));
3497
+ if (ownServices.length === 0) {
3498
+ p.log.error(`${effectiveComposeFile} does not define iii-engine or iii-init. Refusing an unscoped stop.`);
3499
+ process.exit(1);
3500
+ }
3501
+ const composeCommand = ["docker", ...dockerComposeArgs(effectiveComposeFile, projectName, [])].join(" ");
3502
+ if (runCommand(dockerBin, dockerComposeArgs(effectiveComposeFile, projectName, [
3503
+ "rm",
3504
+ "-s",
3505
+ "-f",
3506
+ ...ownServices
3507
+ ]), { label: `${composeCommand} rm -s -f ${ownServices.join(" ")}` })) {
3508
+ clearEnginePidfile();
3509
+ clearEngineState();
3510
+ } else {
3511
+ p.log.error(`docker compose rm failed. The engine may still be running on :${port}. Inspect with:\n ${composeCommand} ps`);
3512
+ process.exit(1);
3513
+ }
3514
+ if (!suppressOutro) p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
2527
3515
  }
2528
3516
  async function runStop() {
3517
+ await assertRuntimePortOwnership();
2529
3518
  p.intro("agentmemory stop");
2530
3519
  const port = getRestPort();
2531
3520
  const state = readEngineState();
2532
3521
  const running = await isEngineRunning();
2533
3522
  const force = args.includes("--force");
2534
3523
  if (state?.kind === "docker") {
2535
- if (!running) {
2536
- p.log.info(`No engine responding on port ${port}.`);
2537
- clearEnginePidfile();
2538
- clearEngineState();
2539
- clearWorkerPidfile();
2540
- p.outro("Nothing to stop.");
2541
- return;
2542
- }
2543
- await stopDockerEngine(state.composeFile, port);
3524
+ if (!running) p.log.info(`No engine responding on port ${port}.`);
3525
+ await stopDockerEngine(state, port);
2544
3526
  return;
2545
3527
  }
2546
3528
  const portPids = findEnginePidsByPort(port);
@@ -2557,7 +3539,7 @@ async function runStop() {
2557
3539
  if (workerPid !== null && portPids.length === 0 && pidfilePid === null) {
2558
3540
  const s = p.spinner();
2559
3541
  s.start(`Stopping orphaned agentmemory worker (pid ${workerPid})...`);
2560
- const ok = await signalAndWait(workerPid, "SIGTERM", 3e3);
3542
+ const ok = await signalAndWait(workerPid, "SIGTERM", SHUTDOWN_HARD_EXIT_MS + 1e3);
2561
3543
  s.stop(ok ? `Stopped worker pid ${workerPid}` : `Failed to stop worker pid ${workerPid}`);
2562
3544
  clearEnginePidfile();
2563
3545
  clearEngineState();
@@ -2594,15 +3576,19 @@ async function runStop() {
2594
3576
  process.exit(1);
2595
3577
  }
2596
3578
  let allStopped = true;
2597
- for (const pid of workerCandidates) {
2598
- const s = p.spinner();
2599
- s.start(`Stopping agentmemory worker (pid ${pid})... [flushing state]`);
2600
- const ok = await signalAndWait(pid, "SIGTERM", 5e3);
2601
- s.stop(ok ? `Stopped worker pid ${pid}` : `Failed to stop worker pid ${pid}`);
2602
- if (!ok) allStopped = false;
2603
- }
3579
+ for (const pid of workerCandidates) if (!await stopWorkerPid(pid, 9e3)) allStopped = false;
3580
+ if (workerCandidates.size > 0 && [...candidates].some((pid) => !workerCandidates.has(pid))) await waitForEngineFlush();
3581
+ const skippedForeign = [];
2604
3582
  for (const pid of candidates) {
2605
3583
  if (workerCandidates.has(pid)) continue;
3584
+ const comm = pidCommand(pid);
3585
+ if (!force && isForeignPortHolder(comm)) {
3586
+ skippedForeign.push({
3587
+ pid,
3588
+ comm
3589
+ });
3590
+ continue;
3591
+ }
2606
3592
  const s = p.spinner();
2607
3593
  s.start(`Stopping iii-engine (pid ${pid})...`);
2608
3594
  const ok = await signalAndWait(pid, "SIGTERM", 3e3);
@@ -2612,6 +3598,11 @@ async function runStop() {
2612
3598
  clearEnginePidfile();
2613
3599
  clearEngineState();
2614
3600
  clearWorkerPidfile();
3601
+ if (skippedForeign.length > 0) {
3602
+ const list = skippedForeign.map((sf) => ` pid ${sf.pid} ${sf.comm}`).join("\n");
3603
+ p.log.error(`Refused to signal process(es) holding :${port} that are not the iii engine:\n${list}\n\nIf the engine runs in Docker, stop it there:\n docker compose ps && docker compose rm -s -f <service>\n\nOr re-run with --force to signal them anyway.`);
3604
+ process.exit(1);
3605
+ }
2615
3606
  if (!allStopped) {
2616
3607
  p.log.error("One or more processes survived SIGKILL. Investigate with `ps`.");
2617
3608
  process.exit(1);
@@ -2619,14 +3610,18 @@ async function runStop() {
2619
3610
  p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
2620
3611
  }
2621
3612
  async function runMcp() {
2622
- await import("./standalone-YsZFFQBM.mjs");
3613
+ await import("./standalone-DKCdrf1d.mjs");
2623
3614
  }
2624
3615
  async function runConnectCmd() {
2625
- const { runConnect } = await import("./connect-JnsJXc68.mjs").then((n) => n.n);
3616
+ const { runConnect } = await import("./connect-B4_ca6BD.mjs").then((n) => n.n);
2626
3617
  await runConnect(args.slice(1));
2627
3618
  }
2628
3619
  async function runImportJsonl() {
2629
- const VALUE_FLAGS = new Set(["--port", "--tools"]);
3620
+ const VALUE_FLAGS = new Set([
3621
+ "--port",
3622
+ "--tools",
3623
+ "--data-dir"
3624
+ ]);
2630
3625
  let maxFiles;
2631
3626
  const tail = args.slice(1);
2632
3627
  const positional = [];
@@ -2675,11 +3670,12 @@ async function runImportJsonl() {
2675
3670
  process.exit(1);
2676
3671
  }
2677
3672
  const body = {};
2678
- if (pathArg) body["path"] = pathArg;
3673
+ if (pathArg) body["path"] = resolve(pathArg.startsWith("~") ? join(homedir(), pathArg.slice(1)) : pathArg);
2679
3674
  if (maxFiles !== void 0) body["maxFiles"] = maxFiles;
2680
- const headers = { "content-type": "application/json" };
2681
- const secret = process.env["AGENTMEMORY_SECRET"];
2682
- if (secret) headers["authorization"] = `Bearer ${secret}`;
3675
+ const headers = {
3676
+ "content-type": "application/json",
3677
+ ...bearerHeaders(base)
3678
+ };
2683
3679
  p.log.info(`Importing JSONL from ${pathArg || "~/.claude/projects"}…`);
2684
3680
  const spinner = p.spinner();
2685
3681
  spinner.start("scanning files");
@@ -2766,20 +3762,46 @@ function safeDelete(path) {
2766
3762
  }
2767
3763
  }
2768
3764
  async function runRemove() {
3765
+ if (selectedInstance !== 0) {
3766
+ p.intro("agentmemory remove");
3767
+ p.log.error(`remove is a global uninstall and cannot target instance ${selectedInstance}. Stop that daemon with \`agentmemory stop --instance ${selectedInstance}\`, then run \`agentmemory remove\` without --instance when you intend to uninstall shared assets.`);
3768
+ process.exit(1);
3769
+ }
3770
+ await assertRuntimePortOwnership();
2769
3771
  p.intro("agentmemory remove");
2770
3772
  const force = args.includes("--force");
2771
3773
  const keepData = args.includes("--keep-data");
3774
+ const ownedState = readEngineState();
3775
+ if (ownedState?.kind === "docker") {
3776
+ const inspection = inspectOwnedDockerEngine(ownedState);
3777
+ if (inspection.status === "running" || inspection.status === "stopped") {
3778
+ persistDockerInspection(ownedState, inspection);
3779
+ if (!keepData) {
3780
+ p.log.error(`Docker-backed removal requires --keep-data so the validated ${inspection.dataMountType} data mount (${inspection.dataMountSource}) and its recovery metadata remain usable. Re-run \`agentmemory remove --keep-data\`; destructive Docker data deletion is intentionally not automated.`);
3781
+ process.exit(1);
3782
+ }
3783
+ } else {
3784
+ const detail = inspection.status === "unavailable" ? inspection.reason : "the owned container is missing";
3785
+ p.log.error(`remove cannot safely reconcile the saved Docker ownership (${detail}). Lifecycle state was preserved for manual recovery.`);
3786
+ process.exit(1);
3787
+ }
3788
+ }
2772
3789
  const home = homedir();
2773
3790
  const connectManifest = loadConnectManifest(home);
3791
+ const localBinIiiVersion = probeLocalBinIiiVersion(home);
3792
+ const options = {
3793
+ force,
3794
+ keepData,
3795
+ preserveRuntimeState: ownedState?.kind === "docker"
3796
+ };
2774
3797
  const plan = buildRemovePlan({
2775
3798
  home,
3799
+ runtimeDir: dirname(engineStatePath()),
3800
+ dataDir: dataDirResolution.dataDir,
2776
3801
  pinnedVersion: IIPINNED_VERSION,
2777
- localBinIiiVersion: probeLocalBinIiiVersion(home),
3802
+ localBinIiiVersion,
2778
3803
  connectManifest
2779
- }, {
2780
- force,
2781
- keepData
2782
- });
3804
+ }, options);
2783
3805
  if (plan.filter((it) => it.applicable).length === 0) {
2784
3806
  p.outro("Nothing to remove. agentmemory is already gone.");
2785
3807
  return;
@@ -2816,19 +3838,17 @@ async function runRemove() {
2816
3838
  }
2817
3839
  }
2818
3840
  if (item.id === "stop-engine") {
2819
- try {
2820
- const portPids = findEnginePidsByPort(getRestPort());
2821
- const pidfilePid = readEnginePidfile();
2822
- const cands = /* @__PURE__ */ new Set();
2823
- if (pidfilePid) cands.add(pidfilePid);
2824
- for (const pid of portPids) cands.add(pid);
2825
- for (const pid of cands) await signalAndWait(pid, "SIGTERM", 3e3);
2826
- clearEnginePidfile();
2827
- clearEngineState();
2828
- p.log.success(cands.size > 0 ? `stopped engine (${cands.size} pid${cands.size === 1 ? "" : "s"})` : "no engine running");
2829
- } catch (err) {
2830
- p.log.warn(`engine stop best-effort: ${err instanceof Error ? err.message : String(err)}`);
3841
+ if (ownedState?.kind === "docker") {
3842
+ await stopDockerEngine(ownedState, getRestPort(), true);
3843
+ p.log.success("stopped Docker engine; container, data mount, and recovery state preserved");
3844
+ continue;
2831
3845
  }
3846
+ const stopped = await stopNativeEngineForRemoval();
3847
+ if (!stopped.ok) {
3848
+ p.log.error(`Removal aborted: ${stopped.reason}.`);
3849
+ process.exit(1);
3850
+ }
3851
+ p.log.success(stopped.stoppedEnginePids > 0 ? `stopped engine (${stopped.stoppedEnginePids} pid${stopped.stoppedEnginePids === 1 ? "" : "s"})` : "no engine running");
2832
3852
  continue;
2833
3853
  }
2834
3854
  if (!item.path) continue;
@@ -2838,18 +3858,82 @@ async function runRemove() {
2838
3858
  }
2839
3859
  p.outro("Done. agentmemory cleanly removed. The npm package itself: npm uninstall -g @agentmemory/agentmemory");
2840
3860
  }
2841
- ({
3861
+ async function runConsole() {
3862
+ const iiiBin = pickCompatibleIii([whichBinary("iii"), ...fallbackIiiPaths().filter((candidate) => existsSync(candidate))]);
3863
+ if (!iiiBin) {
3864
+ p.log.error(`No iii v${IIPINNED_VERSION} found. Start agentmemory once so it installs the pinned engine into ${privateIiiPath()}, then run this again.`);
3865
+ process.exit(1);
3866
+ }
3867
+ let parsed;
3868
+ try {
3869
+ parsed = parseConsoleArgs(args.slice(1), defaultConsolePort(getConfiguredViewerPort()));
3870
+ } catch (err) {
3871
+ p.log.error(err instanceof Error ? err.message : String(err));
3872
+ process.exit(1);
3873
+ }
3874
+ const launch = {
3875
+ iiiBin,
3876
+ consolePort: parsed.consolePort,
3877
+ restPort: getRestPort(),
3878
+ streamPort: getStreamPort(),
3879
+ enginePort: getEnginePort(),
3880
+ extraArgs: parsed.extraArgs
3881
+ };
3882
+ if (!await isEngineRunning()) p.log.warn(`No engine responding on port ${launch.restPort}. The console starts anyway and connects once agentmemory is up.`);
3883
+ p.log.info(`iii console: http://127.0.0.1:${launch.consolePort} (engine REST ${launch.restPort}, streams ${launch.streamPort}, bridge ${launch.enginePort}). The first run downloads the console binary next to the pinned iii.`);
3884
+ const child = spawn(iiiBin, consoleArgs(launch), {
3885
+ stdio: "inherit",
3886
+ detached: true
3887
+ });
3888
+ const forward = (signal) => {
3889
+ try {
3890
+ process.kill(-child.pid, signal);
3891
+ } catch {
3892
+ child.kill(signal);
3893
+ }
3894
+ };
3895
+ for (const signal of [
3896
+ "SIGINT",
3897
+ "SIGTERM",
3898
+ "SIGHUP"
3899
+ ]) process.on(signal, () => forward(signal));
3900
+ const code = await new Promise((resolve) => {
3901
+ child.on("exit", (status, signal) => resolve(status ?? (signal ? 1 : 0)));
3902
+ child.on("error", (err) => {
3903
+ p.log.error(err.message);
3904
+ resolve(1);
3905
+ });
3906
+ });
3907
+ process.exit(code);
3908
+ }
3909
+ async function runCapture() {
3910
+ const code = await runCaptureCommand({
3911
+ base: getBaseUrl(),
3912
+ args: args.slice(1),
3913
+ secret: resolveClientSecret(getBaseUrl())
3914
+ });
3915
+ process.exit(code);
3916
+ }
3917
+ const commands = {
2842
3918
  init: runInit,
2843
3919
  connect: runConnectCmd,
2844
3920
  status: runStatus,
3921
+ console: runConsole,
2845
3922
  doctor: runDoctor,
2846
3923
  demo: runDemo,
2847
3924
  upgrade: runUpgrade,
2848
3925
  stop: runStop,
2849
3926
  remove: runRemove,
2850
3927
  mcp: runMcp,
2851
- "import-jsonl": runImportJsonl
2852
- }[args[0] ?? ""] ?? main)().catch((err) => {
3928
+ "import-jsonl": runImportJsonl,
3929
+ capture: runCapture
3930
+ };
3931
+ const first = args[0] ?? "";
3932
+ async function unknownCommand() {
3933
+ p.log.error(`Unknown command: ${first}. Supported: ${Object.keys(commands).join(", ")}. Run \`agentmemory\` with no arguments to start the memory server, or \`agentmemory --help\` for usage.`);
3934
+ process.exit(1);
3935
+ }
3936
+ (commands[first] ?? (first && !first.startsWith("-") ? unknownCommand : main))().catch((err) => {
2853
3937
  p.log.error(err instanceof Error ? err.message : String(err));
2854
3938
  process.exit(1);
2855
3939
  });