@agentmemory/agentmemory 0.9.29 → 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 +83 -22
- package/AGENTS.md +17 -24
- package/README.md +447 -170
- package/dist/.env.example +83 -22
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +1221 -335
- package/dist/cli.mjs.map +1 -1
- package/dist/{connect-Qaeq15TQ.mjs → connect-B4_ca6BD.mjs} +30 -39
- package/dist/connect-B4_ca6BD.mjs.map +1 -0
- package/dist/docker-compose.yml +40 -14
- 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 +1 -1
- package/dist/hooks/antigravity-bridge.d.mts.map +1 -1
- package/dist/hooks/antigravity-bridge.mjs +24 -15
- package/dist/hooks/antigravity-bridge.mjs.map +1 -1
- package/dist/hooks/notification.mjs +17 -25
- package/dist/hooks/notification.mjs.map +1 -1
- package/dist/hooks/post-commit.mjs +71 -1
- package/dist/hooks/post-commit.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +59 -25
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +87 -35
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +83 -3
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +71 -1
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/prompt-submit.mjs +13 -21
- package/dist/hooks/prompt-submit.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +25 -20
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +80 -3
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +71 -1
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-start.mjs +16 -25
- package/dist/hooks/subagent-start.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +17 -25
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +19 -27
- 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-CJgZmb9X.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 +12943 -6218
- package/dist/index.mjs.map +1 -1
- package/dist/{schema-BFZhNTF7.mjs → keyed-mutex-DchXsyfP.mjs} +28 -4
- 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-G7yt8gGm.mjs → src-Jsq9LCeH.mjs} +5941 -2180
- package/dist/src-Jsq9LCeH.mjs.map +1 -0
- package/dist/{standalone-BqNJDJ04.mjs → standalone-DKCdrf1d.mjs} +39 -12
- package/dist/standalone-DKCdrf1d.mjs.map +1 -0
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +249 -14
- 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/index.html +7224 -2794
- package/docker-compose.yml +40 -14
- package/iii-config.docker.yaml +10 -10
- package/iii-config.yaml +3 -1
- package/integrations/pi/index.ts +2 -3
- package/integrations/pi/security.ts +52 -0
- package/package.json +13 -4
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +26 -4
- package/plugin/.devin-plugin/plugin.json +1 -1
- 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/hooks/hooks.copilot.json +11 -11
- package/plugin/opencode/README.md +266 -30
- package/plugin/opencode/agentmemory-capture.ts +572 -3
- package/plugin/opencode/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
- 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 +1 -1
- package/plugin/scripts/antigravity-bridge.d.mts.map +1 -1
- package/plugin/scripts/antigravity-bridge.mjs +24 -15
- package/plugin/scripts/antigravity-bridge.mjs.map +1 -1
- package/plugin/scripts/notification.mjs +17 -25
- package/plugin/scripts/notification.mjs.map +1 -1
- package/plugin/scripts/plugin-bridge.mjs +378 -0
- package/plugin/scripts/post-commit.mjs +71 -1
- package/plugin/scripts/post-commit.mjs.map +1 -1
- package/plugin/scripts/post-tool-failure.mjs +59 -25
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +87 -35
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +83 -3
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +71 -1
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/prompt-submit.mjs +13 -21
- package/plugin/scripts/prompt-submit.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +25 -20
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +80 -3
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +71 -1
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-start.mjs +16 -25
- package/plugin/scripts/subagent-start.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +17 -25
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +19 -27
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/plugin/skills/_shared/TROUBLESHOOTING.md +22 -6
- package/plugin/skills/agentmemory-agents/REFERENCE.md +3 -3
- package/plugin/skills/agentmemory-config/REFERENCE.md +49 -1
- package/plugin/skills/agentmemory-config/SKILL.md +44 -7
- package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +1 -1
- package/plugin/skills/agentmemory-rest-api/REFERENCE.md +10 -2
- package/plugin/skills/handoff/SKILL.md +4 -2
- package/plugin/skills/memory-discipline/SKILL.md +13 -5
- package/dist/connect-Qaeq15TQ.mjs.map +0 -1
- package/dist/image-refs-BKLmp_sD.mjs.map +0 -1
- package/dist/image-refs-CJgZmb9X.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-BFZhNTF7.mjs.map +0 -1
- package/dist/src-G7yt8gGm.mjs.map +0 -1
- package/dist/standalone-BqNJDJ04.mjs.map +0 -1
- package/dist/tools-registry-BV_2bu7z.mjs +0 -1405
- package/dist/tools-registry-BV_2bu7z.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,
|
|
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
|
};
|
|
@@ -765,85 +972,84 @@ async function wireSelectedAgents(agents) {
|
|
|
765
972
|
p.note(summary.join("\n"), "wire summary");
|
|
766
973
|
}
|
|
767
974
|
//#endregion
|
|
768
|
-
//#region src/cli
|
|
769
|
-
function
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
return
|
|
790
|
-
}
|
|
791
|
-
const xdgDataHome = env["XDG_DATA_HOME"];
|
|
792
|
-
if (useXdg && xdgDataHome && isAbsolute(xdgDataHome)) return join(xdgDataHome, "agentmemory");
|
|
793
|
-
return join(home, ".local", "share", "agentmemory");
|
|
794
|
-
}
|
|
795
|
-
function nearestGitParent(pathValue) {
|
|
796
|
-
let current = pathValue;
|
|
797
|
-
while (true) {
|
|
798
|
-
if (existsSync(join(current, ".git"))) return current;
|
|
799
|
-
const parent = dirname(current);
|
|
800
|
-
if (parent === current) return void 0;
|
|
801
|
-
current = parent;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
function isWithin(parent, child) {
|
|
805
|
-
const pathFromParent = relative(parent, child);
|
|
806
|
-
return pathFromParent === "" || pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent);
|
|
807
|
-
}
|
|
808
|
-
function resolveDataDir(options = {}) {
|
|
809
|
-
const args = options.args ?? process.argv.slice(2);
|
|
810
|
-
const env = options.env ?? process.env;
|
|
811
|
-
const cwd = options.cwd ?? process.cwd();
|
|
812
|
-
const home = options.home ?? homedir();
|
|
813
|
-
const nodePlatform = options.platform ?? platform();
|
|
814
|
-
const parsedInstance = parseInt(argValue(args, "--instance") ?? "0", 10);
|
|
815
|
-
const instance = parsedInstance > 0 && parsedInstance <= 50 ? parsedInstance : 0;
|
|
816
|
-
const forInstance = (dataDir) => instance ? join(dataDir, `instance-${instance}`) : dataDir;
|
|
817
|
-
const flagValue = argValue(args, "--data-dir");
|
|
818
|
-
if (flagValue) return {
|
|
819
|
-
dataDir: forInstance(absoluteDataDir(flagValue, cwd, home)),
|
|
820
|
-
source: "flag"
|
|
821
|
-
};
|
|
822
|
-
const envValue = env["AGENTMEMORY_DATA_DIR"];
|
|
823
|
-
if (envValue) return {
|
|
824
|
-
dataDir: forInstance(absoluteDataDir(envValue, cwd, home)),
|
|
825
|
-
source: "env"
|
|
826
|
-
};
|
|
827
|
-
const legacyDir = resolve(cwd, "data");
|
|
828
|
-
const hasLegacyStore = existsSync(join(legacyDir, "state_store.db")) || existsSync(join(legacyDir, "iii-config.yaml"));
|
|
829
|
-
if (instance === 0 && hasLegacyStore) return {
|
|
830
|
-
dataDir: legacyDir,
|
|
831
|
-
source: "default"
|
|
832
|
-
};
|
|
833
|
-
const defaultDir = platformDefaultDataDir(env, home, nodePlatform);
|
|
834
|
-
const gitParent = nearestGitParent(cwd);
|
|
835
|
-
if (gitParent && isWithin(gitParent, defaultDir)) {
|
|
836
|
-
const relocated = platformDefaultDataDir(env, home, nodePlatform, false);
|
|
837
|
-
if (relocated !== defaultDir) return {
|
|
838
|
-
dataDir: forInstance(relocated),
|
|
839
|
-
source: "default",
|
|
840
|
-
relocatedFrom: defaultDir
|
|
841
|
-
};
|
|
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) };
|
|
842
997
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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;
|
|
847
1053
|
}
|
|
848
1054
|
//#endregion
|
|
849
1055
|
//#region src/cli.ts
|
|
@@ -870,7 +1076,9 @@ if (args.includes("--version") || args.includes("-V")) {
|
|
|
870
1076
|
process.stdout.write(`${VERSION}\n`);
|
|
871
1077
|
process.exit(0);
|
|
872
1078
|
}
|
|
873
|
-
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`;
|
|
874
1082
|
function iiiReleaseAsset() {
|
|
875
1083
|
const p = platform();
|
|
876
1084
|
const a = process.arch;
|
|
@@ -918,6 +1126,8 @@ Commands:
|
|
|
918
1126
|
No arg = interactive picker. --all wires every detected agent.
|
|
919
1127
|
--dry-run shows what would change. --force re-installs.
|
|
920
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
|
|
921
1131
|
doctor Interactive diagnostic + fixer. [F]ix · [S]kip · [?]more · [Q]uit
|
|
922
1132
|
--all: apply every fix without prompting (CI)
|
|
923
1133
|
--dry-run: show what each fix would do, don't execute
|
|
@@ -933,6 +1143,9 @@ Commands:
|
|
|
933
1143
|
the engine was started natively but state file is missing).
|
|
934
1144
|
mcp Start standalone MCP shim — opt-in surface for MCP-only clients
|
|
935
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.
|
|
936
1149
|
import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
|
|
937
1150
|
--max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
|
|
938
1151
|
out-of-range is rejected; for trees >1000 files, batch by subdirectory)
|
|
@@ -958,6 +1171,9 @@ Environment:
|
|
|
958
1171
|
AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
|
|
959
1172
|
native iii-engine binary on first run.
|
|
960
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).
|
|
961
1177
|
AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS
|
|
962
1178
|
Window (seconds) for the smart-search follow-up diagnostic
|
|
963
1179
|
(default 30). Long values overcount, short values undercount.
|
|
@@ -979,21 +1195,62 @@ if (toolsIdx !== -1 && args[toolsIdx + 1]) {
|
|
|
979
1195
|
if (toolsMode !== "all" && toolsMode !== "core") p.log.warn(`Unknown --tools value "${toolsMode}" (valid: all, core); falling back to all.`);
|
|
980
1196
|
process.env["AGENTMEMORY_TOOLS"] = toolsMode;
|
|
981
1197
|
}
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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);
|
|
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);
|
|
990
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);
|
|
991
1244
|
}
|
|
992
1245
|
const dataDirResolution = resolveDataDir({ args });
|
|
993
|
-
|
|
1246
|
+
process.env["AGENTMEMORY_DATA_DIR"] = dataDirResolution.dataDir;
|
|
1247
|
+
process.env["AGENTMEMORY_RUNTIME_DIR"] = selectedInstance > 0 ? dataDirResolution.dataDir : join(homedir(), ".agentmemory");
|
|
994
1248
|
const skipEngine = args.includes("--no-engine");
|
|
1249
|
+
function shouldUseAgentmemoryUrl() {
|
|
1250
|
+
return !hasExplicitLocalPortOverride && URL_CLIENT_COMMANDS.has(args[0] ?? "");
|
|
1251
|
+
}
|
|
995
1252
|
function getRestPort() {
|
|
996
|
-
const url = process.env["AGENTMEMORY_URL"];
|
|
1253
|
+
const url = shouldUseAgentmemoryUrl() ? process.env["AGENTMEMORY_URL"] : void 0;
|
|
997
1254
|
if (url) try {
|
|
998
1255
|
const parsed = new URL(url).port;
|
|
999
1256
|
if (parsed) return parseInt(parsed, 10);
|
|
@@ -1001,10 +1258,13 @@ function getRestPort() {
|
|
|
1001
1258
|
return parseInt(process.env["III_REST_PORT"] || "3111", 10) || 3111;
|
|
1002
1259
|
}
|
|
1003
1260
|
function getBaseUrl() {
|
|
1004
|
-
const url = process.env["AGENTMEMORY_URL"];
|
|
1261
|
+
const url = shouldUseAgentmemoryUrl() ? process.env["AGENTMEMORY_URL"] : void 0;
|
|
1005
1262
|
if (url) return url.replace(/\/+$/, "");
|
|
1006
1263
|
return `http://localhost:${getRestPort()}`;
|
|
1007
1264
|
}
|
|
1265
|
+
function getConfiguredViewerPort() {
|
|
1266
|
+
return parseInt(process.env["III_VIEWER_PORT"] || "", 10) || getRestPort() + 2;
|
|
1267
|
+
}
|
|
1008
1268
|
let discoveredViewerPort = null;
|
|
1009
1269
|
async function discoverViewerPort() {
|
|
1010
1270
|
if (discoveredViewerPort !== null) return;
|
|
@@ -1027,23 +1287,23 @@ function getViewerUrl() {
|
|
|
1027
1287
|
}
|
|
1028
1288
|
try {
|
|
1029
1289
|
const u = new URL(getBaseUrl());
|
|
1030
|
-
const vPort =
|
|
1290
|
+
const vPort = getConfiguredViewerPort();
|
|
1031
1291
|
return `${u.protocol}//${u.hostname}:${vPort}`;
|
|
1032
1292
|
} catch {
|
|
1033
|
-
return `http://localhost:${
|
|
1293
|
+
return `http://localhost:${getConfiguredViewerPort()}`;
|
|
1034
1294
|
}
|
|
1035
1295
|
}
|
|
1036
1296
|
function getStreamPort() {
|
|
1037
1297
|
return parseInt(process.env["III_STREAM_PORT"] || "", 10) || parseInt(process.env["III_STREAMS_PORT"] || "", 10) || getRestPort() + 1;
|
|
1038
1298
|
}
|
|
1039
1299
|
function getEnginePort() {
|
|
1040
|
-
const explicit = parseInt(process.env["III_ENGINE_PORT"] || "", 10);
|
|
1041
|
-
if (explicit) return explicit;
|
|
1042
1300
|
const url = process.env["III_ENGINE_URL"];
|
|
1043
1301
|
if (url) try {
|
|
1044
1302
|
const parsed = new URL(url).port;
|
|
1045
1303
|
if (parsed) return parseInt(parsed, 10);
|
|
1046
1304
|
} catch {}
|
|
1305
|
+
const explicit = parseInt(process.env["III_ENGINE_PORT"] || "", 10);
|
|
1306
|
+
if (explicit) return explicit;
|
|
1047
1307
|
return getRestPort() + 46023;
|
|
1048
1308
|
}
|
|
1049
1309
|
async function isEngineRunning() {
|
|
@@ -1082,21 +1342,9 @@ function findIiiConfig() {
|
|
|
1082
1342
|
join(__dirname, "iii-config.yaml"),
|
|
1083
1343
|
join(__dirname, "..", "iii-config.yaml")
|
|
1084
1344
|
];
|
|
1085
|
-
for (const c of candidates) if (existsSync(c)) return c;
|
|
1345
|
+
for (const c of candidates) if (existsSync(c)) return resolve(c);
|
|
1086
1346
|
return "";
|
|
1087
1347
|
}
|
|
1088
|
-
function yamlSingleQuoted(value) {
|
|
1089
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
1090
|
-
}
|
|
1091
|
-
function renderIiiConfig(template, dataDir) {
|
|
1092
|
-
return template.replace("file_path: ./data/state_store.db", `file_path: ${yamlSingleQuoted(join(dataDir, "state_store.db"))}`).replace("file_path: ./data/stream_store", `file_path: ${yamlSingleQuoted(join(dataDir, "stream_store"))}`);
|
|
1093
|
-
}
|
|
1094
|
-
function writeRuntimeIiiConfig(configPath, dataDir) {
|
|
1095
|
-
mkdirSync(dataDir, { recursive: true });
|
|
1096
|
-
const runtimeConfigPath = join(dataDir, "iii-config.yaml");
|
|
1097
|
-
writeFileSync(runtimeConfigPath, renderIiiConfig(readFileSync(configPath, "utf8"), dataDir));
|
|
1098
|
-
return runtimeConfigPath;
|
|
1099
|
-
}
|
|
1100
1348
|
function warnIfRelocatedDataDir() {
|
|
1101
1349
|
if (!dataDirResolution.relocatedFrom) return;
|
|
1102
1350
|
try {
|
|
@@ -1178,10 +1426,10 @@ function resolveCompatibleIii(iiiBinPath) {
|
|
|
1178
1426
|
return null;
|
|
1179
1427
|
}
|
|
1180
1428
|
function enginePidfilePath() {
|
|
1181
|
-
return
|
|
1429
|
+
return runtimeMetadataPath("iii.pid");
|
|
1182
1430
|
}
|
|
1183
1431
|
function engineStatePath() {
|
|
1184
|
-
return
|
|
1432
|
+
return runtimeMetadataPath("engine-state.json");
|
|
1185
1433
|
}
|
|
1186
1434
|
function writeEnginePidfile(pid) {
|
|
1187
1435
|
try {
|
|
@@ -1207,7 +1455,7 @@ function clearEnginePidfile() {
|
|
|
1207
1455
|
} catch {}
|
|
1208
1456
|
}
|
|
1209
1457
|
function workerPidfilePath() {
|
|
1210
|
-
return
|
|
1458
|
+
return runtimeMetadataPath("worker.pid");
|
|
1211
1459
|
}
|
|
1212
1460
|
function readWorkerPidfile() {
|
|
1213
1461
|
try {
|
|
@@ -1227,7 +1475,15 @@ function writeEngineState(state) {
|
|
|
1227
1475
|
try {
|
|
1228
1476
|
const statePath = engineStatePath();
|
|
1229
1477
|
mkdirSync(dirname(statePath), { recursive: true });
|
|
1230
|
-
|
|
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" });
|
|
1231
1487
|
} catch (err) {
|
|
1232
1488
|
vlog(`writeEngineState: ${err instanceof Error ? err.message : String(err)}`);
|
|
1233
1489
|
}
|
|
@@ -1247,6 +1503,44 @@ function clearEngineState() {
|
|
|
1247
1503
|
unlinkSync(engineStatePath());
|
|
1248
1504
|
} catch {}
|
|
1249
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
|
+
}
|
|
1250
1544
|
function discoverComposeFile() {
|
|
1251
1545
|
return [
|
|
1252
1546
|
join(__dirname, "..", "docker-compose.yml"),
|
|
@@ -1254,6 +1548,251 @@ function discoverComposeFile() {
|
|
|
1254
1548
|
join(process.cwd(), "docker-compose.yml")
|
|
1255
1549
|
].find((c) => existsSync(c)) ?? null;
|
|
1256
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
|
+
}
|
|
1257
1796
|
function isInvokedViaNpx() {
|
|
1258
1797
|
if (process.env["npm_lifecycle_event"] === "npx") return true;
|
|
1259
1798
|
if ((process.argv[1] ?? "").includes("_npx")) return true;
|
|
@@ -1285,56 +1824,12 @@ async function maybeOfferGlobalInstall() {
|
|
|
1285
1824
|
if (runCommand(npmBin, [
|
|
1286
1825
|
"install",
|
|
1287
1826
|
"-g",
|
|
1288
|
-
`@agentmemory/agentmemory@0.9.
|
|
1289
|
-
], { label: `Installing @agentmemory/agentmemory@0.9.
|
|
1827
|
+
`@agentmemory/agentmemory@0.9.30`
|
|
1828
|
+
], { label: `Installing @agentmemory/agentmemory@0.9.30 globally` })) {
|
|
1290
1829
|
p.log.success("Installed globally. `agentmemory stop` etc. will now work in new shells.");
|
|
1291
1830
|
writePrefs({ skipGlobalInstall: true });
|
|
1292
1831
|
} else p.log.warn("Global install failed. Try manually: npm install -g @agentmemory/agentmemory");
|
|
1293
1832
|
}
|
|
1294
|
-
function detectIiiConsole() {
|
|
1295
|
-
const onPath = whichBinary("iii-console");
|
|
1296
|
-
if (onPath) return {
|
|
1297
|
-
kind: "installed",
|
|
1298
|
-
binPath: onPath
|
|
1299
|
-
};
|
|
1300
|
-
const fallback = IS_WINDOWS ? join(process.env["USERPROFILE"] ?? "", ".local", "bin", "iii-console.exe") : join(homedir(), ".local", "bin", "iii-console");
|
|
1301
|
-
if (fallback && existsSync(fallback)) return {
|
|
1302
|
-
kind: "installed",
|
|
1303
|
-
binPath: fallback
|
|
1304
|
-
};
|
|
1305
|
-
return { kind: "missing" };
|
|
1306
|
-
}
|
|
1307
|
-
const III_CONSOLE_INSTALL_CMD = `curl -fsSL https://install.iii.dev/iii/main/install.sh | VERSION=${IIPINNED_VERSION} sh`;
|
|
1308
|
-
function iiiConsoleInstallHint() {
|
|
1309
|
-
if (!IS_WINDOWS) return III_CONSOLE_INSTALL_CMD;
|
|
1310
|
-
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}`;
|
|
1311
|
-
}
|
|
1312
|
-
async function ensureIiiConsole() {
|
|
1313
|
-
const state = detectIiiConsole();
|
|
1314
|
-
if (state.kind === "installed") return state;
|
|
1315
|
-
if (!process.stdin.isTTY || process.env["CI"]) return state;
|
|
1316
|
-
if (readPrefs().skipConsoleInstall) return state;
|
|
1317
|
-
const answer = await p.confirm({
|
|
1318
|
-
message: "iii console gives engine-level visibility (workers, functions, queues, traces). Install now?",
|
|
1319
|
-
initialValue: true
|
|
1320
|
-
});
|
|
1321
|
-
if (p.isCancel(answer)) return state;
|
|
1322
|
-
if (answer === false) {
|
|
1323
|
-
writePrefs({ skipConsoleInstall: true });
|
|
1324
|
-
return state;
|
|
1325
|
-
}
|
|
1326
|
-
const shBin = whichBinary("sh");
|
|
1327
|
-
const curlBin = whichBinary("curl");
|
|
1328
|
-
if (!shBin || !curlBin) {
|
|
1329
|
-
p.log.warn(`curl or sh not found. Install manually:\n ${iiiConsoleInstallHint()}`);
|
|
1330
|
-
return state;
|
|
1331
|
-
}
|
|
1332
|
-
if (!runCommand(shBin, ["-c", III_CONSOLE_INSTALL_CMD], { label: "Installing iii console" })) {
|
|
1333
|
-
p.log.warn(`iii console install failed. Re-run manually:\n ${iiiConsoleInstallHint()}`);
|
|
1334
|
-
return state;
|
|
1335
|
-
}
|
|
1336
|
-
return detectIiiConsole();
|
|
1337
|
-
}
|
|
1338
1833
|
function adoptRunningEngine() {
|
|
1339
1834
|
try {
|
|
1340
1835
|
const existingState = readEngineState();
|
|
@@ -1371,7 +1866,7 @@ async function runIiiInstaller() {
|
|
|
1371
1866
|
};
|
|
1372
1867
|
}
|
|
1373
1868
|
if (IS_WINDOWS || isZipAsset) {
|
|
1374
|
-
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}`);
|
|
1375
1870
|
return {
|
|
1376
1871
|
ok: false,
|
|
1377
1872
|
binPath: null
|
|
@@ -1379,8 +1874,14 @@ async function runIiiInstaller() {
|
|
|
1379
1874
|
}
|
|
1380
1875
|
const shBin = whichBinary("sh");
|
|
1381
1876
|
const curlBin = whichBinary("curl");
|
|
1382
|
-
|
|
1383
|
-
|
|
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.`);
|
|
1384
1885
|
return {
|
|
1385
1886
|
ok: false,
|
|
1386
1887
|
binPath: null
|
|
@@ -1388,28 +1889,68 @@ async function runIiiInstaller() {
|
|
|
1388
1889
|
}
|
|
1389
1890
|
const binDir = agentmemoryBinDir();
|
|
1390
1891
|
const binPath = privateIiiPath();
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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}`);
|
|
1400
1909
|
return {
|
|
1401
1910
|
ok: false,
|
|
1402
1911
|
binPath: null
|
|
1403
1912
|
};
|
|
1404
1913
|
}
|
|
1914
|
+
spinner.stop(`${label} ${pc.green("✓")}`);
|
|
1405
1915
|
return {
|
|
1406
1916
|
ok: true,
|
|
1407
1917
|
binPath
|
|
1408
1918
|
};
|
|
1409
1919
|
}
|
|
1410
1920
|
let startupFailure = null;
|
|
1411
|
-
|
|
1412
|
-
|
|
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();
|
|
1413
1954
|
const child = spawn(bin, spawnArgs, {
|
|
1414
1955
|
detached: true,
|
|
1415
1956
|
stdio: [
|
|
@@ -1417,22 +1958,18 @@ function spawnEngineBackground(bin, spawnArgs, label) {
|
|
|
1417
1958
|
"ignore",
|
|
1418
1959
|
"pipe"
|
|
1419
1960
|
],
|
|
1420
|
-
windowsHide: true
|
|
1961
|
+
windowsHide: true,
|
|
1962
|
+
env: engineChildEnv(process.env),
|
|
1963
|
+
...cwd ? { cwd } : {}
|
|
1421
1964
|
});
|
|
1422
1965
|
const isDocker = label.includes("Docker");
|
|
1423
1966
|
if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
|
|
1424
|
-
const stderrChunks = [];
|
|
1425
|
-
let stderrBytes = 0;
|
|
1426
|
-
const MAX_STDERR_CAPTURE = 16 * 1024;
|
|
1427
1967
|
child.stderr?.on("data", (chunk) => {
|
|
1428
|
-
|
|
1429
|
-
const slice = chunk.subarray(0, MAX_STDERR_CAPTURE - stderrBytes);
|
|
1430
|
-
stderrChunks.push(slice);
|
|
1431
|
-
stderrBytes += slice.length;
|
|
1968
|
+
activeStartupStderr.append(chunk);
|
|
1432
1969
|
});
|
|
1433
1970
|
child.on("exit", (code, signal) => {
|
|
1434
1971
|
if (code !== null && code !== 0 || code === null && signal !== null) {
|
|
1435
|
-
const stderr =
|
|
1972
|
+
const stderr = redactCredentialUrls(activeStartupStderr.text());
|
|
1436
1973
|
startupFailure = {
|
|
1437
1974
|
kind: isDocker ? "docker-crashed" : "engine-crashed",
|
|
1438
1975
|
stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
|
|
@@ -1440,22 +1977,100 @@ function spawnEngineBackground(bin, spawnArgs, label) {
|
|
|
1440
1977
|
};
|
|
1441
1978
|
vlog(`engine exited early: code=${code} signal=${signal}`);
|
|
1442
1979
|
if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
|
|
1443
|
-
if (!isDocker)
|
|
1444
|
-
|
|
1980
|
+
if (!isDocker) {
|
|
1981
|
+
clearEnginePidfile();
|
|
1982
|
+
clearEngineState();
|
|
1983
|
+
}
|
|
1445
1984
|
}
|
|
1446
1985
|
});
|
|
1447
1986
|
child.unref();
|
|
1448
1987
|
return child;
|
|
1449
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
|
+
}
|
|
1450
2060
|
function startIiiBin(iiiBin, configPath) {
|
|
1451
2061
|
const s = p.spinner();
|
|
1452
2062
|
s.start(`Starting iii-engine: ${iiiBin}`);
|
|
2063
|
+
const launch = prepareEngineLaunch(configPath);
|
|
1453
2064
|
writeEngineState({
|
|
1454
2065
|
kind: "native",
|
|
1455
|
-
configPath,
|
|
2066
|
+
configPath: launch.configPath,
|
|
1456
2067
|
binPath: iiiBin
|
|
1457
2068
|
});
|
|
1458
|
-
spawnEngineBackground(iiiBin, [
|
|
2069
|
+
spawnEngineBackground(iiiBin, [
|
|
2070
|
+
"--config",
|
|
2071
|
+
launch.configPath,
|
|
2072
|
+
"--no-update-check"
|
|
2073
|
+
], "iii-engine", launch.cwd);
|
|
1459
2074
|
s.stop(c.ok("iii-engine process started"));
|
|
1460
2075
|
return true;
|
|
1461
2076
|
}
|
|
@@ -1467,37 +2082,66 @@ function pickCompatibleIii(candidates) {
|
|
|
1467
2082
|
}
|
|
1468
2083
|
return null;
|
|
1469
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
|
+
}
|
|
1470
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
|
+
}
|
|
1471
2116
|
const configPath = findIiiConfig();
|
|
1472
2117
|
warnIfRelocatedDataDir();
|
|
1473
|
-
const runtimeConfigPath = configPath ? writeRuntimeIiiConfig(configPath, dataDirResolution.dataDir) : "";
|
|
1474
2118
|
const pathIii = whichBinary("iii");
|
|
1475
|
-
vlog(`iii binary: ${pathIii ?? "(not on PATH)"}, config: ${
|
|
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";
|
|
1476
2127
|
const fallbacks = fallbackIiiPaths().filter((p) => existsSync(p));
|
|
1477
2128
|
for (const f of fallbacks) vlog(`fallback iii at ${f} reports version: ${iiiBinVersion(f) ?? "unknown"}`);
|
|
1478
2129
|
let iiiBin = pickCompatibleIii([pathIii, ...fallbacks]);
|
|
1479
|
-
if (iiiBin &&
|
|
2130
|
+
if (iiiBin && configPath && !(dockerOptIn && dockerBin && composeFile)) {
|
|
1480
2131
|
if (iiiBin !== pathIii) {
|
|
1481
2132
|
p.log.info(`Using iii at: ${c.dim(iiiBin)} (v${c.accent(IIPINNED_VERSION)})`);
|
|
1482
2133
|
process.env["PATH"] = `${dirname(iiiBin)}${delimiter}${process.env["PATH"] ?? ""}`;
|
|
1483
2134
|
}
|
|
1484
|
-
return startIiiBin(iiiBin,
|
|
2135
|
+
return startIiiBin(iiiBin, configPath);
|
|
1485
2136
|
}
|
|
1486
2137
|
if (pathIii && !iiiBin) vlog(`iii on PATH is v${iiiBinVersion(pathIii) ?? "unknown"}, pin is v${IIPINNED_VERSION}. Will install pinned engine to ${privateIiiPath()}.`);
|
|
1487
2138
|
if (!configPath) {
|
|
1488
2139
|
startupFailure = { kind: "no-engine" };
|
|
1489
2140
|
return false;
|
|
1490
2141
|
}
|
|
1491
|
-
const dockerBin = whichBinary("docker");
|
|
1492
2142
|
vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
|
|
1493
2143
|
mkdirSync(dataDirResolution.dataDir, { recursive: true });
|
|
1494
|
-
const composeFile = [
|
|
1495
|
-
join(__dirname, "..", "docker-compose.yml"),
|
|
1496
|
-
join(__dirname, "docker-compose.yml"),
|
|
1497
|
-
join(process.cwd(), "docker-compose.yml")
|
|
1498
|
-
].find((c) => existsSync(c));
|
|
1499
2144
|
vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
|
|
1500
|
-
const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
|
|
1501
2145
|
const interactive = !!process.stdin.isTTY && !process.env["CI"];
|
|
1502
2146
|
let choice;
|
|
1503
2147
|
const pathIiiMismatch = pathIii !== null && resolveCompatibleIii(pathIii) === null;
|
|
@@ -1563,19 +2207,22 @@ async function startEngine() {
|
|
|
1563
2207
|
}
|
|
1564
2208
|
}
|
|
1565
2209
|
if (choice === "docker" && dockerBin && composeFile) {
|
|
2210
|
+
warnIfDockerIgnoresStateBackend(stateBackendKind);
|
|
1566
2211
|
const s = p.spinner();
|
|
1567
2212
|
s.start("Starting iii-engine via Docker...");
|
|
2213
|
+
configureDockerHostUser();
|
|
2214
|
+
const projectName = dockerProjectName(getRestPort());
|
|
1568
2215
|
writeEngineState({
|
|
1569
2216
|
kind: "docker",
|
|
1570
|
-
|
|
1571
|
-
});
|
|
1572
|
-
spawnEngineBackground(dockerBin, [
|
|
1573
|
-
"compose",
|
|
1574
|
-
"-f",
|
|
2217
|
+
schemaVersion: 2,
|
|
1575
2218
|
composeFile,
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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");
|
|
1579
2226
|
s.stop("Docker compose started");
|
|
1580
2227
|
return true;
|
|
1581
2228
|
}
|
|
@@ -1591,25 +2238,65 @@ async function waitForEngine(timeoutMs) {
|
|
|
1591
2238
|
}
|
|
1592
2239
|
return false;
|
|
1593
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
|
+
}
|
|
1594
2275
|
function installInstructions() {
|
|
1595
2276
|
const releaseUrl = iiiReleaseUrl();
|
|
1596
|
-
if (IS_WINDOWS)
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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}`;
|
|
1613
2300
|
return [
|
|
1614
2301
|
`agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
|
|
1615
2302
|
"",
|
|
@@ -1636,7 +2323,8 @@ async function waitForAgentmemoryReady(timeoutMs) {
|
|
|
1636
2323
|
return false;
|
|
1637
2324
|
}
|
|
1638
2325
|
function getEngineHost() {
|
|
1639
|
-
|
|
2326
|
+
const envKeys = shouldUseAgentmemoryUrl() ? ["III_ENGINE_URL", "AGENTMEMORY_URL"] : ["III_ENGINE_URL"];
|
|
2327
|
+
for (const envKey of envKeys) {
|
|
1640
2328
|
const raw = process.env[envKey];
|
|
1641
2329
|
if (!raw) continue;
|
|
1642
2330
|
try {
|
|
@@ -1646,13 +2334,16 @@ function getEngineHost() {
|
|
|
1646
2334
|
}
|
|
1647
2335
|
return "localhost";
|
|
1648
2336
|
}
|
|
1649
|
-
function printReadyHint(
|
|
2337
|
+
function printReadyHint() {
|
|
1650
2338
|
const restUrl = getBaseUrl();
|
|
1651
2339
|
const viewerUrl = getViewerUrl();
|
|
1652
2340
|
const engineHost = getEngineHost();
|
|
1653
2341
|
const streamUrl = `ws://${engineHost}:${getStreamPort()}`;
|
|
1654
2342
|
const engineUrl = `ws://${engineHost}:${getEnginePort()}`;
|
|
1655
|
-
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)`)}`;
|
|
1656
2347
|
const lines = [
|
|
1657
2348
|
`${c.label("REST API")} ${c.url(restUrl)}`,
|
|
1658
2349
|
`${c.label("Viewer")} ${c.url(viewerUrl)}`,
|
|
@@ -1661,10 +2352,11 @@ function printReadyHint(consoleState) {
|
|
|
1661
2352
|
consoleLine
|
|
1662
2353
|
];
|
|
1663
2354
|
p.note(lines.join("\n"), `agentmemory v${c.accent(VERSION)}`);
|
|
1664
|
-
const demoCommand =
|
|
2355
|
+
const demoCommand = `${invocation} demo${portFlags}`;
|
|
1665
2356
|
process.stdout.write(`\n${c.dim("Try:")} ${c.cmd(demoCommand)}\n`);
|
|
1666
2357
|
}
|
|
1667
2358
|
async function main() {
|
|
2359
|
+
await assertRuntimePortOwnership();
|
|
1668
2360
|
try {
|
|
1669
2361
|
if ((await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(1500) })).ok) {
|
|
1670
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\`.`);
|
|
@@ -1678,14 +2370,14 @@ async function main() {
|
|
|
1678
2370
|
if (firstRun || IS_RESET) await runOnboarding();
|
|
1679
2371
|
if (skipEngine) {
|
|
1680
2372
|
if (IS_VERBOSE) p.log.info("Skipping engine check (--no-engine)");
|
|
1681
|
-
await import("./src-
|
|
2373
|
+
await import("./src-Jsq9LCeH.mjs");
|
|
1682
2374
|
if (await waitForAgentmemoryReady(15e3)) {
|
|
1683
|
-
const consoleState = await ensureIiiConsole();
|
|
1684
2375
|
await maybeOfferGlobalInstall();
|
|
1685
|
-
printReadyHint(
|
|
2376
|
+
printReadyHint();
|
|
1686
2377
|
}
|
|
1687
2378
|
return;
|
|
1688
2379
|
}
|
|
2380
|
+
if (await reconcilePersistedDockerEngine()) return;
|
|
1689
2381
|
if (await isEngineRunning()) {
|
|
1690
2382
|
if (IS_VERBOSE) p.log.success("iii-engine is running");
|
|
1691
2383
|
const persisted = readEngineState();
|
|
@@ -1693,12 +2385,13 @@ async function main() {
|
|
|
1693
2385
|
const detected = attachedBin ? iiiBinVersion(attachedBin) : null;
|
|
1694
2386
|
if (detected === IIPINNED_VERSION) {
|
|
1695
2387
|
adoptRunningEngine();
|
|
1696
|
-
await
|
|
1697
|
-
if (await waitForAgentmemoryReady(15e3)) {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
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);
|
|
1701
2392
|
}
|
|
2393
|
+
await maybeOfferGlobalInstall();
|
|
2394
|
+
printReadyHint();
|
|
1702
2395
|
return;
|
|
1703
2396
|
}
|
|
1704
2397
|
const detectedLabel = detected ? `v${detected}` : "an unverified version";
|
|
@@ -1718,7 +2411,7 @@ async function main() {
|
|
|
1718
2411
|
` ${c.cmd(base)}`,
|
|
1719
2412
|
"",
|
|
1720
2413
|
c.dim(`Step 2 needs no manual install. To install iii v${IIPINNED_VERSION} yourself (replaces your global iii), curl:`),
|
|
1721
|
-
` ${c.cmd(
|
|
2414
|
+
` ${c.cmd(IIIENGINE_INSTALL_CMD)}`,
|
|
1722
2415
|
` ${c.dim("or download the release:")} ${c.url(`https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`)}`
|
|
1723
2416
|
].join("\n"), "engine conflict");
|
|
1724
2417
|
process.exit(1);
|
|
@@ -1735,6 +2428,7 @@ async function main() {
|
|
|
1735
2428
|
if (!await waitForEngine(15e3)) {
|
|
1736
2429
|
const port = getRestPort();
|
|
1737
2430
|
s.stop("iii-engine did not become ready within 15s");
|
|
2431
|
+
printDockerStartupLogs();
|
|
1738
2432
|
if (startupFailure?.kind === "engine-crashed" || startupFailure?.kind === "docker-crashed") {
|
|
1739
2433
|
p.log.error("The iii-engine process crashed on startup.");
|
|
1740
2434
|
if (startupFailure.binary) p.log.info(`Binary: ${startupFailure.binary}`);
|
|
@@ -1751,6 +2445,7 @@ async function main() {
|
|
|
1751
2445
|
].join("\n"), "Troubleshooting");
|
|
1752
2446
|
} else {
|
|
1753
2447
|
p.log.error("The engine process started but the REST API never responded.");
|
|
2448
|
+
printCapturedStartupStderr();
|
|
1754
2449
|
p.note([
|
|
1755
2450
|
`Check whether port ${port} is already bound by another process:`,
|
|
1756
2451
|
portInUseDiagnostic(port),
|
|
@@ -1764,19 +2459,18 @@ async function main() {
|
|
|
1764
2459
|
process.exit(1);
|
|
1765
2460
|
}
|
|
1766
2461
|
s.stop(c.ok("iii-engine is ready"));
|
|
1767
|
-
await
|
|
1768
|
-
if (await waitForAgentmemoryReady(15e3)) {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
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);
|
|
1772
2466
|
}
|
|
2467
|
+
await maybeOfferGlobalInstall();
|
|
2468
|
+
printReadyHint();
|
|
1773
2469
|
writePrefs({ skipSplash: true });
|
|
1774
2470
|
}
|
|
1775
2471
|
async function apiFetch(base, path, timeoutMs = 5e3) {
|
|
1776
2472
|
try {
|
|
1777
|
-
const headers =
|
|
1778
|
-
const secret = process.env["AGENTMEMORY_SECRET"];
|
|
1779
|
-
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
|
2473
|
+
const headers = bearerHeaders(base);
|
|
1780
2474
|
return await (await fetch(`${base}/agentmemory/${path}`, {
|
|
1781
2475
|
signal: AbortSignal.timeout(timeoutMs),
|
|
1782
2476
|
headers
|
|
@@ -2065,7 +2759,7 @@ async function passiveServerChecks() {
|
|
|
2065
2759
|
}, {
|
|
2066
2760
|
name: "Embedding provider",
|
|
2067
2761
|
ok: hasEmbed,
|
|
2068
|
-
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"
|
|
2069
2763
|
});
|
|
2070
2764
|
for (const f of flags?.flags || []) checks.push({
|
|
2071
2765
|
name: f.label,
|
|
@@ -2279,7 +2973,10 @@ async function postJson(url, body, timeoutMs = 5e3) {
|
|
|
2279
2973
|
try {
|
|
2280
2974
|
const res = await fetch(url, {
|
|
2281
2975
|
method: "POST",
|
|
2282
|
-
headers: {
|
|
2976
|
+
headers: {
|
|
2977
|
+
"Content-Type": "application/json",
|
|
2978
|
+
...bearerHeaders(url)
|
|
2979
|
+
},
|
|
2283
2980
|
body: JSON.stringify(body),
|
|
2284
2981
|
signal: AbortSignal.timeout(timeoutMs)
|
|
2285
2982
|
});
|
|
@@ -2292,7 +2989,10 @@ async function postJson(url, body, timeoutMs = 5e3) {
|
|
|
2292
2989
|
async function postJsonStrict(url, body, timeoutMs = 5e3) {
|
|
2293
2990
|
const res = await fetch(url, {
|
|
2294
2991
|
method: "POST",
|
|
2295
|
-
headers: {
|
|
2992
|
+
headers: {
|
|
2993
|
+
"Content-Type": "application/json",
|
|
2994
|
+
...bearerHeaders(url)
|
|
2995
|
+
},
|
|
2296
2996
|
body: JSON.stringify(body),
|
|
2297
2997
|
signal: AbortSignal.timeout(timeoutMs)
|
|
2298
2998
|
});
|
|
@@ -2327,7 +3027,10 @@ async function seedDemoSession(base, project, session) {
|
|
|
2327
3027
|
try {
|
|
2328
3028
|
const res = await fetch(url, {
|
|
2329
3029
|
method: "POST",
|
|
2330
|
-
headers: {
|
|
3030
|
+
headers: {
|
|
3031
|
+
"Content-Type": "application/json",
|
|
3032
|
+
...bearerHeaders(url)
|
|
3033
|
+
},
|
|
2331
3034
|
body: JSON.stringify(payload),
|
|
2332
3035
|
signal: AbortSignal.timeout(5e3)
|
|
2333
3036
|
});
|
|
@@ -2399,6 +3102,7 @@ async function runInit() {
|
|
|
2399
3102
|
p.outro(`Edit ${target} and you're set.`);
|
|
2400
3103
|
}
|
|
2401
3104
|
async function startServerForDemo() {
|
|
3105
|
+
await assertRuntimePortOwnership();
|
|
2402
3106
|
if (await isAgentmemoryReady()) return async () => {};
|
|
2403
3107
|
const startedEngine = !await isEngineRunning();
|
|
2404
3108
|
if (startedEngine) {
|
|
@@ -2408,11 +3112,13 @@ async function startServerForDemo() {
|
|
|
2408
3112
|
process.exit(1);
|
|
2409
3113
|
}
|
|
2410
3114
|
if (!await waitForEngine(15e3)) {
|
|
3115
|
+
printCapturedStartupStderr();
|
|
3116
|
+
printDockerStartupLogs();
|
|
2411
3117
|
p.log.error("iii-engine did not become ready within 15s.");
|
|
2412
3118
|
process.exit(1);
|
|
2413
3119
|
}
|
|
2414
3120
|
}
|
|
2415
|
-
await
|
|
3121
|
+
await startWorkerForEngineState();
|
|
2416
3122
|
if (!await waitForAgentmemoryReady(15e3)) {
|
|
2417
3123
|
p.log.error("agentmemory worker did not become ready within 15s.");
|
|
2418
3124
|
process.exit(1);
|
|
@@ -2422,7 +3128,7 @@ async function startServerForDemo() {
|
|
|
2422
3128
|
const port = getRestPort();
|
|
2423
3129
|
const state = readEngineState();
|
|
2424
3130
|
if (state?.kind === "docker") {
|
|
2425
|
-
await stopDockerEngine(state
|
|
3131
|
+
await stopDockerEngine(state, port).catch(() => {});
|
|
2426
3132
|
return;
|
|
2427
3133
|
}
|
|
2428
3134
|
const pids = new Set(findEnginePidsByPort(port));
|
|
@@ -2482,8 +3188,8 @@ async function runDemoBody(base) {
|
|
|
2482
3188
|
"",
|
|
2483
3189
|
...semanticHits > 0 ? [c.accent(`Notice: searching "database performance optimization"`), c.accent(`found the N+1 query fix — keyword matching can't do that.`)] : [
|
|
2484
3190
|
c.dim(`Note: "database performance optimization" found nothing —`),
|
|
2485
|
-
c.dim(`
|
|
2486
|
-
c.dim(`
|
|
3191
|
+
c.dim(`set EMBEDDING_PROVIDER=local for on-device semantic recall,`),
|
|
3192
|
+
c.dim(`or add an embedding API key in ~/.agentmemory/.env.`)
|
|
2487
3193
|
],
|
|
2488
3194
|
"",
|
|
2489
3195
|
`Viewer: ${c.url(getViewerUrl())}`,
|
|
@@ -2533,14 +3239,22 @@ async function runUpgrade() {
|
|
|
2533
3239
|
};
|
|
2534
3240
|
if (hasPackageJson) if (!!pnpmBin && hasPnpmLock && pnpmBin) {
|
|
2535
3241
|
requireSuccess(runCommand(pnpmBin, ["install"], { label: "Refreshing dependencies (pnpm install)" }), "pnpm install");
|
|
2536
|
-
runCommand(pnpmBin, [
|
|
2537
|
-
|
|
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}`,
|
|
2538
3248
|
optional: true
|
|
2539
3249
|
});
|
|
2540
3250
|
} else if (npmBin) {
|
|
2541
3251
|
requireSuccess(runCommand(npmBin, ["install"], { label: "Refreshing dependencies (npm install)" }), "npm install");
|
|
2542
|
-
runCommand(npmBin, [
|
|
2543
|
-
|
|
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}`,
|
|
2544
3258
|
optional: true
|
|
2545
3259
|
});
|
|
2546
3260
|
} else p.log.warn("No package manager found (pnpm/npm). Skipping JS dependency upgrade.");
|
|
@@ -2572,6 +3286,22 @@ async function runUpgrade() {
|
|
|
2572
3286
|
function pidAlive(pid) {
|
|
2573
3287
|
try {
|
|
2574
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 {}
|
|
2575
3305
|
return true;
|
|
2576
3306
|
} catch (err) {
|
|
2577
3307
|
return err?.code === "EPERM";
|
|
@@ -2606,6 +3336,23 @@ async function signalAndWait(pid, initialSignal, timeoutMs) {
|
|
|
2606
3336
|
await new Promise((r) => setTimeout(r, 200));
|
|
2607
3337
|
return !pidAlive(pid);
|
|
2608
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
|
+
}
|
|
2609
3356
|
async function stopWorkerPid(pid, graceMs) {
|
|
2610
3357
|
const s = p.spinner();
|
|
2611
3358
|
s.start(`Stopping agentmemory worker (pid ${pid})... [flushing state]`);
|
|
@@ -2613,6 +3360,46 @@ async function stopWorkerPid(pid, graceMs) {
|
|
|
2613
3360
|
s.stop(ok ? `Stopped worker pid ${pid}` : `Failed to stop worker pid ${pid}`);
|
|
2614
3361
|
return ok;
|
|
2615
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
|
+
}
|
|
2616
3403
|
function pidCommand(pid) {
|
|
2617
3404
|
if (IS_WINDOWS) return "";
|
|
2618
3405
|
try {
|
|
@@ -2662,65 +3449,80 @@ function findEnginePidsByPort(port) {
|
|
|
2662
3449
|
return [];
|
|
2663
3450
|
}
|
|
2664
3451
|
}
|
|
2665
|
-
async function stopDockerEngine(
|
|
3452
|
+
async function stopDockerEngine(state, port, suppressOutro = false) {
|
|
2666
3453
|
const dockerBin = whichBinary("docker");
|
|
2667
3454
|
if (!dockerBin) {
|
|
2668
|
-
p.log.error(
|
|
3455
|
+
p.log.error("Engine was started via Docker, but `docker` is no longer on PATH. Lifecycle state was preserved.");
|
|
2669
3456
|
process.exit(1);
|
|
2670
3457
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
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.`);
|
|
2673
3461
|
process.exit(1);
|
|
2674
3462
|
}
|
|
3463
|
+
const resolvedState = persistDockerInspection(state, inspection);
|
|
2675
3464
|
const workerPid = readWorkerPidfile();
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
3465
|
+
if (workerPid && !await stopWorkerPid(workerPid, 5e3)) {
|
|
3466
|
+
p.log.error("The agentmemory worker could not be stopped; Docker ownership state was preserved.");
|
|
3467
|
+
process.exit(1);
|
|
3468
|
+
}
|
|
3469
|
+
clearWorkerPidfile();
|
|
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.");
|
|
3488
|
+
process.exit(1);
|
|
3489
|
+
}
|
|
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);
|
|
2683
3494
|
}
|
|
2684
|
-
const
|
|
3495
|
+
const composeText = readFileSync(effectiveComposeFile, "utf-8");
|
|
3496
|
+
const ownServices = ["iii-engine", "iii-init"].filter((service) => new RegExp(`^\\s+${service}:`, "m").test(composeText));
|
|
2685
3497
|
if (ownServices.length === 0) {
|
|
2686
|
-
p.log.error(`${
|
|
3498
|
+
p.log.error(`${effectiveComposeFile} does not define iii-engine or iii-init. Refusing an unscoped stop.`);
|
|
2687
3499
|
process.exit(1);
|
|
2688
3500
|
}
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2691
|
-
"-f",
|
|
2692
|
-
composeFile,
|
|
3501
|
+
const composeCommand = ["docker", ...dockerComposeArgs(effectiveComposeFile, projectName, [])].join(" ");
|
|
3502
|
+
if (runCommand(dockerBin, dockerComposeArgs(effectiveComposeFile, projectName, [
|
|
2693
3503
|
"rm",
|
|
2694
3504
|
"-s",
|
|
2695
3505
|
"-f",
|
|
2696
3506
|
...ownServices
|
|
2697
|
-
], { label:
|
|
2698
|
-
if (workerStopped) clearWorkerPidfile();
|
|
2699
|
-
if (ok) {
|
|
3507
|
+
]), { label: `${composeCommand} rm -s -f ${ownServices.join(" ")}` })) {
|
|
2700
3508
|
clearEnginePidfile();
|
|
2701
3509
|
clearEngineState();
|
|
2702
3510
|
} else {
|
|
2703
|
-
p.log.error(`docker compose rm failed. The engine may still be running on :${port}. Inspect with:\n
|
|
3511
|
+
p.log.error(`docker compose rm failed. The engine may still be running on :${port}. Inspect with:\n ${composeCommand} ps`);
|
|
2704
3512
|
process.exit(1);
|
|
2705
3513
|
}
|
|
2706
|
-
p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
|
|
3514
|
+
if (!suppressOutro) p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
|
|
2707
3515
|
}
|
|
2708
3516
|
async function runStop() {
|
|
3517
|
+
await assertRuntimePortOwnership();
|
|
2709
3518
|
p.intro("agentmemory stop");
|
|
2710
3519
|
const port = getRestPort();
|
|
2711
3520
|
const state = readEngineState();
|
|
2712
3521
|
const running = await isEngineRunning();
|
|
2713
3522
|
const force = args.includes("--force");
|
|
2714
3523
|
if (state?.kind === "docker") {
|
|
2715
|
-
if (!running) {
|
|
2716
|
-
|
|
2717
|
-
clearEnginePidfile();
|
|
2718
|
-
clearEngineState();
|
|
2719
|
-
clearWorkerPidfile();
|
|
2720
|
-
p.outro("Nothing to stop.");
|
|
2721
|
-
return;
|
|
2722
|
-
}
|
|
2723
|
-
await stopDockerEngine(state.composeFile, port);
|
|
3524
|
+
if (!running) p.log.info(`No engine responding on port ${port}.`);
|
|
3525
|
+
await stopDockerEngine(state, port);
|
|
2724
3526
|
return;
|
|
2725
3527
|
}
|
|
2726
3528
|
const portPids = findEnginePidsByPort(port);
|
|
@@ -2737,7 +3539,7 @@ async function runStop() {
|
|
|
2737
3539
|
if (workerPid !== null && portPids.length === 0 && pidfilePid === null) {
|
|
2738
3540
|
const s = p.spinner();
|
|
2739
3541
|
s.start(`Stopping orphaned agentmemory worker (pid ${workerPid})...`);
|
|
2740
|
-
const ok = await signalAndWait(workerPid, "SIGTERM",
|
|
3542
|
+
const ok = await signalAndWait(workerPid, "SIGTERM", SHUTDOWN_HARD_EXIT_MS + 1e3);
|
|
2741
3543
|
s.stop(ok ? `Stopped worker pid ${workerPid}` : `Failed to stop worker pid ${workerPid}`);
|
|
2742
3544
|
clearEnginePidfile();
|
|
2743
3545
|
clearEngineState();
|
|
@@ -2774,7 +3576,8 @@ async function runStop() {
|
|
|
2774
3576
|
process.exit(1);
|
|
2775
3577
|
}
|
|
2776
3578
|
let allStopped = true;
|
|
2777
|
-
for (const pid of workerCandidates) if (!await stopWorkerPid(pid,
|
|
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();
|
|
2778
3581
|
const skippedForeign = [];
|
|
2779
3582
|
for (const pid of candidates) {
|
|
2780
3583
|
if (workerCandidates.has(pid)) continue;
|
|
@@ -2807,10 +3610,10 @@ async function runStop() {
|
|
|
2807
3610
|
p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
|
|
2808
3611
|
}
|
|
2809
3612
|
async function runMcp() {
|
|
2810
|
-
await import("./standalone-
|
|
3613
|
+
await import("./standalone-DKCdrf1d.mjs");
|
|
2811
3614
|
}
|
|
2812
3615
|
async function runConnectCmd() {
|
|
2813
|
-
const { runConnect } = await import("./connect-
|
|
3616
|
+
const { runConnect } = await import("./connect-B4_ca6BD.mjs").then((n) => n.n);
|
|
2814
3617
|
await runConnect(args.slice(1));
|
|
2815
3618
|
}
|
|
2816
3619
|
async function runImportJsonl() {
|
|
@@ -2867,11 +3670,12 @@ async function runImportJsonl() {
|
|
|
2867
3670
|
process.exit(1);
|
|
2868
3671
|
}
|
|
2869
3672
|
const body = {};
|
|
2870
|
-
if (pathArg) body["path"] = pathArg;
|
|
3673
|
+
if (pathArg) body["path"] = resolve(pathArg.startsWith("~") ? join(homedir(), pathArg.slice(1)) : pathArg);
|
|
2871
3674
|
if (maxFiles !== void 0) body["maxFiles"] = maxFiles;
|
|
2872
|
-
const headers = {
|
|
2873
|
-
|
|
2874
|
-
|
|
3675
|
+
const headers = {
|
|
3676
|
+
"content-type": "application/json",
|
|
3677
|
+
...bearerHeaders(base)
|
|
3678
|
+
};
|
|
2875
3679
|
p.log.info(`Importing JSONL from ${pathArg || "~/.claude/projects"}…`);
|
|
2876
3680
|
const spinner = p.spinner();
|
|
2877
3681
|
spinner.start("scanning files");
|
|
@@ -2958,20 +3762,46 @@ function safeDelete(path) {
|
|
|
2958
3762
|
}
|
|
2959
3763
|
}
|
|
2960
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();
|
|
2961
3771
|
p.intro("agentmemory remove");
|
|
2962
3772
|
const force = args.includes("--force");
|
|
2963
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
|
+
}
|
|
2964
3789
|
const home = homedir();
|
|
2965
3790
|
const connectManifest = loadConnectManifest(home);
|
|
3791
|
+
const localBinIiiVersion = probeLocalBinIiiVersion(home);
|
|
3792
|
+
const options = {
|
|
3793
|
+
force,
|
|
3794
|
+
keepData,
|
|
3795
|
+
preserveRuntimeState: ownedState?.kind === "docker"
|
|
3796
|
+
};
|
|
2966
3797
|
const plan = buildRemovePlan({
|
|
2967
3798
|
home,
|
|
3799
|
+
runtimeDir: dirname(engineStatePath()),
|
|
3800
|
+
dataDir: dataDirResolution.dataDir,
|
|
2968
3801
|
pinnedVersion: IIPINNED_VERSION,
|
|
2969
|
-
localBinIiiVersion
|
|
3802
|
+
localBinIiiVersion,
|
|
2970
3803
|
connectManifest
|
|
2971
|
-
},
|
|
2972
|
-
force,
|
|
2973
|
-
keepData
|
|
2974
|
-
});
|
|
3804
|
+
}, options);
|
|
2975
3805
|
if (plan.filter((it) => it.applicable).length === 0) {
|
|
2976
3806
|
p.outro("Nothing to remove. agentmemory is already gone.");
|
|
2977
3807
|
return;
|
|
@@ -3008,19 +3838,17 @@ async function runRemove() {
|
|
|
3008
3838
|
}
|
|
3009
3839
|
}
|
|
3010
3840
|
if (item.id === "stop-engine") {
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
p.log.success(cands.size > 0 ? `stopped engine (${cands.size} pid${cands.size === 1 ? "" : "s"})` : "no engine running");
|
|
3021
|
-
} catch (err) {
|
|
3022
|
-
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;
|
|
3845
|
+
}
|
|
3846
|
+
const stopped = await stopNativeEngineForRemoval();
|
|
3847
|
+
if (!stopped.ok) {
|
|
3848
|
+
p.log.error(`Removal aborted: ${stopped.reason}.`);
|
|
3849
|
+
process.exit(1);
|
|
3023
3850
|
}
|
|
3851
|
+
p.log.success(stopped.stoppedEnginePids > 0 ? `stopped engine (${stopped.stoppedEnginePids} pid${stopped.stoppedEnginePids === 1 ? "" : "s"})` : "no engine running");
|
|
3024
3852
|
continue;
|
|
3025
3853
|
}
|
|
3026
3854
|
if (!item.path) continue;
|
|
@@ -3030,17 +3858,75 @@ async function runRemove() {
|
|
|
3030
3858
|
}
|
|
3031
3859
|
p.outro("Done. agentmemory cleanly removed. The npm package itself: npm uninstall -g @agentmemory/agentmemory");
|
|
3032
3860
|
}
|
|
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
|
+
}
|
|
3033
3917
|
const commands = {
|
|
3034
3918
|
init: runInit,
|
|
3035
3919
|
connect: runConnectCmd,
|
|
3036
3920
|
status: runStatus,
|
|
3921
|
+
console: runConsole,
|
|
3037
3922
|
doctor: runDoctor,
|
|
3038
3923
|
demo: runDemo,
|
|
3039
3924
|
upgrade: runUpgrade,
|
|
3040
3925
|
stop: runStop,
|
|
3041
3926
|
remove: runRemove,
|
|
3042
3927
|
mcp: runMcp,
|
|
3043
|
-
"import-jsonl": runImportJsonl
|
|
3928
|
+
"import-jsonl": runImportJsonl,
|
|
3929
|
+
capture: runCapture
|
|
3044
3930
|
};
|
|
3045
3931
|
const first = args[0] ?? "";
|
|
3046
3932
|
async function unknownCommand() {
|