@agentmemory/agentmemory 0.9.31 → 0.9.32

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 (138) hide show
  1. package/.env.example +18 -15
  2. package/AGENTS.md +4 -4
  3. package/README.md +92 -270
  4. package/dist/.env.example +18 -15
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +530 -75
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{compression-recovery-CnhLQC6N.mjs → compression-recovery-CZh63Jmz.mjs} +174 -89
  9. package/dist/compression-recovery-CZh63Jmz.mjs.map +1 -0
  10. package/dist/{connect-BSLIAMpw.mjs → connect-BX3njQ3Y.mjs} +28 -14
  11. package/dist/connect-BX3njQ3Y.mjs.map +1 -0
  12. package/dist/hooks/_capture.d.mts +3 -2
  13. package/dist/hooks/_capture.d.mts.map +1 -1
  14. package/dist/hooks/_capture.mjs +87 -8
  15. package/dist/hooks/_capture.mjs.map +1 -1
  16. package/dist/hooks/post-commit.mjs +58 -5
  17. package/dist/hooks/post-commit.mjs.map +1 -1
  18. package/dist/hooks/post-tool-failure.mjs +7 -2
  19. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  20. package/dist/hooks/post-tool-use.mjs +7 -2
  21. package/dist/hooks/post-tool-use.mjs.map +1 -1
  22. package/dist/hooks/pre-compact.mjs +67 -12
  23. package/dist/hooks/pre-compact.mjs.map +1 -1
  24. package/dist/hooks/pre-tool-use.mjs +58 -5
  25. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  26. package/dist/hooks/session-end.mjs +14 -3
  27. package/dist/hooks/session-end.mjs.map +1 -1
  28. package/dist/hooks/session-start.mjs +58 -5
  29. package/dist/hooks/session-start.mjs.map +1 -1
  30. package/dist/hooks/stop.mjs +2 -1
  31. package/dist/hooks/stop.mjs.map +1 -1
  32. package/dist/hooks/subagent-stop.mjs +15 -5
  33. package/dist/hooks/subagent-stop.mjs.map +1 -1
  34. package/dist/hooks/task-completed.mjs +8 -1
  35. package/dist/hooks/task-completed.mjs.map +1 -1
  36. package/dist/index.mjs +724 -200
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/integrations/dsh.mjs +31 -4
  39. package/dist/integrations/dsh.mjs.map +1 -1
  40. package/dist/{secret-store-D9GwO0xp.mjs → secret-store-iYXRcrO8.mjs} +197 -55
  41. package/dist/secret-store-iYXRcrO8.mjs.map +1 -0
  42. package/dist/{src-DaMtDzt_.mjs → src-C23nMpda.mjs} +359 -62
  43. package/dist/src-C23nMpda.mjs.map +1 -0
  44. package/dist/{standalone-BlqshyZ5.mjs → standalone-BYo5USDb.mjs} +169 -10
  45. package/dist/standalone-BYo5USDb.mjs.map +1 -0
  46. package/dist/standalone.d.mts.map +1 -1
  47. package/dist/standalone.mjs +240 -10
  48. package/dist/standalone.mjs.map +1 -1
  49. package/dist/viewer/index.html +49 -28
  50. package/integrations/hermes/README.md +11 -1
  51. package/integrations/hermes/__init__.py +1 -0
  52. package/integrations/hermes/plugin.yaml +1 -1
  53. package/integrations/omp/dist/index.mjs +31 -4
  54. package/integrations/omp/package.json +1 -1
  55. package/integrations/omp/skills/agentmemory-agents/REFERENCE.md +1 -1
  56. package/integrations/omp/skills/agentmemory-architecture/SKILL.md +5 -1
  57. package/integrations/omp/skills/agentmemory-config/REFERENCE.md +2 -1
  58. package/integrations/omp/skills/agentmemory-config/SKILL.md +29 -9
  59. package/integrations/omp/skills/agentmemory-hooks/SKILL.md +17 -6
  60. package/integrations/omp/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  61. package/integrations/omp/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  62. package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  63. package/integrations/omp/skills/agentmemory-rest-api/SKILL.md +1 -1
  64. package/integrations/openclaw/README.md +10 -0
  65. package/integrations/openclaw/openclaw.plugin.json +1 -1
  66. package/integrations/openclaw/package.json +1 -1
  67. package/integrations/openclaw/plugin.yaml +1 -1
  68. package/integrations/openclaw/skills/agentmemory-agents/REFERENCE.md +1 -1
  69. package/integrations/openclaw/skills/agentmemory-architecture/SKILL.md +5 -1
  70. package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +2 -1
  71. package/integrations/openclaw/skills/agentmemory-config/SKILL.md +29 -9
  72. package/integrations/openclaw/skills/agentmemory-hooks/SKILL.md +17 -6
  73. package/integrations/openclaw/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  74. package/integrations/openclaw/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  75. package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  76. package/integrations/openclaw/skills/agentmemory-rest-api/SKILL.md +1 -1
  77. package/integrations/pi/README.md +1 -0
  78. package/integrations/pi/dist/index.mjs +92 -11
  79. package/integrations/pi/index.ts +56 -6
  80. package/integrations/pi/package.json +1 -1
  81. package/integrations/pi/skills/agentmemory-agents/REFERENCE.md +1 -1
  82. package/integrations/pi/skills/agentmemory-architecture/SKILL.md +5 -1
  83. package/integrations/pi/skills/agentmemory-config/REFERENCE.md +2 -1
  84. package/integrations/pi/skills/agentmemory-config/SKILL.md +29 -9
  85. package/integrations/pi/skills/agentmemory-hooks/SKILL.md +17 -6
  86. package/integrations/pi/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  87. package/integrations/pi/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  88. package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  89. package/integrations/pi/skills/agentmemory-rest-api/SKILL.md +1 -1
  90. package/package.json +4 -3
  91. package/plugin/.claude-plugin/plugin.json +16 -3
  92. package/plugin/.codex-plugin/plugin.json +1 -1
  93. package/plugin/.devin-plugin/plugin.json +3 -4
  94. package/plugin/.mcp.json +5 -1
  95. package/plugin/README.md +71 -0
  96. package/plugin/hooks/hooks.json +24 -12
  97. package/plugin/opencode/plugin.json +1 -1
  98. package/plugin/plugin.json +1 -1
  99. package/plugin/scripts/_capture.d.mts +3 -2
  100. package/plugin/scripts/_capture.d.mts.map +1 -1
  101. package/plugin/scripts/_capture.mjs +87 -8
  102. package/plugin/scripts/_capture.mjs.map +1 -1
  103. package/plugin/scripts/plugin-bridge.mjs +25 -6
  104. package/plugin/scripts/post-tool-failure.mjs +7 -2
  105. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  106. package/plugin/scripts/post-tool-use.mjs +7 -2
  107. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  108. package/plugin/scripts/pre-compact.mjs +67 -12
  109. package/plugin/scripts/pre-compact.mjs.map +1 -1
  110. package/plugin/scripts/pre-tool-use.mjs +58 -5
  111. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  112. package/plugin/scripts/session-end.mjs +14 -3
  113. package/plugin/scripts/session-end.mjs.map +1 -1
  114. package/plugin/scripts/session-start.mjs +58 -5
  115. package/plugin/scripts/session-start.mjs.map +1 -1
  116. package/plugin/scripts/stop.mjs +2 -1
  117. package/plugin/scripts/stop.mjs.map +1 -1
  118. package/plugin/scripts/subagent-stop.mjs +15 -5
  119. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  120. package/plugin/scripts/task-completed.mjs +8 -1
  121. package/plugin/scripts/task-completed.mjs.map +1 -1
  122. package/plugin/skills/agentmemory-agents/REFERENCE.md +1 -1
  123. package/plugin/skills/agentmemory-architecture/SKILL.md +5 -1
  124. package/plugin/skills/agentmemory-config/REFERENCE.md +2 -1
  125. package/plugin/skills/agentmemory-config/SKILL.md +29 -9
  126. package/plugin/skills/agentmemory-hooks/SKILL.md +17 -6
  127. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
  128. package/plugin/skills/agentmemory-mcp-tools/SKILL.md +10 -0
  129. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +2 -1
  130. package/plugin/skills/agentmemory-rest-api/SKILL.md +1 -1
  131. package/dist/compression-recovery-CnhLQC6N.mjs.map +0 -1
  132. package/dist/connect-BSLIAMpw.mjs.map +0 -1
  133. package/dist/secret-store-D9GwO0xp.mjs.map +0 -1
  134. package/dist/src-DaMtDzt_.mjs.map +0 -1
  135. package/dist/standalone-BlqshyZ5.mjs.map +0 -1
  136. package/plugin/scripts/post-commit.d.mts +0 -1
  137. package/plugin/scripts/post-commit.mjs +0 -204
  138. package/plugin/scripts/post-commit.mjs.map +0 -1
