@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.
- package/.env.example +98 -28
- package/AGENTS.md +19 -26
- package/README.md +619 -274
- package/dist/.env.example +98 -28
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +1328 -244
- package/dist/cli.mjs.map +1 -1
- package/dist/{connect-JnsJXc68.mjs → connect-B4_ca6BD.mjs} +619 -70
- package/dist/connect-B4_ca6BD.mjs.map +1 -0
- package/dist/docker-compose.yml +42 -16
- package/dist/hooks/_capture.d.mts +22 -0
- package/dist/hooks/_capture.d.mts.map +1 -0
- package/dist/hooks/_capture.mjs +908 -0
- package/dist/hooks/_capture.mjs.map +1 -0
- package/dist/hooks/antigravity-bridge.d.mts +8 -0
- package/dist/hooks/antigravity-bridge.d.mts.map +1 -0
- package/dist/hooks/antigravity-bridge.mjs +123 -0
- package/dist/hooks/antigravity-bridge.mjs.map +1 -0
- package/dist/hooks/notification.mjs +34 -27
- package/dist/hooks/notification.mjs.map +1 -1
- package/dist/hooks/post-commit.mjs +84 -2
- package/dist/hooks/post-commit.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +71 -26
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +99 -36
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +95 -5
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +72 -2
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/prompt-submit.mjs +25 -22
- package/dist/hooks/prompt-submit.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +93 -24
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +101 -6
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +72 -8
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-start.mjs +28 -26
- package/dist/hooks/subagent-start.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +29 -26
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +31 -28
- package/dist/hooks/task-completed.mjs.map +1 -1
- package/dist/iii-config.docker.yaml +10 -10
- package/dist/iii-config.yaml +3 -1
- package/dist/{image-refs-BKLmp_sD.mjs → image-refs-BaT-9oMU.mjs} +2 -2
- package/dist/image-refs-BaT-9oMU.mjs.map +1 -0
- package/dist/{image-refs-C2YU7BIm.mjs → image-refs-T_urbwHM.mjs} +3 -3
- package/dist/image-refs-T_urbwHM.mjs.map +1 -0
- package/dist/{keyed-mutex-DTHtcZrt.mjs → image-store-BsN4vyjr.mjs} +2 -14
- package/dist/image-store-BsN4vyjr.mjs.map +1 -0
- package/dist/index.mjs +14021 -6494
- package/dist/index.mjs.map +1 -1
- package/dist/keyed-mutex-DchXsyfP.mjs +221 -0
- package/dist/keyed-mutex-DchXsyfP.mjs.map +1 -0
- package/dist/secret-store-BnY3A50C.mjs +2348 -0
- package/dist/secret-store-BnY3A50C.mjs.map +1 -0
- package/dist/{src-CzgoepGU.mjs → src-Jsq9LCeH.mjs} +7349 -2940
- package/dist/src-Jsq9LCeH.mjs.map +1 -0
- package/dist/{standalone-YsZFFQBM.mjs → standalone-DKCdrf1d.mjs} +62 -24
- package/dist/standalone-DKCdrf1d.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +298 -27
- package/dist/standalone.mjs.map +1 -1
- package/dist/status-IRz7ta8-.mjs +2344 -0
- package/dist/status-IRz7ta8-.mjs.map +1 -0
- package/dist/viewer/favicon.svg +35 -1
- package/dist/viewer/index.html +7428 -2608
- package/docker-compose.yml +42 -16
- package/iii-config.docker.yaml +10 -10
- package/iii-config.yaml +3 -1
- package/integrations/pi/README.md +77 -0
- package/integrations/pi/index.ts +402 -0
- package/integrations/pi/package.json +32 -0
- package/integrations/pi/security.ts +87 -0
- package/package.json +15 -7
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +26 -4
- package/plugin/.devin-plugin/plugin.json +32 -0
- package/plugin/.mcp.codex.json +12 -0
- package/plugin/.mcp.copilot.json +2 -7
- package/plugin/.mcp.json +2 -7
- package/plugin/assets/icon.svg +35 -0
- package/plugin/cursor/hooks.json +41 -0
- package/plugin/cursor/mcp.json +12 -0
- package/plugin/hooks/hooks.antigravity.json +43 -0
- package/plugin/hooks/hooks.copilot.json +11 -11
- package/plugin/hooks/hooks.devin.json +67 -0
- package/plugin/hooks/hooks.droid.json +55 -0
- package/plugin/opencode/README.md +267 -31
- package/plugin/opencode/agentmemory-capture.ts +644 -15
- package/plugin/opencode/plugin.json +1 -1
- package/plugin/plugin.json +2 -2
- package/plugin/scripts/_capture.d.mts +22 -0
- package/plugin/scripts/_capture.d.mts.map +1 -0
- package/plugin/scripts/_capture.mjs +908 -0
- package/plugin/scripts/_capture.mjs.map +1 -0
- package/plugin/scripts/antigravity-bridge.d.mts +8 -0
- package/plugin/scripts/antigravity-bridge.d.mts.map +1 -0
- package/plugin/scripts/antigravity-bridge.mjs +123 -0
- package/plugin/scripts/antigravity-bridge.mjs.map +1 -0
- package/plugin/scripts/notification.mjs +34 -27
- package/plugin/scripts/notification.mjs.map +1 -1
- package/plugin/scripts/plugin-bridge.mjs +378 -0
- package/plugin/scripts/post-commit.mjs +84 -2
- package/plugin/scripts/post-commit.mjs.map +1 -1
- package/plugin/scripts/post-tool-failure.mjs +71 -26
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +99 -36
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +95 -5
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +72 -2
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/prompt-submit.mjs +25 -22
- package/plugin/scripts/prompt-submit.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +93 -24
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +101 -6
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +72 -8
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-start.mjs +28 -26
- package/plugin/scripts/subagent-start.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +29 -26
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +31 -28
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/plugin/skills/_shared/TROUBLESHOOTING.md +22 -6
- package/plugin/skills/agentmemory-agents/REFERENCE.md +8 -5
- package/plugin/skills/agentmemory-config/REFERENCE.md +52 -1
- package/plugin/skills/agentmemory-config/SKILL.md +44 -7
- package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +4 -3
- package/plugin/skills/agentmemory-rest-api/REFERENCE.md +23 -2
- package/plugin/skills/forget/SKILL.md +4 -1
- package/plugin/skills/handoff/SKILL.md +4 -2
- package/plugin/skills/lesson/SKILL.md +64 -0
- package/plugin/skills/memory-discipline/SKILL.md +65 -0
- package/plugin/skills/recall/SKILL.md +4 -1
- package/plugin/skills/remember/SKILL.md +6 -1
- package/dist/config-ChC9EtgV.mjs +0 -259
- package/dist/config-ChC9EtgV.mjs.map +0 -1
- package/dist/connect-JnsJXc68.mjs.map +0 -1
- package/dist/image-refs-BKLmp_sD.mjs.map +0 -1
- package/dist/image-refs-C2YU7BIm.mjs.map +0 -1
- package/dist/keyed-mutex-DTHtcZrt.mjs.map +0 -1
- package/dist/logger-BEB7pCI9.mjs +0 -43
- package/dist/logger-BEB7pCI9.mjs.map +0 -1
- package/dist/schema-CNiCWzV9.mjs +0 -78
- package/dist/schema-CNiCWzV9.mjs.map +0 -1
- package/dist/src-CzgoepGU.mjs.map +0 -1
- package/dist/standalone-YsZFFQBM.mjs.map +0 -1
- package/dist/tools-registry-D3NEoe5m.mjs +0 -1115
- package/dist/tools-registry-D3NEoe5m.mjs.map +0 -1
- package/plugin/scripts/diagnostics.d.mts +0 -17
- package/plugin/scripts/diagnostics.d.mts.map +0 -1
- 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-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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(
|
|
229
|
-
return join(
|
|
229
|
+
function pidfilePath(runtimeDir) {
|
|
230
|
+
return join(runtimeDir, "iii.pid");
|
|
230
231
|
}
|
|
231
|
-
function
|
|
232
|
-
return join(
|
|
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(
|
|
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(
|
|
291
|
+
path: pidfilePath(runtimeDir),
|
|
291
292
|
alwaysAsk: false,
|
|
292
|
-
applicable: pathExists(pidfilePath(
|
|
293
|
-
sizeBytes: safeSize(pidfilePath(
|
|
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(
|
|
307
|
+
path: enginePath(runtimeDir),
|
|
299
308
|
alwaysAsk: false,
|
|
300
|
-
applicable: pathExists(enginePath(
|
|
301
|
-
sizeBytes: safeSize(enginePath(
|
|
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:
|
|
359
|
-
path: dataDir
|
|
375
|
+
description: `Delete memory data directory (${dataDir}) — will ask separately`,
|
|
376
|
+
path: dataDir,
|
|
360
377
|
alwaysAsk: true,
|
|
361
|
-
applicable: !options.keepData && pathExists(dataDir
|
|
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 —
|
|
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
|
|
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
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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 =
|
|
1290
|
+
const vPort = getConfiguredViewerPort();
|
|
945
1291
|
return `${u.protocol}//${u.hostname}:${vPort}`;
|
|
946
1292
|
} catch {
|
|
947
|
-
return `http://localhost:${
|
|
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
|
|
1429
|
+
return runtimeMetadataPath("iii.pid");
|
|
1072
1430
|
}
|
|
1073
1431
|
function engineStatePath() {
|
|
1074
|
-
return
|
|
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
|
|
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
|
-
|
|
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.
|
|
1179
|
-
], { label: `Installing @agentmemory/agentmemory@0.9.
|
|
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
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
1327
|
-
|
|
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, [
|
|
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
|
-
|
|
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
|
-
|
|
1451
|
-
});
|
|
1452
|
-
spawnEngineBackground(dockerBin, [
|
|
1453
|
-
"compose",
|
|
1454
|
-
"-f",
|
|
2217
|
+
schemaVersion: 2,
|
|
1455
2218
|
composeFile,
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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)
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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-
|
|
2373
|
+
await import("./src-Jsq9LCeH.mjs");
|
|
1556
2374
|
if (await waitForAgentmemoryReady(15e3)) {
|
|
1557
|
-
const consoleState = await ensureIiiConsole();
|
|
1558
2375
|
await maybeOfferGlobalInstall();
|
|
1559
|
-
printReadyHint(
|
|
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
|
|
1571
|
-
if (await waitForAgentmemoryReady(15e3)) {
|
|
1572
|
-
|
|
1573
|
-
|
|
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(
|
|
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
|
|
1642
|
-
if (await waitForAgentmemoryReady(15e3)) {
|
|
1643
|
-
|
|
1644
|
-
|
|
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.
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
|
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
|
|
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
|
-
|
|
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, [
|
|
2407
|
-
|
|
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, [
|
|
2413
|
-
|
|
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(
|
|
3452
|
+
async function stopDockerEngine(state, port, suppressOutro = false) {
|
|
2504
3453
|
const dockerBin = whichBinary("docker");
|
|
2505
3454
|
if (!dockerBin) {
|
|
2506
|
-
p.log.error(
|
|
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
|
-
|
|
2510
|
-
|
|
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 (
|
|
2523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
2599
|
-
|
|
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-
|
|
3613
|
+
await import("./standalone-DKCdrf1d.mjs");
|
|
2623
3614
|
}
|
|
2624
3615
|
async function runConnectCmd() {
|
|
2625
|
-
const { runConnect } = await import("./connect-
|
|
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([
|
|
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 = {
|
|
2681
|
-
|
|
2682
|
-
|
|
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
|
|
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
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
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
|
-
|
|
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
|
});
|