package/dist/cli.mjs CHANGED
@@ -1,21 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { a as runAdapter, i as resolveAdapter, r as knownAgents, t as ADAPTERS } from "./connect-BSLIAMpw.mjs";
2
+ import { a as runAdapter, i as resolveAdapter, r as knownAgents, t as ADAPTERS } from "./connect-BX3njQ3Y.mjs";
3
3
  import { s as generateId } from "./keyed-mutex-DCUh7PG-.mjs";
4
- import { A as engineFlushWaitMs, C as III_PINNED_VERSION, L as detectLlmProviderKind, M as redactCredentialUrls, N as runtimeConfigPath, O as setBootVerbose, X as getStateBackend, Z as getStateSaveIntervalMs, b as ESSENTIAL_TOOLS, et as hydrateProcessEnvFromFile, gt as parseEnvContent, i as resolveClientSecret, j as engineStateConfigPaths, q as getRedisUrl, t as bearerHeaders, w as VERSION, x as getAllTools } from "./secret-store-D9GwO0xp.mjs";
5
- import { $t as resolveDataDir, Gt as retainSent, Ht as drainSpool, Jt as spoolSummary, Ut as parseSentMark, an as runtimeMetadataPath, nn as registerStartupCleanup, nt as CURL_AUTH_HEADER, rn as runStartupCleanup, t as parseCompressionScope, tn as SHUTDOWN_HARD_EXIT_MS } from "./compression-recovery-CnhLQC6N.mjs";
4
+ import { A as setBootVerbose, B as detectLlmProviderKind, C as getAllTools, E as VERSION, F as runtimeConfigPath, I as LLM_PROVIDER_KEYS, M as engineFlushWaitMs, N as engineStateConfigPaths, P as redactCredentialUrls, S as ESSENTIAL_TOOLS, T as III_PINNED_VERSION, V as embeddingOptInHint, Z as getRedisUrl, et as getStateBackend, i as resolveClientSecret, it as hydrateProcessEnvFromFile, mt as llmOptInHint, t as bearerHeaders, tt as getStateSaveIntervalMs, xt as parseEnvContent } from "./secret-store-iYXRcrO8.mjs";
5
+ import { $t as resolveDataDir, Gt as retainSent, Ht as drainSpool, Jt as spoolSummary, Ut as parseSentMark, an as runtimeMetadataPath, nn as registerStartupCleanup, nt as CURL_AUTH_HEADER, rn as runStartupCleanup, t as parseCompressionScope, tn as SHUTDOWN_HARD_EXIT_MS } from "./compression-recovery-CZh63Jmz.mjs";
6
6
  import { execFileSync, spawn, spawnSync } from "node:child_process";
7
7
  import { appendFileSync, chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
8
- import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
8
+ import { basename, delimiter, dirname, isAbsolute, join, resolve, win32 } 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
+ import { createHash, randomBytes } from "node:crypto";
14
14
  import { copyFile, mkdir } from "node:fs/promises";
15
15
  import { parse, stringify } from "yaml";
16
16
  import { createConnection } from "node:net";
17
17
  import { registerWorker } from "iii-sdk";
18
18
  //#region src/cli/doctor-diagnostics.ts
19
+ function canAutoFix(d, status) {
20
+ return !d.manualOnly && !status?.manualFix;
21
+ }
22
+ function doctorPromptsAllowed(env, stdinIsTTY, stdoutIsTTY) {
23
+ return Boolean(stdinIsTTY) && Boolean(stdoutIsTTY) && !env["CI"];
24
+ }
19
25
  /** Common placeholder values shipped in .env.example. */
20
26
  const PLACEHOLDER_VALUES = new Set([
21
27
  "",
@@ -70,22 +76,28 @@ function buildDiagnostics(effects) {
70
76
  },
71
77
  {
72
78
  id: "no-llm-provider-key",
73
- message: "No LLM provider API key found in ~/.agentmemory/.env or the environment.",
74
- fixPreview: "Open ~/.agentmemory/.env in $EDITOR and paste your key, then re-check.",
75
- moreInfo: "Set at least one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY. Placeholder values count as missing. OPENAI_API_KEY counts only while OPENAI_API_KEY_FOR_LLM is not false, and AGENTMEMORY_ALLOW_AGENT_SDK=true needs no key. A value exported in the environment wins over the file, but only for a daemon started from that environment, and counts even when the file is absent.",
79
+ message: "No LLM provider chosen for agentmemory in ~/.agentmemory/.env or the environment.",
80
+ fixPreview: "Open ~/.agentmemory/.env in $EDITOR, set AGENTMEMORY_LLM_PROVIDER and that provider's key, then re-check.",
81
+ moreInfo: "agentmemory sends session content to an LLM provider only after you choose one: set AGENTMEMORY_LLM_PROVIDER to anthropic, openai, gemini, openrouter or minimax, and set that provider's key (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY or GOOGLE_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY). A key alone never turns LLM use on. Placeholder values count as missing. AGENTMEMORY_ALLOW_AGENT_SDK=true needs no key. A value exported in the environment wins over the file, but only for a daemon started from that environment.",
76
82
  check: async () => {
77
83
  const file = effects.readEnvFile();
78
84
  const env = { ...file };
79
85
  for (const [k, v] of Object.entries(effects.processEnv())) if (v !== void 0) env[k] = v;
80
86
  for (const k of placeholderProviderKeys(env)) delete env[k];
81
- if (detectLlmProviderKind(env) !== "llm") return {
82
- ok: false,
83
- detail: effects.envFileExists() ? "no provider key set" : "no provider key set (run env-missing fix first)"
84
- };
85
- const real = realProviderKeys(env).filter((k) => detectLlmProviderKind({
86
- [k]: env[k],
87
- OPENAI_API_KEY_FOR_LLM: env.OPENAI_API_KEY_FOR_LLM
88
- }) === "llm");
87
+ if (detectLlmProviderKind(env) !== "llm") {
88
+ const hint = llmOptInHint(env);
89
+ if (hint) return {
90
+ ok: false,
91
+ detail: hint
92
+ };
93
+ return {
94
+ ok: false,
95
+ detail: effects.envFileExists() ? "no provider chosen" : "no provider chosen (run env-missing fix first)"
96
+ };
97
+ }
98
+ const selected = (env.AGENTMEMORY_LLM_PROVIDER ?? "").trim().toLowerCase();
99
+ const selectedKeys = selected in LLM_PROVIDER_KEYS ? LLM_PROVIDER_KEYS[selected] : [];
100
+ const real = realProviderKeys(env).filter((k) => selectedKeys.includes(k));
89
101
  const fromEnv = real.filter((k) => file[k] !== env[k]);
90
102
  const named = real.map((k) => fromEnv.includes(k) ? `${k} (environment)` : k);
91
103
  if (named.length === 0) return {
@@ -100,28 +112,61 @@ function buildDiagnostics(effects) {
100
112
  },
101
113
  fix: (ctx) => effects.openEditor(ctx.envPath)
102
114
  },
115
+ {
116
+ id: "embedding-provider-not-chosen",
117
+ message: "Cloud embeddings are not set up: EMBEDDING_PROVIDER does not choose a present key, or names a provider whose key is missing.",
118
+ fixPreview: "Open ~/.agentmemory/.env in $EDITOR and set EMBEDDING_PROVIDER (local, openai, voyage, cohere, gemini or openrouter).",
119
+ moreInfo: "Cloud embeddings send memory text to that provider, so agentmemory uses them only when EMBEDDING_PROVIDER names the provider. A key alone never turns them on. EMBEDDING_PROVIDER=local runs on-device. Without a provider, search is keyword and graph only.",
120
+ check: async () => {
121
+ const env = { ...effects.readEnvFile() };
122
+ for (const [k, v] of Object.entries(effects.processEnv())) if (v !== void 0) env[k] = v;
123
+ const hint = embeddingOptInHint(env);
124
+ return hint ? {
125
+ ok: false,
126
+ detail: hint
127
+ } : { ok: true };
128
+ },
129
+ fix: (ctx) => effects.openEditor(ctx.envPath)
130
+ },
103
131
  {
104
132
  id: "engine-version-mismatch",
105
- message: "iii binary on PATH doesn't match the version agentmemory pins to.",
133
+ message: "No iii binary matches the version agentmemory pins to.",
106
134
  fixPreview: "Reinstall the pinned iii-engine and restart the managed Worker Compose project.",
107
- moreInfo: "agentmemory manages its pinned iii engine and workers through Worker Compose; a foreign process on PATH is not the instance binary. Running a mismatched binary surfaces as EPIPE reconnect loops and empty search results.",
135
+ moreInfo: "agentmemory manages its pinned iii engine and workers through Worker Compose; a foreign process on PATH is not the instance binary. Running a mismatched binary surfaces as EPIPE reconnect loops and empty search results. Before an instance is configured, a matching private install at ~/.agentmemory/bin/iii can satisfy the pin.",
108
136
  check: async (ctx) => {
137
+ if (await effects.externalEngineRunning()) return {
138
+ ok: false,
139
+ detail: "external engine version is unknown",
140
+ manualFix: "Verify the external engine version, or stop it before starting a managed instance."
141
+ };
142
+ const privateVersion = effects.iiiBinaryVersion(effects.localBinIiiPath());
109
143
  const bin = effects.findIiiBinary();
144
+ const pathVersion = bin ? effects.iiiBinaryVersion(bin) : null;
145
+ if (privateVersion === ctx.pinnedVersion) {
146
+ const note = pathVersion && pathVersion !== ctx.pinnedVersion ? `; PATH iii is ${pathVersion}; the pinned private install is available for startup` : "";
147
+ return {
148
+ ok: true,
149
+ detail: `private install ${privateVersion} (pinned ${ctx.pinnedVersion})${note}`
150
+ };
151
+ }
110
152
  if (!bin) return {
111
153
  ok: false,
112
- detail: "iii not on PATH"
154
+ detail: "iii not on PATH and no private install"
113
155
  };
114
- const v = effects.iiiBinaryVersion(bin);
115
- if (!v) return {
156
+ if (!pathVersion) return {
116
157
  ok: false,
117
158
  detail: "iii on PATH but --version failed"
118
159
  };
119
160
  return {
120
- ok: v === ctx.pinnedVersion,
121
- detail: `${v} (pinned ${ctx.pinnedVersion})`
161
+ ok: pathVersion === ctx.pinnedVersion,
162
+ detail: `${pathVersion} (pinned ${ctx.pinnedVersion})`
122
163
  };
123
164
  },
124
165
  fix: async () => {
166
+ if (await effects.externalEngineRunning()) return {
167
+ ok: false,
168
+ message: "Verify or stop the external engine manually."
169
+ };
125
170
  const r = await effects.runIiiInstaller();
126
171
  if (!r.ok) return r;
127
172
  await effects.runStop();
@@ -188,20 +233,25 @@ function buildDiagnostics(effects) {
188
233
  },
189
234
  {
190
235
  id: "iii-on-path-not-local-bin",
191
- message: "iii is on PATH but not at agentmemory's private install path.",
236
+ message: "iii is on PATH but agentmemory's private install is absent or mismatched.",
192
237
  fixPreview: "Install the pinned version to ~/.agentmemory/bin — won't touch your PATH.",
193
238
  moreInfo: "agentmemory installs its pinned engine to ~/.agentmemory/bin/iii so a user-managed iii on PATH (homebrew, cargo, manual install) stays untouched. When agentmemory needs the pin and PATH doesn't have it, it falls back to the private install. If neither exists, run the installer.",
194
239
  manualOnly: true,
195
- check: async () => {
240
+ check: async (ctx) => {
196
241
  const bin = effects.findIiiBinary();
197
242
  if (!bin) return {
198
243
  ok: true,
199
244
  detail: "iii not on PATH (handled elsewhere)"
200
245
  };
201
246
  const localBin = effects.localBinIiiPath();
247
+ if (bin === localBin) return { ok: true };
248
+ if (effects.iiiBinaryVersion(localBin) === ctx.pinnedVersion) return {
249
+ ok: true,
250
+ detail: `private install pinned; PATH iii at ${bin} stays untouched`
251
+ };
202
252
  return {
203
- ok: bin === localBin,
204
- detail: bin === localBin ? void 0 : `iii at: ${bin}`
253
+ ok: false,
254
+ detail: `iii at: ${bin}`
205
255
  };
206
256
  },
207
257
  fix: async () => effects.runIiiInstaller().then((r) => ({
@@ -222,7 +272,7 @@ function dryRunPlan(ctx, results) {
222
272
  if (status.ok) continue;
223
273
  n++;
224
274
  lines.push(`${n}. [${diagnostic.id}] ${diagnostic.message}`);
225
- lines.push(` would fix: ${diagnostic.fixPreview}`);
275
+ lines.push(` ${canAutoFix(diagnostic, status) ? "would fix" : "manual fix"}: ${status.manualFix ?? diagnostic.fixPreview}`);
226
276
  if (status.detail) lines.push(` detail: ${status.detail}`);
227
277
  }
228
278
  if (lines.length === 0) lines.push(`All checks passing for ${ctx.baseUrl} — no fixes to run.`);
@@ -511,6 +561,251 @@ function installIiiArchive(opts) {
511
561
  };
512
562
  return { ok: true };
513
563
  }
564
+ function iiiZipArchivePath(binDir) {
565
+ return join(binDir, `.iii-download-${process.pid}-${randomBytes(4).toString("hex")}.zip`);
566
+ }
567
+ function removeQuietly(path) {
568
+ try {
569
+ rmSync(path, {
570
+ force: true,
571
+ recursive: true
572
+ });
573
+ } catch {}
574
+ }
575
+ function removeStaleInstallFiles(binDir, now = Date.now()) {
576
+ const removed = [];
577
+ let entries;
578
+ try {
579
+ entries = readdirSync(binDir);
580
+ } catch {
581
+ return removed;
582
+ }
583
+ for (const name of entries) {
584
+ if (!name.startsWith(".iii-download-") && !name.startsWith(".iii-staging-")) continue;
585
+ const path = join(binDir, name);
586
+ try {
587
+ if (now - statSync(path).mtimeMs < 9e5) continue;
588
+ } catch {
589
+ continue;
590
+ }
591
+ removeQuietly(path);
592
+ if (!existsSync(path)) removed.push(name);
593
+ }
594
+ return removed;
595
+ }
596
+ const PROXY_ENV_KEYS = [
597
+ "HTTPS_PROXY",
598
+ "https_proxy",
599
+ "HTTP_PROXY",
600
+ "http_proxy",
601
+ "ALL_PROXY",
602
+ "all_proxy"
603
+ ];
604
+ function proxyConfigured(env) {
605
+ return PROXY_ENV_KEYS.some((key) => Boolean(env[key]?.trim()));
606
+ }
607
+ function windowsCurlDownloadCommand(url, dest, env = process.env) {
608
+ return {
609
+ command: join(env["SystemRoot"] || env["SYSTEMROOT"] || "C:\\Windows", "System32", "curl.exe"),
610
+ args: [
611
+ "-fsSL",
612
+ "--connect-timeout",
613
+ String(15),
614
+ "--max-time",
615
+ String(300),
616
+ "-o",
617
+ dest,
618
+ url
619
+ ]
620
+ };
621
+ }
622
+ function powershellLiteral(value) {
623
+ return `'${value.replace(/'/g, "''")}'`;
624
+ }
625
+ function windowsZipExtractCommands(archive, binDir, env = process.env) {
626
+ const system32 = join(env["SystemRoot"] || env["SYSTEMROOT"] || "C:\\Windows", "System32");
627
+ return [{
628
+ command: join(system32, "tar.exe"),
629
+ args: [
630
+ "-xf",
631
+ archive,
632
+ "-C",
633
+ binDir
634
+ ]
635
+ }, {
636
+ command: join(system32, "WindowsPowerShell", "v1.0", "powershell.exe"),
637
+ args: [
638
+ "-NoProfile",
639
+ "-NonInteractive",
640
+ "-Command",
641
+ `Expand-Archive -LiteralPath ${powershellLiteral(archive)} -DestinationPath ${powershellLiteral(binDir)} -Force`
642
+ ]
643
+ }];
644
+ }
645
+ async function downloadToFile(url, dest, env = process.env, run = runInstallCommand) {
646
+ if (proxyConfigured(env)) {
647
+ const step = windowsCurlDownloadCommand(url, dest, env);
648
+ const result = await run(step.command, step.args);
649
+ if (result.status !== 0) throw new Error(describeInstallFailure(result));
650
+ return;
651
+ }
652
+ const res = await fetch(url, {
653
+ redirect: "follow",
654
+ signal: AbortSignal.timeout(300 * 1e3)
655
+ });
656
+ if (!res.ok) throw new Error(`download failed with HTTP ${res.status} ${res.statusText}`.trim());
657
+ writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
658
+ }
659
+ function runInstallCommand(command, args) {
660
+ return new Promise((done) => {
661
+ let stdout = "";
662
+ let stderr = "";
663
+ let timedOut = false;
664
+ const child = spawn(command, args, {
665
+ stdio: [
666
+ "ignore",
667
+ "pipe",
668
+ "pipe"
669
+ ],
670
+ windowsHide: true,
671
+ shell: false
672
+ });
673
+ const killOnExit = () => {
674
+ try {
675
+ child.kill();
676
+ } catch {}
677
+ };
678
+ process.once("exit", killOnExit);
679
+ const timer = setTimeout(() => {
680
+ timedOut = true;
681
+ child.kill();
682
+ }, III_INSTALL_SPAWN_TIMEOUT_MS);
683
+ child.stdout?.on("data", (chunk) => {
684
+ stdout += chunk;
685
+ });
686
+ child.stderr?.on("data", (chunk) => {
687
+ stderr += chunk;
688
+ });
689
+ let spawnError;
690
+ child.on("error", (error) => {
691
+ spawnError = error;
692
+ if (child.pid !== void 0) return;
693
+ clearTimeout(timer);
694
+ process.off("exit", killOnExit);
695
+ done({
696
+ status: null,
697
+ stdout,
698
+ stderr,
699
+ error
700
+ });
701
+ });
702
+ child.on("close", (status, signal) => {
703
+ clearTimeout(timer);
704
+ process.off("exit", killOnExit);
705
+ if (spawnError && status === null && !timedOut) {
706
+ done({
707
+ status: null,
708
+ stdout,
709
+ stderr,
710
+ signal,
711
+ error: spawnError
712
+ });
713
+ return;
714
+ }
715
+ if (timedOut) {
716
+ done({
717
+ status: null,
718
+ stdout,
719
+ stderr,
720
+ signal,
721
+ error: Object.assign(/* @__PURE__ */ new Error("timed out"), { code: "ETIMEDOUT" })
722
+ });
723
+ return;
724
+ }
725
+ done({
726
+ status,
727
+ stdout,
728
+ stderr,
729
+ signal
730
+ });
731
+ });
732
+ });
733
+ }
734
+ async function installIiiZip(opts) {
735
+ const expected = expectedIiiSha256(opts.version, opts.asset, opts.checksums);
736
+ if (!expected) return {
737
+ ok: false,
738
+ reason: "no-checksum",
739
+ detail: `no pinned SHA-256 for ${opts.asset} in iii v${opts.version}, so the download cannot be verified`
740
+ };
741
+ const run = opts.run ?? runInstallCommand;
742
+ const download = opts.download ?? ((url, dest) => downloadToFile(url, dest, opts.env ?? process.env, run));
743
+ const archive = iiiZipArchivePath(opts.binDir);
744
+ try {
745
+ mkdirSync(opts.binDir, { recursive: true });
746
+ removeStaleInstallFiles(opts.binDir);
747
+ await download(opts.releaseUrl, archive);
748
+ } catch (err) {
749
+ removeQuietly(archive);
750
+ return {
751
+ ok: false,
752
+ reason: "download",
753
+ detail: err instanceof Error ? err.message : String(err)
754
+ };
755
+ }
756
+ let actual;
757
+ try {
758
+ actual = sha256File(archive);
759
+ } catch (err) {
760
+ removeQuietly(archive);
761
+ return {
762
+ ok: false,
763
+ reason: "download",
764
+ detail: err instanceof Error ? err.message : String(err)
765
+ };
766
+ }
767
+ if (actual !== expected) {
768
+ removeQuietly(archive);
769
+ return {
770
+ ok: false,
771
+ reason: "checksum",
772
+ detail: `SHA-256 mismatch for ${opts.asset}: expected ${expected}, got ${actual}. The download was deleted and nothing was installed`
773
+ };
774
+ }
775
+ const staging = join(opts.binDir, `.iii-staging-${process.pid}-${randomBytes(4).toString("hex")}`);
776
+ const staged = join(staging, basename(opts.binPath));
777
+ const failures = [];
778
+ try {
779
+ for (const step of windowsZipExtractCommands(archive, staging, opts.env)) {
780
+ removeQuietly(staging);
781
+ mkdirSync(staging, { recursive: true });
782
+ const result = await run(step.command, step.args);
783
+ if (result.status === 0 && existsSync(staged)) try {
784
+ renameSync(staged, opts.binPath);
785
+ return { ok: true };
786
+ } catch (err) {
787
+ failures.push(`could not move ${staged} to ${opts.binPath}: ${err instanceof Error ? err.message : String(err)}`);
788
+ break;
789
+ }
790
+ failures.push(`${step.command}: ${result.status === 0 ? `finished but ${basename(opts.binPath)} is missing from the archive` : describeInstallFailure(result)}`);
791
+ }
792
+ return {
793
+ ok: false,
794
+ reason: "extract",
795
+ detail: failures.join("; ")
796
+ };
797
+ } catch (err) {
798
+ failures.push(err instanceof Error ? err.message : String(err));
799
+ return {
800
+ ok: false,
801
+ reason: "extract",
802
+ detail: failures.join("; ")
803
+ };
804
+ } finally {
805
+ removeQuietly(staging);
806
+ removeQuietly(archive);
807
+ }
808
+ }
514
809
  //#endregion
515
810
  //#region src/cli/startup-stderr.ts
516
811
  function createStartupStderrCapture(maxBytes = 16 * 1024) {
@@ -725,6 +1020,12 @@ function sameResolvedExecutable(actual, expected, windows = false) {
725
1020
  return false;
726
1021
  }
727
1022
  }
1023
+ function classifyComposeProcess(inspected, expected, windows = false) {
1024
+ if (!inspected || !inspected.command) return "unknown";
1025
+ if (!(inspected.command.includes("compose --namespace") && inspected.command.includes(expected.namespace) && inspected.command.includes(expected.file))) return "foreign";
1026
+ if (inspected.executable && sameResolvedExecutable(inspected.executable, expected.binPath, windows)) return "match";
1027
+ return "unknown";
1028
+ }
728
1029
  function readUnixProcessExecutable(pid, os) {
729
1030
  if (!Number.isSafeInteger(pid) || pid <= 0) return "";
730
1031
  if (os === "darwin") return readMacProcessExecutable(pid);
@@ -1002,6 +1303,7 @@ async function runOnboarding() {
1002
1303
  if (hint) p.log.info(hint);
1003
1304
  }
1004
1305
  const envPath = await seedEnvFile(provider);
1306
+ const providerWritten = setLlmProviderInEnv(envPath, provider);
1005
1307
  await maybePromptContextInjection(envPath);
1006
1308
  writePrefs({
1007
1309
  lastAgent: agents[0] ?? null,
@@ -1015,8 +1317,9 @@ async function runOnboarding() {
1015
1317
  else lines.push(`! Could not write ~/.agentmemory/.env — run \`agentmemory init\` after this completes.`);
1016
1318
  if (provider) {
1017
1319
  const envKey = PROVIDERS.find((x) => x.value === provider)?.envKey;
1018
- if (envKey) lines.push(` Uncomment ${envKey}= in that file to enable ${provider}.`);
1019
- } else lines.push(" No provider chosen — agentmemory will run in BM25-only mode.");
1320
+ if (envKey) lines.push(providerWritten ? ` Set AGENTMEMORY_LLM_PROVIDER=${provider}. Add ${envKey}= to that file to enable ${provider}.` : ` Add AGENTMEMORY_LLM_PROVIDER=${provider} and ${envKey}= to that file to enable ${provider}.`);
1321
+ } else if (providerWritten) lines.push(" No LLM provider chosen. LLM summaries and compression stay off.");
1322
+ else lines.push(` ! Could not remove AGENTMEMORY_LLM_PROVIDER from ${envPath}. Delete that line to keep LLM features off.`);
1020
1323
  p.note(lines.join("\n"), "ready");
1021
1324
  if (agents.length > 0) await wireSelectedAgents(agents);
1022
1325
  return {
@@ -1024,6 +1327,19 @@ async function runOnboarding() {
1024
1327
  provider
1025
1328
  };
1026
1329
  }
1330
+ function setLlmProviderInEnv(envPath, provider) {
1331
+ if (provider === null) delete process.env["AGENTMEMORY_LLM_PROVIDER"];
1332
+ if (!envPath || !existsSync(envPath)) return provider === null;
1333
+ try {
1334
+ const current = readFileSync(envPath, "utf-8");
1335
+ const kept = current.split("\n").filter((entry) => !/^\s*AGENTMEMORY_LLM_PROVIDER\s*=/.test(entry)).join("\n");
1336
+ const next = provider === null ? kept : `${kept}${kept.length > 0 && !kept.endsWith("\n") ? "\n" : ""}AGENTMEMORY_LLM_PROVIDER=${provider}\n`;
1337
+ if (next !== current) writeFileSync(envPath, next, { mode: 384 });
1338
+ return true;
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
1027
1343
  function enableInjectContextInEnv(envPath) {
1028
1344
  if (!envPath || !existsSync(envPath)) return false;
1029
1345
  try {
@@ -1361,6 +1677,10 @@ const COMPOSE_WORKER_VERSIONS = {
1361
1677
  state: "0.22.20",
1362
1678
  http: "0.22.7"
1363
1679
  };
1680
+ const COMPOSE_CONTAINER_COUNT = Object.keys(COMPOSE_WORKER_VERSIONS).length + 1;
1681
+ function composeDownTimeoutMs() {
1682
+ return (20 * COMPOSE_CONTAINER_COUNT + 30) * 1e3;
1683
+ }
1364
1684
  function composeNamespace(restPort) {
1365
1685
  return `agentmemory-${restPort}`;
1366
1686
  }
@@ -1376,8 +1696,8 @@ function composeProjectPaths(dataDir) {
1376
1696
  }
1377
1697
  const AGENTMEMORY_WORKER_ENV_ALLOWLIST = new Set([
1378
1698
  "EMBEDDING_PROVIDER",
1699
+ "AGENTMEMORY_LLM_PROVIDER",
1379
1700
  "OPENAI_API_KEY",
1380
- "OPENAI_API_KEY_FOR_LLM",
1381
1701
  "OPENAI_BASE_URL",
1382
1702
  "OPENAI_MODEL",
1383
1703
  "OPENAI_EMBEDDING_API_KEY",
@@ -1515,17 +1835,33 @@ function restartPolicyLines(indent, enabled) {
1515
1835
  `${indent} window: 60s`
1516
1836
  ];
1517
1837
  }
1518
- function shellQuote$1(value, windows) {
1838
+ function shellQuote$1(value) {
1519
1839
  if (/[\u0000-\u001f\u007f]/.test(value)) throw new Error("A shell command argument contains an unsupported control character.");
1520
- if (windows) {
1521
- if (value.includes("\"")) throw new Error("Windows shell arguments cannot contain a double quote.");
1522
- return `"${value.replace(/%/g, "%%")}"`;
1523
- }
1524
1840
  return `'${value.replace(/'/g, `'"'"'`)}'`;
1525
1841
  }
1526
- function shellCommand(program, args, windows) {
1527
- const command = [program, ...args].map((part) => shellQuote$1(part, windows)).join(" ");
1528
- return windows ? `"${command}"` : command;
1842
+ function shellCommand(program, args) {
1843
+ return [program, ...args].map((part) => shellQuote$1(part)).join(" ");
1844
+ }
1845
+ const WINDOWS_RUN_SAFE = /^[A-Za-z0-9._:\\/-]+$/;
1846
+ const WINDOWS_EXE_SAFE = /^[A-Za-z0-9._-]+$/;
1847
+ function windowsWorkerScript(script) {
1848
+ if (!WINDOWS_RUN_SAFE.test(script)) throw new Error(`On Windows, AGENTMEMORY_WORKER_BIN_DIR must be a path without spaces or cmd metacharacters; got ${JSON.stringify(script)}.`);
1849
+ return script;
1850
+ }
1851
+ function windowsNodeRun(nodeBin) {
1852
+ const base = win32.basename(nodeBin);
1853
+ if (!WINDOWS_EXE_SAFE.test(base)) throw new Error(`The Node executable name must not contain spaces or cmd metacharacters on Windows; got ${JSON.stringify(nodeBin)}.`);
1854
+ return `${base} dist/index.mjs`;
1855
+ }
1856
+ function windowsWorkerPath(nodeBin, inherited) {
1857
+ const entries = [win32.dirname(nodeBin), ...(inherited ?? "").split(";")].map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1858
+ const seen = /* @__PURE__ */ new Set();
1859
+ return entries.filter((entry) => {
1860
+ const key = entry.toLowerCase();
1861
+ if (seen.has(key)) return false;
1862
+ seen.add(key);
1863
+ return true;
1864
+ }).join(";");
1529
1865
  }
1530
1866
  function yamlString(value) {
1531
1867
  if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) throw new Error("A Compose path or configuration value contains an unsupported control character.");
@@ -1597,17 +1933,18 @@ function renderWorkerCompose(options) {
1597
1933
  const { restPort, viewerPort, enginePort } = options.ports;
1598
1934
  const paths = composeProjectPaths(options.dataDir);
1599
1935
  const namespace = composeNamespace(restPort);
1600
- const workerBinDir = options.workerBinDir ? options.workerBinDirIsAbsolute ? resolve(options.workerBinDir) : localWorkerDirectory(options.workerBinDir, options.cwd ?? process.cwd(), options.projectRoot) : null;
1936
+ const windows = options.windows ?? process.platform === "win32";
1937
+ const workerBinDir = options.workerBinDir ? options.workerBinDirIsAbsolute ? windows ? win32.resolve(options.workerBinDir) : resolve(options.workerBinDir) : localWorkerDirectory(options.workerBinDir, options.cwd ?? process.cwd(), options.projectRoot) : null;
1601
1938
  const startupSeconds = Math.ceil((options.startupTimeoutMs ?? 3e5) / 1e3);
1602
1939
  const restartEnabled = !options.disableRestartPolicy;
1603
1940
  if (!Number.isSafeInteger(startupSeconds) || startupSeconds < 1 || startupSeconds * 1e3 > 3600999) throw new Error("Compose worker startup timeout must be between 1 and 3600000 milliseconds.");
1604
1941
  const workerSource = (name) => workerBinDir ? `path://${localWorkerProjectDir(paths, name)}` : `package://api.workers.iii.dev/${name}`;
1605
- const workerScript = (name) => workerBinDir ? join(workerBinDir, name) : "";
1942
+ const workerScript = (name) => workerBinDir ? windows ? win32.join(workerBinDir, name) : join(workerBinDir, name) : "";
1606
1943
  const filteredEnvFile = options.envFile ? join(paths.directory, ".agentmemory-filtered.env") : void 0;
1607
1944
  const lines = [
1608
1945
  `namespace: ${namespace}`,
1609
1946
  "startup_timeout: 60s",
1610
- "stop_timeout: 20s",
1947
+ `stop_timeout: 20s`,
1611
1948
  "required_default: true",
1612
1949
  "engine:",
1613
1950
  ` url: ws://127.0.0.1:${enginePort}`,
@@ -1623,7 +1960,7 @@ function renderWorkerCompose(options) {
1623
1960
  "containers:"
1624
1961
  ];
1625
1962
  const stateScript = workerScript("state");
1626
- lines.push(" state:", ` worker: ${yamlString(workerSource("state"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.state)}`] : [], " config_name: state", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(shellCommand(stateScript, [], options.windows ?? process.platform === "win32"))}`] : [], " config_override:", " adapter:", " name: kv", " config:");
1963
+ lines.push(" state:", ` worker: ${yamlString(workerSource("state"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.state)}`] : [], " config_name: state", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(windows ? windowsWorkerScript(stateScript) : shellCommand(stateScript, []))}`] : [], " config_override:", " adapter:", " name: kv", " config:");
1627
1964
  if (options.stateBackend?.kind === "redis") {
1628
1965
  const redisUrl = options.redisUrl ?? options.stateBackend.redisUrl;
1629
1966
  if (!redisUrl) throw new Error("AGENTMEMORY_STATE_BACKEND=redis requires AGENTMEMORY_REDIS_URL to be set (e.g. redis://localhost:6379).");
@@ -1631,11 +1968,11 @@ function renderWorkerCompose(options) {
1631
1968
  lines.splice(lines.length - 2, 2, " name: redis", " config:", ` redis_url: ${yamlString(redisUrl)}`);
1632
1969
  } else lines.push(" store_method: file_based", ` file_path: ${yamlString(join(options.dataDir, "state_store.db"))}`, ` save_interval_ms: ${stateSaveInterval(options.workerEnvironment?.AGENTMEMORY_STATE_SAVE_INTERVAL_MS)}`);
1633
1970
  const httpScript = workerScript("http");
1634
- lines.push(" http:", ` worker: ${yamlString(workerSource("http"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.http)}`] : [], " config_name: http", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(shellCommand(httpScript, [], options.windows ?? process.platform === "win32"))}`] : [], " config_override:", " host: 127.0.0.1", ` port: ${restPort}`, " default_timeout: 180000", " cors:", " allowed_origins:", ` - ${yamlString(`http://localhost:${restPort}`)}`, ` - ${yamlString(`http://localhost:${viewerPort}`)}`, ` - ${yamlString(`http://127.0.0.1:${restPort}`)}`, ` - ${yamlString(`http://127.0.0.1:${viewerPort}`)}`, " allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]");
1635
- lines.push(" agentmemory:", ` worker: ${yamlString(`path://${options.projectRoot}`)}`, " start_after: [state, http]", ` startup_timeout: ${startupSeconds}s`, " config_name: agentmemory", ...restartPolicyLines(" ", restartEnabled), " scripts:", ` run: ${yamlString(shellCommand(options.nodeBin, ["dist/index.mjs"], options.windows ?? process.platform === "win32"))}`);
1971
+ lines.push(" http:", ` worker: ${yamlString(workerSource("http"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.http)}`] : [], " config_name: http", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(windows ? windowsWorkerScript(httpScript) : shellCommand(httpScript, []))}`] : [], " config_override:", " host: 127.0.0.1", ` port: ${restPort}`, " default_timeout: 180000", " cors:", " allowed_origins:", ` - ${yamlString(`http://localhost:${restPort}`)}`, ` - ${yamlString(`http://localhost:${viewerPort}`)}`, ` - ${yamlString(`http://127.0.0.1:${restPort}`)}`, ` - ${yamlString(`http://127.0.0.1:${viewerPort}`)}`, " allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]");
1972
+ lines.push(" agentmemory:", ` worker: ${yamlString(`path://${options.projectRoot}`)}`, " start_after: [state, http]", ` startup_timeout: ${startupSeconds}s`, " config_name: agentmemory", ...restartPolicyLines(" ", restartEnabled), " scripts:", ` run: ${yamlString(windows ? windowsNodeRun(options.nodeBin) : shellCommand(options.nodeBin, ["dist/index.mjs"]))}`);
1636
1973
  if (filteredEnvFile) lines.push(` env_file: [${yamlString(filteredEnvFile)}]`);
1637
1974
  const explicitWorkerEnvironment = Object.entries(options.workerEnvironment ?? {}).filter(([key, value]) => value !== void 0 && AGENTMEMORY_WORKER_ENV_ALLOWLIST.has(key) && !COMPOSE_RESERVED_ENV.has(key)).sort(([left], [right]) => left.localeCompare(right));
1638
- lines.push(" environment:", ...explicitWorkerEnvironment.map(([key, value]) => ` ${key}: ${yamlString(value)}`), ` HOME: ${yamlString(options.homeDir)}`, ` PATH: ${yamlString(`${dirname(options.nodeBin)}:/usr/local/bin:/usr/bin:/bin`)}`, ` AGENTMEMORY_DATA_DIR: ${yamlString(options.dataDir)}`, ` AGENTMEMORY_RUNTIME_DIR: ${yamlString(options.dataDir)}`, ...filteredEnvFile ? [` AGENTMEMORY_ENV_FILE: ${yamlString(filteredEnvFile)}`] : [], ...options.stateBackend?.kind === "redis" && options.redisUrl ? [` AGENTMEMORY_REDIS_URL: ${yamlString(options.redisUrl)}`, " AGENTMEMORY_STATE_BACKEND: 'redis'"] : [], ` III_ENGINE_PORT: ${yamlString(String(enginePort))}`, ` III_REST_PORT: ${yamlString(String(restPort))}`, ` III_VIEWER_PORT: ${yamlString(String(viewerPort))}`, " III_TELEMETRY_ENABLED: 'false'", ...options.linux ?? process.platform === "linux" ? [" MALLOC_ARENA_MAX: '2'"] : [], "");
1975
+ lines.push(" environment:", ...explicitWorkerEnvironment.map(([key, value]) => ` ${key}: ${yamlString(value)}`), ...windows ? [` NoDefaultCurrentDirectoryInExePath: ${yamlString("1")}`] : [], ` HOME: ${yamlString(options.homeDir)}`, ` PATH: ${yamlString(windows ? windowsWorkerPath(options.nodeBin, options.workerEnvironment?.PATH ?? process.env.PATH) : `${dirname(options.nodeBin)}:/usr/local/bin:/usr/bin:/bin`)}`, ` AGENTMEMORY_DATA_DIR: ${yamlString(options.dataDir)}`, ` AGENTMEMORY_RUNTIME_DIR: ${yamlString(options.dataDir)}`, ...filteredEnvFile ? [` AGENTMEMORY_ENV_FILE: ${yamlString(filteredEnvFile)}`] : [], ...options.stateBackend?.kind === "redis" && options.redisUrl ? [` AGENTMEMORY_REDIS_URL: ${yamlString(options.redisUrl)}`, " AGENTMEMORY_STATE_BACKEND: 'redis'"] : [], ` III_ENGINE_PORT: ${yamlString(String(enginePort))}`, ` III_REST_PORT: ${yamlString(String(restPort))}`, ` III_VIEWER_PORT: ${yamlString(String(viewerPort))}`, " III_TELEMETRY_ENABLED: 'false'", ...options.linux ?? process.platform === "linux" ? [" MALLOC_ARENA_MAX: '2'"] : [], "");
1639
1976
  return lines.join("\n");
1640
1977
  }
1641
1978
  function writeWorkerCompose(options) {
@@ -1731,7 +2068,7 @@ function composeStateRegistryFailure(dataDir, file) {
1731
2068
  for (const stateFile of files) {
1732
2069
  const state = JSON.parse(readFileSync(stateFile, "utf8"));
1733
2070
  const message = Object.entries(state.containers ?? {}).filter(([, value]) => value.status === "failed").map(([name, value]) => `${name}: ${value.last_error ?? "failed"}`).join("\n");
1734
- if (state.compose_path !== file) continue;
2071
+ if (!sameComposeFile(state.compose_path, file)) continue;
1735
2072
  if (message && composeRegistryFailureMessage(message)) return message;
1736
2073
  }
1737
2074
  } catch {}
@@ -1743,8 +2080,30 @@ function composeWorkerFailureSummary(project) {
1743
2080
  if (!failed.length) return null;
1744
2081
  return failed.map((container) => `${container.container ?? "worker"}: ${container.last_error ?? "failed after restart attempts were exhausted"}`).join("\n");
1745
2082
  }
1746
- function ownedComposeProject(status, file) {
1747
- return status.file === file ? status : status.projects?.find((item) => item.file === file) ?? null;
2083
+ function defaultCanonicalizer(windows) {
2084
+ return (value) => {
2085
+ try {
2086
+ return windows ? realpathSync.native(value) : realpathSync(value);
2087
+ } catch {
2088
+ return value;
2089
+ }
2090
+ };
2091
+ }
2092
+ function normalizeWindowsPath(value) {
2093
+ let out = value;
2094
+ if (out.startsWith("\\\\?\\UNC\\")) out = `\\\\${out.slice(8)}`;
2095
+ else if (out.startsWith("\\\\?\\")) out = out.slice(4);
2096
+ return out.replace(/\//g, "\\").toLowerCase();
2097
+ }
2098
+ function sameComposeFile(left, right, windows = process.platform === "win32", canonicalize = defaultCanonicalizer(windows)) {
2099
+ if (!left) return false;
2100
+ if (!windows) return left === right || canonicalize(left) === canonicalize(right);
2101
+ if (normalizeWindowsPath(left) === normalizeWindowsPath(right)) return true;
2102
+ return normalizeWindowsPath(canonicalize(left)) === normalizeWindowsPath(canonicalize(right));
2103
+ }
2104
+ function ownedComposeProject(status, file, windows = process.platform === "win32", canonicalize = defaultCanonicalizer(windows)) {
2105
+ if (sameComposeFile(status.file, file, windows, canonicalize)) return status;
2106
+ return status.projects?.find((item) => sameComposeFile(item.file, file, windows, canonicalize)) ?? null;
1748
2107
  }
1749
2108
  function cleanDaemonEnv(env) {
1750
2109
  const out = {};
@@ -1794,8 +2153,29 @@ function composeEnvironment(env, stateDir) {
1794
2153
  ...env.AGENTMEMORY_DISABLE_COMPOSE_RESTART === "1" ? { AGENTMEMORY_DISABLE_COMPOSE_RESTART: "1" } : {}
1795
2154
  };
1796
2155
  }
1797
- async function composeCall(options) {
1798
- const client = registerWorker(options.engineUrl, {
2156
+ let quietDepth = 0;
2157
+ let savedDebug = null;
2158
+ async function withQuietSdkDebug(run) {
2159
+ if (quietDepth === 0) {
2160
+ savedDebug = console.debug;
2161
+ console.debug = () => {};
2162
+ }
2163
+ quietDepth++;
2164
+ try {
2165
+ return await run();
2166
+ } finally {
2167
+ quietDepth--;
2168
+ if (quietDepth === 0 && savedDebug) {
2169
+ console.debug = savedDebug;
2170
+ savedDebug = null;
2171
+ }
2172
+ }
2173
+ }
2174
+ async function composeCall(options, register = registerWorker) {
2175
+ return withQuietSdkDebug(() => composeCallWithClient(options, register));
2176
+ }
2177
+ async function composeCallWithClient(options, register) {
2178
+ const client = register(options.engineUrl, {
1799
2179
  workerName: "agentmemory-cli",
1800
2180
  namespace: options.namespace,
1801
2181
  invocationTimeoutMs: options.timeoutMs ?? 3e4,
@@ -2253,8 +2633,20 @@ function fallbackIiiPaths() {
2253
2633
  paths.push("/usr/local/bin/iii");
2254
2634
  return paths;
2255
2635
  }
2636
+ const III_VERSION_PROBE_TIMEOUTS_MS = [3e3, 3e4];
2637
+ function isProbeTimeout(err) {
2638
+ const e = err;
2639
+ return e?.code === "ETIMEDOUT" || e?.signal === "SIGTERM";
2640
+ }
2641
+ let iiiProbeScan = null;
2256
2642
  function iiiBinVersion(binPath) {
2257
- try {
2643
+ if (iiiProbeScan?.has(binPath)) return iiiProbeScan.get(binPath);
2644
+ const version = probeIiiVersion(binPath);
2645
+ iiiProbeScan?.set(binPath, version);
2646
+ return version;
2647
+ }
2648
+ function probeIiiVersion(binPath) {
2649
+ for (const timeout of III_VERSION_PROBE_TIMEOUTS_MS) try {
2258
2650
  const match = execFileSync(binPath, ["--version"], {
2259
2651
  encoding: "utf-8",
2260
2652
  stdio: [
@@ -2262,12 +2654,14 @@ function iiiBinVersion(binPath) {
2262
2654
  "pipe",
2263
2655
  "ignore"
2264
2656
  ],
2265
- timeout: 3e3
2657
+ timeout
2266
2658
  }).match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
2267
2659
  return match ? match[1] : null;
2268
- } catch {
2269
- return null;
2660
+ } catch (err) {
2661
+ if (!isProbeTimeout(err)) return null;
2662
+ vlog(`iii --version at ${binPath} timed out after ${timeout}ms`);
2270
2663
  }
2664
+ return null;
2271
2665
  }
2272
2666
  function resolveCompatibleIii(iiiBinPath) {
2273
2667
  if (!iiiBinPath) return null;
@@ -2434,7 +2828,7 @@ async function startWorkerForEngineState(background = false) {
2434
2828
  child.unref();
2435
2829
  } else {
2436
2830
  importedStartupWorker = true;
2437
- await import("./src-DaMtDzt_.mjs");
2831
+ await import("./src-C23nMpda.mjs");
2438
2832
  }
2439
2833
  },
2440
2834
  probe: workerReadinessProbe,
@@ -2514,8 +2908,8 @@ async function maybeOfferGlobalInstall() {
2514
2908
  if (runCommand(npmBin, [
2515
2909
  "install",
2516
2910
  "-g",
2517
- `@agentmemory/agentmemory@0.9.31`
2518
- ], { label: `Installing @agentmemory/agentmemory@0.9.31 globally` })) {
2911
+ `@agentmemory/agentmemory@0.9.32`
2912
+ ], { label: `Installing @agentmemory/agentmemory@0.9.32 globally` })) {
2519
2913
  p.log.success("Installed globally. `agentmemory stop` etc. will now work in new shells.");
2520
2914
  writePrefs({ skipGlobalInstall: true });
2521
2915
  } else p.log.warn("Global install failed. Try manually: npm install -g @agentmemory/agentmemory");
@@ -2532,7 +2926,28 @@ async function runIiiInstaller() {
2532
2926
  };
2533
2927
  }
2534
2928
  if (IS_WINDOWS || isZipAsset) {
2535
- 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)\nInstall the pinned iii binary from ${releaseUrl}`);
2929
+ const binDir = agentmemoryBinDir();
2930
+ const binPath = privateIiiPath();
2931
+ const label = `Installing iii-engine v${IIPINNED_VERSION} (pinned)`;
2932
+ const spinner = p.spinner();
2933
+ spinner.start(label);
2934
+ const outcome = await installIiiZip({
2935
+ releaseUrl,
2936
+ version: IIPINNED_VERSION,
2937
+ asset: asset ?? "",
2938
+ binDir,
2939
+ binPath
2940
+ });
2941
+ if (outcome.ok) {
2942
+ spinner.stop(`${label} ${pc.green("✓")}`);
2943
+ iiiProbeScan?.delete(binPath);
2944
+ return {
2945
+ ok: true,
2946
+ binPath
2947
+ };
2948
+ }
2949
+ spinner.stop(`${label} ${pc.red("✗")}`);
2950
+ p.log.warn(`iii-engine auto-install failed: ${outcome.detail.slice(0, 300)}\nInstall it manually, then re-run agentmemory:\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 into ${binDir}`);
2536
2951
  return {
2537
2952
  ok: false,
2538
2953
  binPath: null
@@ -2578,6 +2993,7 @@ async function runIiiInstaller() {
2578
2993
  };
2579
2994
  }
2580
2995
  spinner.stop(`${label} ${pc.green("✓")}`);
2996
+ iiiProbeScan?.delete(binPath);
2581
2997
  return {
2582
2998
  ok: true,
2583
2999
  binPath
@@ -2679,6 +3095,12 @@ function printComposeStartupLogs() {
2679
3095
  if (logs.text) p.note(`Log file: ${logs.path}\n\n${logs.text}`, "Recent Worker Compose output");
2680
3096
  }
2681
3097
  function composePidMatches(pid, state) {
3098
+ return composePidIdentity(pid, state) === "match";
3099
+ }
3100
+ function composePidIdentity(pid, state) {
3101
+ return classifyComposeProcess(inspectComposeProcess(pid), state, IS_WINDOWS);
3102
+ }
3103
+ function inspectComposeProcess(pid) {
2682
3104
  try {
2683
3105
  let command = "";
2684
3106
  let executable = "";
@@ -2730,11 +3152,12 @@ function composePidMatches(pid, state) {
2730
3152
  }).trim();
2731
3153
  executable = readUnixProcessExecutable(pid, "linux");
2732
3154
  }
2733
- if (!(command.includes("compose") && command.includes("compose --namespace") && command.includes(state.namespace) && command.includes(state.file))) return false;
2734
- if (!executable) return false;
2735
- return sameResolvedExecutable(executable, state.binPath, IS_WINDOWS);
3155
+ return {
3156
+ command,
3157
+ executable
3158
+ };
2736
3159
  } catch {
2737
- return false;
3160
+ return null;
2738
3161
  }
2739
3162
  }
2740
3163
  function composeChildProcesses(state) {
@@ -2895,6 +3318,14 @@ function pickCompatibleIii(candidates) {
2895
3318
  return null;
2896
3319
  }
2897
3320
  async function startEngine() {
3321
+ iiiProbeScan = /* @__PURE__ */ new Map();
3322
+ try {
3323
+ return await scanAndStartEngine();
3324
+ } finally {
3325
+ iiiProbeScan = null;
3326
+ }
3327
+ }
3328
+ async function scanAndStartEngine() {
2898
3329
  startupFailure = null;
2899
3330
  engineReadinessDeadline = null;
2900
3331
  let stateBackendKind;
@@ -2931,7 +3362,7 @@ async function startEngine() {
2931
3362
  const pathIii = whichBinary("iii");
2932
3363
  vlog(`iii binary: ${pathIii ?? "(not on PATH)"}`);
2933
3364
  const fallbacks = fallbackIiiPaths().filter((p) => existsSync(p));
2934
- for (const f of fallbacks) vlog(`fallback iii at ${f} reports version: ${iiiBinVersion(f) ?? "unknown"}`);
3365
+ vlog(`fallback iii candidates: ${fallbacks.length > 0 ? fallbacks.join(", ") : "(none)"}`);
2935
3366
  let iiiBin = pickCompatibleIii([pathIii, ...fallbacks]);
2936
3367
  if (iiiBin) {
2937
3368
  if (iiiBin !== pathIii) {
@@ -2948,6 +3379,9 @@ async function startEngine() {
2948
3379
  choice = "install";
2949
3380
  const detected = iiiBinVersion(pathIii);
2950
3381
  p.log.info(`iii on PATH is v${detected ?? "unknown"} but agentmemory pins v${IIPINNED_VERSION}. Installing pinned engine to ~/.agentmemory/bin (leaves your existing iii untouched).`);
3382
+ } else if (IS_WINDOWS) {
3383
+ choice = "install";
3384
+ p.log.warn("iii-engine binary not found locally.");
2951
3385
  } else if (!interactive) {
2952
3386
  choice = "install";
2953
3387
  p.log.info("Non-interactive environment detected — auto-installing iii-engine.");
@@ -3020,7 +3454,7 @@ function installInstructions() {
3020
3454
  ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
3021
3455
  ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
3022
3456
  verify,
3023
- " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
3457
+ " 3. Extract iii.exe to %USERPROFILE%\\.agentmemory\\bin\\iii.exe (or add to PATH)",
3024
3458
  " 4. Re-run: npx @agentmemory/agentmemory",
3025
3459
  "",
3026
3460
  "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
@@ -3062,7 +3496,7 @@ async function reconcilePersistedComposeEngine() {
3062
3496
  const state = readEngineState();
3063
3497
  if (state?.kind !== "compose") return false;
3064
3498
  if (!await isEngineRunning()) {
3065
- if (state.composePid && pidAlive(state.composePid)) {
3499
+ if (state.composePid && pidAlive(state.composePid) && composePidIdentity(state.composePid, state) !== "foreign") {
3066
3500
  p.log.error(`Worker Compose daemon ${state.composePid} is alive but REST ${state.restPort} is not ready. Inspect ${state.logFile}; use agentmemory stop to reconcile.`);
3067
3501
  process.exit(1);
3068
3502
  }
@@ -3330,13 +3764,15 @@ async function runStatus() {
3330
3764
  lines.push(` Injected: ~${estInjectedTokens.toLocaleString()} tokens`);
3331
3765
  }
3332
3766
  if (flagsRes) {
3333
- const provider = flagsRes.provider === "llm" ? pc.green("✓ llm") : pc.yellow("✗ noop (no key)");
3767
+ const provider = flagsRes.provider === "llm" ? pc.green("✓ llm") : pc.yellow("✗ noop (LLM features off)");
3334
3768
  const embedding = embeddingStatusLabel(flagsRes.embeddingProvider);
3335
3769
  const embed = embedding ? pc.green(`✓ ${embedding}`) : pc.dim("bm25-only");
3336
3770
  const flagRows = (flagsRes.flags || []).map((f) => ` ${f.enabled ? pc.green("✓") : pc.dim("✗")} ${pc.bold(f.key.padEnd(32))} ${f.label}`);
3337
3771
  lines.push("");
3338
3772
  lines.push(`Provider: ${provider}`);
3339
3773
  lines.push(`Embeddings: ${embed}`);
3774
+ if (typeof flagsRes.providerHint === "string") lines.push(pc.yellow(` ${flagsRes.providerHint}`));
3775
+ if (typeof flagsRes.embeddingHint === "string") lines.push(pc.yellow(` ${flagsRes.embeddingHint}`));
3340
3776
  lines.push(`Flags:`);
3341
3777
  flagRows.forEach((r) => lines.push(r));
3342
3778
  }
@@ -3452,6 +3888,11 @@ function buildDoctorEffects() {
3452
3888
  return state?.kind === "compose" ? state.binPath : privateIiiPath();
3453
3889
  },
3454
3890
  iiiBinaryVersion: (binPath) => iiiBinVersion(binPath),
3891
+ externalEngineRunning: async () => {
3892
+ const state = readEngineState();
3893
+ const remoteTarget = shouldUseAgentmemoryUrl() && process.env["AGENTMEMORY_URL"];
3894
+ return !(state?.kind === "compose" && state.restPort === getRestPort() && !remoteTarget) && await isEngineRunning();
3895
+ },
3455
3896
  viewerReachable: async (timeoutMs = 2e3) => {
3456
3897
  try {
3457
3898
  await discoverViewerPort();
@@ -3595,11 +4036,11 @@ async function passiveServerChecks() {
3595
4036
  }, {
3596
4037
  name: "LLM provider",
3597
4038
  ok: hasLlm,
3598
- hint: hasLlm ? void 0 : "set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env"
4039
+ hint: hasLlm ? void 0 : typeof flags?.providerHint === "string" ? flags.providerHint : "set AGENTMEMORY_LLM_PROVIDER and that provider's key (for example ANTHROPIC_API_KEY) in ~/.agentmemory/.env"
3599
4040
  }, {
3600
4041
  name: "Embedding provider",
3601
4042
  ok: hasEmbed,
3602
- 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"
4043
+ hint: hasEmbed ? void 0 : typeof flags?.embeddingHint === "string" ? flags.embeddingHint : "Running BM25-only. Set EMBEDDING_PROVIDER=local for on-device semantic search, or EMBEDDING_PROVIDER=openai, voyage, cohere, gemini or openrouter with that provider's key"
3603
4044
  });
3604
4045
  for (const f of flags?.flags || []) checks.push({
3605
4046
  name: f.label,
@@ -3703,6 +4144,8 @@ async function runDoctor() {
3703
4144
  let fixed = 0;
3704
4145
  let skipped = 0;
3705
4146
  let quit = false;
4147
+ const promptsAllowed = doctorPromptsAllowed(process.env, process.stdin.isTTY, process.stdout.isTTY);
4148
+ let autoFixHintShown = false;
3706
4149
  for (const d of diagnostics) {
3707
4150
  if (quit) {
3708
4151
  skipped++;
@@ -3715,13 +4158,25 @@ async function runDoctor() {
3715
4158
  }
3716
4159
  failed++;
3717
4160
  p.log.warn(`${d.id} ✗ ${status.detail ?? ""}`.trim());
4161
+ if (!canAutoFix(d, status)) {
4162
+ p.log.info(`manual fix: ${status.manualFix ?? d.fixPreview}`);
4163
+ skipped++;
4164
+ continue;
4165
+ }
3718
4166
  p.log.info(`why: ${d.fixPreview}`);
3719
- if (d.manualOnly) p.log.info(`(manual fix only — see "${d.id}" docs)`);
3720
4167
  if (applyAll) {
3721
4168
  if ((await applyFixWithReport(d, ctx, false)).ok) fixed++;
3722
4169
  if (!(await d.check(ctx)).ok) p.log.warn(`${d.id} still failing after fix.`);
3723
4170
  continue;
3724
4171
  }
4172
+ if (!promptsAllowed) {
4173
+ skipped++;
4174
+ if (!autoFixHintShown) {
4175
+ autoFixHintShown = true;
4176
+ p.log.info("Not prompting in a non-interactive shell. Run `agentmemory doctor --all` to apply the fixes.");
4177
+ }
4178
+ continue;
4179
+ }
3725
4180
  while (true) {
3726
4181
  const action = await askFixAction(d);
3727
4182
  if (action === "fix") {
@@ -3938,7 +4393,7 @@ async function runInit() {
3938
4393
  "All keys are commented out by default. Uncomment the ones you want.",
3939
4394
  "",
3940
4395
  "Common next steps:",
3941
- " 1. Pick an LLM provider key (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY / etc.)",
4396
+ " 1. Optional: to send session content to an LLM for summaries, set AGENTMEMORY_LLM_PROVIDER and that provider's key (for example ANTHROPIC_API_KEY)",
3942
4397
  " 2. Run `npx @agentmemory/agentmemory doctor` to verify the daemon sees them",
3943
4398
  " 3. Run `npx @agentmemory/agentmemory` to start the worker"
3944
4399
  ].join("\n"), "Next steps");
@@ -4016,7 +4471,7 @@ async function runDemoBody(base) {
4016
4471
  ...semanticHits > 0 ? [c.accent(`Notice: searching "database performance optimization"`), c.accent(`found the N+1 query fix — keyword matching can't do that.`)] : [
4017
4472
  c.dim(`Note: "database performance optimization" found nothing —`),
4018
4473
  c.dim(`set EMBEDDING_PROVIDER=local for on-device semantic recall,`),
4019
- c.dim(`or add an embedding API key in ~/.agentmemory/.env.`)
4474
+ c.dim(`or set EMBEDDING_PROVIDER and its API key in ~/.agentmemory/.env.`)
4020
4475
  ],
4021
4476
  "",
4022
4477
  `Viewer: ${c.url(getViewerUrl())}`,
@@ -4313,7 +4768,7 @@ async function runStop() {
4313
4768
  namespace: state.namespace,
4314
4769
  functionId: "compose::down",
4315
4770
  file: state.file,
4316
- timeoutMs: 6e4
4771
+ timeoutMs: composeDownTimeoutMs()
4317
4772
  });
4318
4773
  } catch (err) {
4319
4774
  const message = err instanceof Error ? err.message : String(err);
@@ -4416,10 +4871,10 @@ async function runStop() {
4416
4871
  p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
4417
4872
  }
4418
4873
  async function runMcp() {
4419
- await import("./standalone-BlqshyZ5.mjs");
4874
+ await import("./standalone-BYo5USDb.mjs");
4420
4875
  }
4421
4876
  async function runConnectCmd() {
4422
- const { runConnect } = await import("./connect-BSLIAMpw.mjs").then((n) => n.n);
4877
+ const { runConnect } = await import("./connect-BX3njQ3Y.mjs").then((n) => n.n);
4423
4878
  await runConnect(args.slice(1));
4424
4879
  }
4425
4880
  async function runImportJsonl() {