@agentmemory/agentmemory 0.9.13 → 0.9.15
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/README.md +23 -21
- package/dist/cli.mjs +1612 -113
- package/dist/cli.mjs.map +1 -1
- package/dist/connect-hRTF7E2c.mjs +525 -0
- package/dist/connect-hRTF7E2c.mjs.map +1 -0
- package/dist/{image-refs-HVu22rfu.mjs → image-refs-R3tin9MR.mjs} +2 -2
- package/dist/{image-refs-HVu22rfu.mjs.map → image-refs-R3tin9MR.mjs.map} +1 -1
- package/dist/{image-store-BfN1vDbj.mjs → image-store-DyrKZKqZ.mjs} +1 -1
- package/dist/index.mjs +63 -35
- package/dist/index.mjs.map +1 -1
- package/dist/{src-Ca9oX6Hq.mjs → src-BGcqJR1a.mjs} +62 -72
- package/dist/src-BGcqJR1a.mjs.map +1 -0
- package/dist/{standalone-BpbiNqr9.mjs → standalone-BQOaGF4z.mjs} +3 -3
- package/dist/{standalone-BpbiNqr9.mjs.map → standalone-BQOaGF4z.mjs.map} +1 -1
- package/dist/standalone.mjs +1 -1
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-D5l632PP.mjs → tools-registry-BF0pgZmI.mjs} +2 -6
- package/dist/tools-registry-BF0pgZmI.mjs.map +1 -0
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/src-Ca9oX6Hq.mjs.map +0 -1
- package/dist/tools-registry-D5l632PP.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, readdirSync, readlinkSync, statSync } from "node:fs";
|
|
3
|
+
import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
4
4
|
import { delimiter, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
9
10
|
|
|
10
11
|
//#region src/state/schema.ts
|
|
11
12
|
const KV = {
|
|
@@ -75,12 +76,741 @@ function jaccardSimilarity(a, b) {
|
|
|
75
76
|
return intersection / (setA.size + setB.size - intersection);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/cli/doctor-diagnostics.ts
|
|
81
|
+
/** Common placeholder values shipped in .env.example. */
|
|
82
|
+
const PLACEHOLDER_VALUES = new Set([
|
|
83
|
+
"",
|
|
84
|
+
"your-key-here",
|
|
85
|
+
"sk-ant-...",
|
|
86
|
+
"sk-...",
|
|
87
|
+
"changeme",
|
|
88
|
+
"todo",
|
|
89
|
+
"xxx"
|
|
90
|
+
]);
|
|
91
|
+
const PROVIDER_KEY_NAMES = [
|
|
92
|
+
"ANTHROPIC_API_KEY",
|
|
93
|
+
"OPENAI_API_KEY",
|
|
94
|
+
"GEMINI_API_KEY",
|
|
95
|
+
"GOOGLE_API_KEY",
|
|
96
|
+
"OPENROUTER_API_KEY",
|
|
97
|
+
"MINIMAX_API_KEY"
|
|
98
|
+
];
|
|
99
|
+
function parseEnvFile(content) {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
102
|
+
const line = rawLine.trim();
|
|
103
|
+
if (!line || line.startsWith("#")) continue;
|
|
104
|
+
const eq = line.indexOf("=");
|
|
105
|
+
if (eq < 0) continue;
|
|
106
|
+
const key = line.slice(0, eq).trim();
|
|
107
|
+
let value = line.slice(eq + 1).trim();
|
|
108
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
109
|
+
out[key] = value;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/** Returns the list of provider keys that look real (non-placeholder). */
|
|
114
|
+
function realProviderKeys(env) {
|
|
115
|
+
return PROVIDER_KEY_NAMES.filter((k) => {
|
|
116
|
+
const v = (env[k] ?? "").trim();
|
|
117
|
+
if (!v) return false;
|
|
118
|
+
if (PLACEHOLDER_VALUES.has(v.toLowerCase())) return false;
|
|
119
|
+
if (/^x+$/i.test(v.replace(/[-_]/g, ""))) return false;
|
|
120
|
+
return true;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Returns the list of provider key NAMES that exist but are placeholders. */
|
|
124
|
+
function placeholderProviderKeys(env) {
|
|
125
|
+
return PROVIDER_KEY_NAMES.filter((k) => {
|
|
126
|
+
const v = (env[k] ?? "").trim();
|
|
127
|
+
if (!v) return false;
|
|
128
|
+
if (PLACEHOLDER_VALUES.has(v.toLowerCase())) return true;
|
|
129
|
+
if (/^x+$/i.test(v.replace(/[-_]/g, ""))) return true;
|
|
130
|
+
return false;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function buildDiagnostics(effects) {
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
id: "env-missing",
|
|
137
|
+
message: "~/.agentmemory/.env is missing.",
|
|
138
|
+
fixPreview: "Copy .env.example into ~/.agentmemory/.env (your keys file).",
|
|
139
|
+
moreInfo: "agentmemory reads provider API keys (Anthropic, OpenAI, Gemini, …) from ~/.agentmemory/.env. Without this file the daemon falls back to BM25-only search and no LLM-backed enrichment runs.",
|
|
140
|
+
check: async () => ({
|
|
141
|
+
ok: effects.envFileExists(),
|
|
142
|
+
detail: effects.envFileExists() ? void 0 : "no env file"
|
|
143
|
+
}),
|
|
144
|
+
fix: () => effects.runInit()
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "no-llm-provider-key",
|
|
148
|
+
message: "No LLM provider API key found in ~/.agentmemory/.env.",
|
|
149
|
+
fixPreview: "Open ~/.agentmemory/.env in $EDITOR and paste your key, then re-check.",
|
|
150
|
+
moreInfo: "Set at least one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY. The daemon picks the first that resolves to a real (non-placeholder) value at startup.",
|
|
151
|
+
check: async () => {
|
|
152
|
+
if (!effects.envFileExists()) return {
|
|
153
|
+
ok: false,
|
|
154
|
+
detail: "env file missing (run env-missing fix first)"
|
|
155
|
+
};
|
|
156
|
+
const real = realProviderKeys(effects.readEnvFile());
|
|
157
|
+
return {
|
|
158
|
+
ok: real.length > 0,
|
|
159
|
+
detail: real.length > 0 ? `found: ${real.join(", ")}` : "no provider key set"
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
fix: (ctx) => effects.openEditor(ctx.envPath)
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "engine-version-mismatch",
|
|
166
|
+
message: "iii binary on PATH doesn't match the version agentmemory pins to.",
|
|
167
|
+
fixPreview: "Re-run the iii installer for the pinned version and restart the engine.",
|
|
168
|
+
moreInfo: "agentmemory pins the iii engine to a specific release because newer engines use a different worker model. Running a mismatched binary surfaces as EPIPE reconnect loops and empty search results.",
|
|
169
|
+
check: async (ctx) => {
|
|
170
|
+
const bin = effects.findIiiBinary();
|
|
171
|
+
if (!bin) return {
|
|
172
|
+
ok: false,
|
|
173
|
+
detail: "iii not on PATH"
|
|
174
|
+
};
|
|
175
|
+
const v = effects.iiiBinaryVersion(bin);
|
|
176
|
+
if (!v) return {
|
|
177
|
+
ok: false,
|
|
178
|
+
detail: "iii on PATH but --version failed"
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
ok: v === ctx.pinnedVersion,
|
|
182
|
+
detail: `${v} (pinned ${ctx.pinnedVersion})`
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
fix: async () => {
|
|
186
|
+
const r = await effects.runIiiInstaller();
|
|
187
|
+
if (!r.ok) return r;
|
|
188
|
+
await effects.runStop();
|
|
189
|
+
return effects.runStart();
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "viewer-unreachable",
|
|
194
|
+
message: "Viewer port not reachable.",
|
|
195
|
+
fixPreview: "Stop the engine, restart it, and retry the viewer probe.",
|
|
196
|
+
moreInfo: "The viewer is served on REST port + 2 (default 3113). If it never came up the most common cause is port collision; a sibling PR ships auto-bump for this case. If that lands first this check just verifies; otherwise restart the engine to retry binding.",
|
|
197
|
+
check: async () => ({
|
|
198
|
+
ok: await effects.viewerReachable(),
|
|
199
|
+
detail: void 0
|
|
200
|
+
}),
|
|
201
|
+
fix: async () => {
|
|
202
|
+
const stopped = await effects.runStop();
|
|
203
|
+
if (!stopped.ok) return stopped;
|
|
204
|
+
return effects.runStart();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "stale-pidfile",
|
|
209
|
+
message: "Stale pidfile: pid recorded but the process is gone.",
|
|
210
|
+
fixPreview: "Clear ~/.agentmemory/iii.pid + engine-state.json, then restart.",
|
|
211
|
+
moreInfo: "When the engine crashes hard (kill -9, OOM, host reboot) the pidfile sticks around. agentmemory refuses to start a second engine on top of a stale pid, so this state must be cleared explicitly.",
|
|
212
|
+
check: async () => {
|
|
213
|
+
if (!effects.pidfileExists()) return {
|
|
214
|
+
ok: true,
|
|
215
|
+
detail: "no pidfile"
|
|
216
|
+
};
|
|
217
|
+
const alive = effects.pidfilePidIsAlive();
|
|
218
|
+
if (alive === null) return {
|
|
219
|
+
ok: true,
|
|
220
|
+
detail: "pidfile unreadable"
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
ok: alive,
|
|
224
|
+
detail: alive ? "pid is alive" : "pid is gone"
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
fix: async () => {
|
|
228
|
+
effects.clearEnginePidAndState();
|
|
229
|
+
return effects.runStart();
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "env-placeholder-keys",
|
|
234
|
+
message: "~/.agentmemory/.env contains placeholder/empty API keys.",
|
|
235
|
+
fixPreview: "Open ~/.agentmemory/.env in $EDITOR to paste real values.",
|
|
236
|
+
moreInfo: "Lines like ANTHROPIC_API_KEY=sk-ant-... or =your-key-here are treated as absent. The daemon will fall back to BM25-only search. Replace placeholders with real keys or comment the line out.",
|
|
237
|
+
check: async () => {
|
|
238
|
+
if (!effects.envFileExists()) return {
|
|
239
|
+
ok: true,
|
|
240
|
+
detail: "env file missing (handled by env-missing)"
|
|
241
|
+
};
|
|
242
|
+
const placeholders = placeholderProviderKeys(effects.readEnvFile());
|
|
243
|
+
return {
|
|
244
|
+
ok: placeholders.length === 0,
|
|
245
|
+
detail: placeholders.length === 0 ? void 0 : `placeholder: ${placeholders.join(", ")}`
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
fix: (ctx) => effects.openEditor(ctx.envPath)
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "iii-on-path-not-local-bin",
|
|
252
|
+
message: "iii is on PATH but not in ~/.local/bin/iii (where we install).",
|
|
253
|
+
fixPreview: "Suggest re-installing the pinned version via the installer — won't touch your PATH.",
|
|
254
|
+
moreInfo: "agentmemory's installer writes to ~/.local/bin/iii. When a user-managed iii lives somewhere else (homebrew, cargo, $XDG_BIN) we don't auto-overwrite it. If you want our pinned build, run the installer; otherwise this is informational.",
|
|
255
|
+
manualOnly: true,
|
|
256
|
+
check: async () => {
|
|
257
|
+
const bin = effects.findIiiBinary();
|
|
258
|
+
if (!bin) return {
|
|
259
|
+
ok: true,
|
|
260
|
+
detail: "iii not on PATH (handled elsewhere)"
|
|
261
|
+
};
|
|
262
|
+
const localBin = effects.localBinIiiPath();
|
|
263
|
+
return {
|
|
264
|
+
ok: bin === localBin,
|
|
265
|
+
detail: bin === localBin ? void 0 : `iii at: ${bin}`
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
fix: async () => effects.runIiiInstaller().then((r) => ({
|
|
269
|
+
ok: r.ok,
|
|
270
|
+
message: r.message ?? "Installer wrote to ~/.local/bin/iii. Your PATH wasn't modified — adjust it yourself if needed."
|
|
271
|
+
}))
|
|
272
|
+
}
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Dry-run output: each failing check's fix preview, prefixed by the diagnostic
|
|
277
|
+
* message. Pure function so we can snapshot-test the format.
|
|
278
|
+
*/
|
|
279
|
+
function dryRunPlan(ctx, results) {
|
|
280
|
+
const lines = [];
|
|
281
|
+
let n = 0;
|
|
282
|
+
for (const { diagnostic, status } of results) {
|
|
283
|
+
if (status.ok) continue;
|
|
284
|
+
n++;
|
|
285
|
+
lines.push(`${n}. [${diagnostic.id}] ${diagnostic.message}`);
|
|
286
|
+
lines.push(` would fix: ${diagnostic.fixPreview}`);
|
|
287
|
+
if (status.detail) lines.push(` detail: ${status.detail}`);
|
|
288
|
+
}
|
|
289
|
+
if (lines.length === 0) lines.push(`All checks passing for ${ctx.baseUrl} — no fixes to run.`);
|
|
290
|
+
return lines;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/cli/remove-plan.ts
|
|
295
|
+
function pidfilePath(home) {
|
|
296
|
+
return join(home, ".agentmemory", "iii.pid");
|
|
297
|
+
}
|
|
298
|
+
function enginePath(home) {
|
|
299
|
+
return join(home, ".agentmemory", "engine-state.json");
|
|
300
|
+
}
|
|
301
|
+
function envPath(home) {
|
|
302
|
+
return join(home, ".agentmemory", ".env");
|
|
303
|
+
}
|
|
304
|
+
function preferencesPath(home) {
|
|
305
|
+
return join(home, ".agentmemory", "preferences.json");
|
|
306
|
+
}
|
|
307
|
+
function backupsDir(home) {
|
|
308
|
+
return join(home, ".agentmemory", "backups");
|
|
309
|
+
}
|
|
310
|
+
function dataDir(home) {
|
|
311
|
+
return join(home, ".agentmemory", "data");
|
|
312
|
+
}
|
|
313
|
+
function localBinIii(home) {
|
|
314
|
+
return join(home, ".local", "bin", "iii");
|
|
315
|
+
}
|
|
316
|
+
function safeSize(path) {
|
|
317
|
+
try {
|
|
318
|
+
return statSync(path).size;
|
|
319
|
+
} catch {
|
|
320
|
+
return -1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function pathExists(path) {
|
|
324
|
+
try {
|
|
325
|
+
return existsSync(path);
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Build the destruction plan for `agentmemory remove`.
|
|
332
|
+
*
|
|
333
|
+
* Plan items are returned regardless of whether `applicable` is true — the
|
|
334
|
+
* caller can decide whether to skip-and-log or hide entirely. This keeps
|
|
335
|
+
* the structure stable for tests.
|
|
336
|
+
*/
|
|
337
|
+
function buildRemovePlan(ctx, options) {
|
|
338
|
+
const { home, pinnedVersion, localBinIiiVersion, connectManifest } = ctx;
|
|
339
|
+
const plan = [];
|
|
340
|
+
plan.push({
|
|
341
|
+
id: "stop-engine",
|
|
342
|
+
description: "Stop running iii-engine (if any) cleanly",
|
|
343
|
+
path: null,
|
|
344
|
+
alwaysAsk: false,
|
|
345
|
+
applicable: pathExists(pidfilePath(home)) || pathExists(enginePath(home)),
|
|
346
|
+
sizeBytes: -1
|
|
347
|
+
});
|
|
348
|
+
plan.push({
|
|
349
|
+
id: "pidfile",
|
|
350
|
+
description: "Delete pidfile",
|
|
351
|
+
path: pidfilePath(home),
|
|
352
|
+
alwaysAsk: false,
|
|
353
|
+
applicable: pathExists(pidfilePath(home)),
|
|
354
|
+
sizeBytes: safeSize(pidfilePath(home))
|
|
355
|
+
});
|
|
356
|
+
plan.push({
|
|
357
|
+
id: "engine-state",
|
|
358
|
+
description: "Delete engine-state.json",
|
|
359
|
+
path: enginePath(home),
|
|
360
|
+
alwaysAsk: false,
|
|
361
|
+
applicable: pathExists(enginePath(home)),
|
|
362
|
+
sizeBytes: safeSize(enginePath(home))
|
|
363
|
+
});
|
|
364
|
+
plan.push({
|
|
365
|
+
id: "env",
|
|
366
|
+
description: "Delete .env (your API keys) — will ask separately",
|
|
367
|
+
path: envPath(home),
|
|
368
|
+
alwaysAsk: true,
|
|
369
|
+
applicable: !options.keepData && pathExists(envPath(home)),
|
|
370
|
+
sizeBytes: safeSize(envPath(home))
|
|
371
|
+
});
|
|
372
|
+
plan.push({
|
|
373
|
+
id: "preferences",
|
|
374
|
+
description: "Delete preferences.json",
|
|
375
|
+
path: preferencesPath(home),
|
|
376
|
+
alwaysAsk: false,
|
|
377
|
+
applicable: !options.keepData && pathExists(preferencesPath(home)),
|
|
378
|
+
sizeBytes: safeSize(preferencesPath(home))
|
|
379
|
+
});
|
|
380
|
+
plan.push({
|
|
381
|
+
id: "backups",
|
|
382
|
+
description: "Delete backups/ directory (connect manifest + backups)",
|
|
383
|
+
path: backupsDir(home),
|
|
384
|
+
alwaysAsk: false,
|
|
385
|
+
applicable: !options.keepData && pathExists(backupsDir(home)),
|
|
386
|
+
sizeBytes: -1
|
|
387
|
+
});
|
|
388
|
+
if (connectManifest?.installed?.length) for (const entry of connectManifest.installed) plan.push({
|
|
389
|
+
id: `connect:${entry.target}`,
|
|
390
|
+
description: `Remove agent connection (${entry.agent ?? "unknown"})`,
|
|
391
|
+
path: entry.target,
|
|
392
|
+
alwaysAsk: false,
|
|
393
|
+
applicable: pathExists(entry.target),
|
|
394
|
+
sizeBytes: safeSize(entry.target)
|
|
395
|
+
});
|
|
396
|
+
const localIii = localBinIii(home);
|
|
397
|
+
if (pathExists(localIii)) {
|
|
398
|
+
const matches = localBinIiiVersion === pinnedVersion;
|
|
399
|
+
plan.push({
|
|
400
|
+
id: "local-bin-iii",
|
|
401
|
+
description: matches ? `Delete ~/.local/bin/iii (matches pinned v${pinnedVersion})` : `Delete ~/.local/bin/iii (version ${localBinIiiVersion ?? "unknown"} != pinned v${pinnedVersion}) — will ask`,
|
|
402
|
+
path: localIii,
|
|
403
|
+
alwaysAsk: !matches,
|
|
404
|
+
applicable: true,
|
|
405
|
+
sizeBytes: safeSize(localIii)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
plan.push({
|
|
409
|
+
id: "data-dir",
|
|
410
|
+
description: "Delete memory data directory (~/.agentmemory/data/) — will ask separately",
|
|
411
|
+
path: dataDir(home),
|
|
412
|
+
alwaysAsk: true,
|
|
413
|
+
applicable: !options.keepData && pathExists(dataDir(home)),
|
|
414
|
+
sizeBytes: -1
|
|
415
|
+
});
|
|
416
|
+
return plan;
|
|
417
|
+
}
|
|
418
|
+
/** Format a plan for the user — one line per item. */
|
|
419
|
+
function formatPlan(plan) {
|
|
420
|
+
return plan.filter((p) => p.applicable).map((p, i) => {
|
|
421
|
+
const tag = p.alwaysAsk ? " [asks]" : "";
|
|
422
|
+
const sz = p.sizeBytes > 0 ? ` (${humanBytes(p.sizeBytes)})` : "";
|
|
423
|
+
return ` ${i + 1}. ${p.description}${tag}${sz}${p.path ? `\n ${p.path}` : ""}`;
|
|
424
|
+
}).join("\n");
|
|
425
|
+
}
|
|
426
|
+
function humanBytes(n) {
|
|
427
|
+
if (n < 1024) return `${n} B`;
|
|
428
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
429
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/cli/splash.ts
|
|
434
|
+
const IS_COLOR_TTY = !!process.stdout.isTTY && !process.env["NO_COLOR"];
|
|
435
|
+
function accent(s) {
|
|
436
|
+
return IS_COLOR_TTY ? `\x1b[38;5;208m${s}\x1b[0m` : s;
|
|
437
|
+
}
|
|
438
|
+
function dim(s) {
|
|
439
|
+
return IS_COLOR_TTY ? `\x1b[2m${s}\x1b[22m` : s;
|
|
440
|
+
}
|
|
441
|
+
function bold(s) {
|
|
442
|
+
return IS_COLOR_TTY ? `\x1b[1m${s}\x1b[22m` : s;
|
|
443
|
+
}
|
|
444
|
+
function getTerminalWidth() {
|
|
445
|
+
const w = process.stdout.columns;
|
|
446
|
+
return typeof w === "number" && w > 0 ? w : 80;
|
|
447
|
+
}
|
|
448
|
+
const TAGLINE = "Persistent memory for AI coding agents";
|
|
449
|
+
function fullBanner(version) {
|
|
450
|
+
const lines = ["", ...[
|
|
451
|
+
" _ ",
|
|
452
|
+
" __ _ __ _ ___ _ _ | |_ _ __ ___ _ __ ___ ___ _ __ _ _ ",
|
|
453
|
+
" / _` |/ _` |/ _ \\ '_\\| __| ' \\/ -_) ' \\ _ \\ / _ \\| '__| | | | ",
|
|
454
|
+
"| (_| | (_| | __/ | || |_| | | \\___| | | | | | (_) | | | |_| | ",
|
|
455
|
+
" \\__,_|\\__, |\\___|_| \\__|_| |_| |_| |_| |_|\\___/|_| \\__, | ",
|
|
456
|
+
" |___/ |___/ "
|
|
457
|
+
].map((line) => " " + accent(line))];
|
|
458
|
+
lines.push("");
|
|
459
|
+
lines.push(" " + bold(TAGLINE) + " " + dim(`v${version}`));
|
|
460
|
+
lines.push("");
|
|
461
|
+
return lines.join("\n");
|
|
462
|
+
}
|
|
463
|
+
function compactBanner(version) {
|
|
464
|
+
return [
|
|
465
|
+
"",
|
|
466
|
+
" " + bold(accent("agentmemory")),
|
|
467
|
+
" " + dim(`v${version} · ${TAGLINE}`),
|
|
468
|
+
""
|
|
469
|
+
].join("\n");
|
|
470
|
+
}
|
|
471
|
+
function minimalBanner(version) {
|
|
472
|
+
return `${accent("agentmemory")} ${dim(`v${version}`)}`;
|
|
473
|
+
}
|
|
474
|
+
function renderSplash(version) {
|
|
475
|
+
const width = getTerminalWidth();
|
|
476
|
+
let out;
|
|
477
|
+
if (width >= 120) out = fullBanner(version);
|
|
478
|
+
else if (width >= 80) out = compactBanner(version);
|
|
479
|
+
else out = minimalBanner(version);
|
|
480
|
+
process.stdout.write(out + "\n");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/cli/preferences.ts
|
|
485
|
+
const DEFAULTS = {
|
|
486
|
+
schemaVersion: 1,
|
|
487
|
+
lastAgent: null,
|
|
488
|
+
lastAgents: [],
|
|
489
|
+
lastProvider: null,
|
|
490
|
+
skipSplash: false,
|
|
491
|
+
skipNpxHint: false,
|
|
492
|
+
firstRunAt: null
|
|
493
|
+
};
|
|
494
|
+
function prefsDir() {
|
|
495
|
+
return join(homedir(), ".agentmemory");
|
|
496
|
+
}
|
|
497
|
+
function prefsPath() {
|
|
498
|
+
return join(prefsDir(), "preferences.json");
|
|
499
|
+
}
|
|
500
|
+
function readPrefs() {
|
|
501
|
+
try {
|
|
502
|
+
if (!existsSync(prefsPath())) return { ...DEFAULTS };
|
|
503
|
+
const raw = readFileSync(prefsPath(), "utf-8");
|
|
504
|
+
const parsed = JSON.parse(raw);
|
|
505
|
+
return {
|
|
506
|
+
...DEFAULTS,
|
|
507
|
+
...parsed,
|
|
508
|
+
schemaVersion: 1
|
|
509
|
+
};
|
|
510
|
+
} catch {
|
|
511
|
+
return { ...DEFAULTS };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function writePrefs(p) {
|
|
515
|
+
try {
|
|
516
|
+
mkdirSync(prefsDir(), { recursive: true });
|
|
517
|
+
const next = {
|
|
518
|
+
...readPrefs(),
|
|
519
|
+
...p,
|
|
520
|
+
schemaVersion: 1
|
|
521
|
+
};
|
|
522
|
+
const target = prefsPath();
|
|
523
|
+
const tmp = target + ".tmp";
|
|
524
|
+
const fd = openSync(tmp, "w", 384);
|
|
525
|
+
try {
|
|
526
|
+
writeSync(fd, JSON.stringify(next, null, 2) + "\n");
|
|
527
|
+
try {
|
|
528
|
+
fsyncSync(fd);
|
|
529
|
+
} catch {}
|
|
530
|
+
} finally {
|
|
531
|
+
closeSync(fd);
|
|
532
|
+
}
|
|
533
|
+
renameSync(tmp, target);
|
|
534
|
+
} catch {}
|
|
535
|
+
}
|
|
536
|
+
function resetPrefs() {
|
|
537
|
+
try {
|
|
538
|
+
unlinkSync(prefsPath());
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
function isFirstRun() {
|
|
542
|
+
if (!existsSync(prefsPath())) return true;
|
|
543
|
+
return readPrefs().firstRunAt === null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/cli/onboarding.ts
|
|
548
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
549
|
+
const NATIVE_AGENTS = [
|
|
550
|
+
{
|
|
551
|
+
value: "claude-code",
|
|
552
|
+
label: "Claude Code",
|
|
553
|
+
glyph: "⟁"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
value: "codex",
|
|
557
|
+
label: "Codex",
|
|
558
|
+
glyph: "◎"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
value: "openhuman",
|
|
562
|
+
label: "OpenHuman",
|
|
563
|
+
glyph: "◇"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
value: "openclaw",
|
|
567
|
+
label: "OpenClaw",
|
|
568
|
+
glyph: "◇"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
value: "hermes",
|
|
572
|
+
label: "Hermes",
|
|
573
|
+
glyph: "◇"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
value: "pi",
|
|
577
|
+
label: "Pi",
|
|
578
|
+
glyph: "◇"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
value: "cursor",
|
|
582
|
+
label: "Cursor",
|
|
583
|
+
glyph: "◫"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
value: "gemini-cli",
|
|
587
|
+
label: "Gemini CLI",
|
|
588
|
+
glyph: "✦"
|
|
589
|
+
}
|
|
590
|
+
];
|
|
591
|
+
const MCP_AGENTS = [
|
|
592
|
+
{
|
|
593
|
+
value: "opencode",
|
|
594
|
+
label: "OpenCode",
|
|
595
|
+
glyph: "⬡"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
value: "cline",
|
|
599
|
+
label: "Cline",
|
|
600
|
+
glyph: "◇"
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
value: "goose",
|
|
604
|
+
label: "Goose",
|
|
605
|
+
glyph: "◇"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
value: "kilo",
|
|
609
|
+
label: "Kilo",
|
|
610
|
+
glyph: "◇"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
value: "aider",
|
|
614
|
+
label: "Aider",
|
|
615
|
+
glyph: "◇"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
value: "claude-desktop",
|
|
619
|
+
label: "Claude Desktop",
|
|
620
|
+
glyph: "⟁"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
value: "windsurf",
|
|
624
|
+
label: "Windsurf",
|
|
625
|
+
glyph: "◇"
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
value: "roo",
|
|
629
|
+
label: "Roo",
|
|
630
|
+
glyph: "◇"
|
|
631
|
+
}
|
|
632
|
+
];
|
|
633
|
+
const PROVIDERS = [
|
|
634
|
+
{
|
|
635
|
+
value: "anthropic",
|
|
636
|
+
label: "Anthropic — claude",
|
|
637
|
+
envKey: "ANTHROPIC_API_KEY"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
value: "openai",
|
|
641
|
+
label: "OpenAI — gpt",
|
|
642
|
+
envKey: "OPENAI_API_KEY"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
value: "gemini",
|
|
646
|
+
label: "Google — gemini",
|
|
647
|
+
envKey: "GEMINI_API_KEY"
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
value: "openrouter",
|
|
651
|
+
label: "OpenRouter — multi-model",
|
|
652
|
+
envKey: "OPENROUTER_API_KEY"
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
value: "minimax",
|
|
656
|
+
label: "MiniMax — minimax-m1",
|
|
657
|
+
envKey: "MINIMAX_API_KEY"
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
value: "skip",
|
|
661
|
+
label: "Skip — BM25-only mode (no LLM key)",
|
|
662
|
+
envKey: null
|
|
663
|
+
}
|
|
664
|
+
];
|
|
665
|
+
function buildAgentOptions() {
|
|
666
|
+
return [...NATIVE_AGENTS.map((a) => ({
|
|
667
|
+
value: a.value,
|
|
668
|
+
label: `${a.glyph} ${a.label}`,
|
|
669
|
+
hint: "native plugin"
|
|
670
|
+
})), ...MCP_AGENTS.map((a) => ({
|
|
671
|
+
value: a.value,
|
|
672
|
+
label: `${a.glyph} ${a.label}`,
|
|
673
|
+
hint: "MCP server"
|
|
674
|
+
}))];
|
|
675
|
+
}
|
|
676
|
+
function findEnvExample$1() {
|
|
677
|
+
const candidates = [
|
|
678
|
+
join(__dirname$1, "..", "..", ".env.example"),
|
|
679
|
+
join(__dirname$1, "..", ".env.example"),
|
|
680
|
+
join(__dirname$1, ".env.example"),
|
|
681
|
+
join(process.cwd(), ".env.example")
|
|
682
|
+
];
|
|
683
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
async function seedEnvFile(provider) {
|
|
687
|
+
const target = join(homedir(), ".agentmemory", ".env");
|
|
688
|
+
await mkdir(dirname(target), { recursive: true });
|
|
689
|
+
const template = findEnvExample$1();
|
|
690
|
+
if (template && !existsSync(target)) try {
|
|
691
|
+
await copyFile(template, target, constants.COPYFILE_EXCL);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
if (err?.code !== "EEXIST") return null;
|
|
694
|
+
}
|
|
695
|
+
else if (!template && !existsSync(target)) {
|
|
696
|
+
const lines = [
|
|
697
|
+
"# agentmemory environment — uncomment what you need",
|
|
698
|
+
"# AGENTMEMORY_URL=http://localhost:3111",
|
|
699
|
+
""
|
|
700
|
+
];
|
|
701
|
+
const envKey = PROVIDERS.find((x) => x.value === provider)?.envKey;
|
|
702
|
+
if (envKey) lines.push(`# ${envKey}=`);
|
|
703
|
+
writeFileSync(target, lines.join("\n"), { mode: 384 });
|
|
704
|
+
}
|
|
705
|
+
return target;
|
|
706
|
+
}
|
|
707
|
+
async function runOnboarding() {
|
|
708
|
+
p.note([
|
|
709
|
+
"Welcome to agentmemory.",
|
|
710
|
+
"",
|
|
711
|
+
"Persistent memory for your AI coding agents. We'll pick which",
|
|
712
|
+
"agents to wire up and which provider (if any) handles compression",
|
|
713
|
+
"and consolidation. Either step can be changed later in ~/.agentmemory/.env."
|
|
714
|
+
].join("\n"), "first-run setup");
|
|
715
|
+
const agentsPicked = await p.multiselect({
|
|
716
|
+
message: "Which agents will use agentmemory? (space to toggle, enter to confirm)",
|
|
717
|
+
options: buildAgentOptions(),
|
|
718
|
+
required: false,
|
|
719
|
+
initialValues: ["claude-code"]
|
|
720
|
+
});
|
|
721
|
+
if (p.isCancel(agentsPicked)) {
|
|
722
|
+
p.cancel("Setup cancelled. Re-run any time with: agentmemory --reset");
|
|
723
|
+
process.exit(0);
|
|
724
|
+
}
|
|
725
|
+
const providerPicked = await p.select({
|
|
726
|
+
message: "Which LLM provider should agentmemory use for compress/consolidate?",
|
|
727
|
+
options: PROVIDERS.map(({ value, label }) => ({
|
|
728
|
+
value,
|
|
729
|
+
label
|
|
730
|
+
})),
|
|
731
|
+
initialValue: "anthropic"
|
|
732
|
+
});
|
|
733
|
+
if (p.isCancel(providerPicked)) {
|
|
734
|
+
p.cancel("Setup cancelled. Re-run any time with: agentmemory --reset");
|
|
735
|
+
process.exit(0);
|
|
736
|
+
}
|
|
737
|
+
const provider = providerPicked === "skip" ? null : providerPicked;
|
|
738
|
+
const agents = agentsPicked ?? [];
|
|
739
|
+
const envPath = await seedEnvFile(provider);
|
|
740
|
+
writePrefs({
|
|
741
|
+
lastAgent: agents[0] ?? null,
|
|
742
|
+
lastAgents: agents,
|
|
743
|
+
lastProvider: provider,
|
|
744
|
+
skipSplash: true,
|
|
745
|
+
firstRunAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
746
|
+
});
|
|
747
|
+
const lines = [`✓ Saved preferences to ${join(homedir(), ".agentmemory", "preferences.json")}`];
|
|
748
|
+
if (envPath) lines.push(`✓ Wrote ${envPath} (edit to add your API key)`);
|
|
749
|
+
else lines.push(`! Could not write ~/.agentmemory/.env — run \`agentmemory init\` after this completes.`);
|
|
750
|
+
if (provider) {
|
|
751
|
+
const envKey = PROVIDERS.find((x) => x.value === provider)?.envKey;
|
|
752
|
+
if (envKey) lines.push(` Uncomment ${envKey}= in that file to enable ${provider}.`);
|
|
753
|
+
} else lines.push(" No provider chosen — agentmemory will run in BM25-only mode.");
|
|
754
|
+
p.note(lines.join("\n"), "ready");
|
|
755
|
+
return {
|
|
756
|
+
agents,
|
|
757
|
+
provider
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
//#endregion
|
|
762
|
+
//#region src/logger.ts
|
|
763
|
+
function fmt(level, msg, fields) {
|
|
764
|
+
if (!fields || Object.keys(fields).length === 0) return `[agentmemory] ${level} ${msg}`;
|
|
765
|
+
try {
|
|
766
|
+
return `[agentmemory] ${level} ${msg} ${JSON.stringify(fields)}`;
|
|
767
|
+
} catch {
|
|
768
|
+
return `[agentmemory] ${level} ${msg}`;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function emit(level, msg, fields) {
|
|
772
|
+
try {
|
|
773
|
+
process.stderr.write(fmt(level, msg, fields) + "\n");
|
|
774
|
+
} catch {}
|
|
775
|
+
}
|
|
776
|
+
const logger = {
|
|
777
|
+
info(msg, fields) {
|
|
778
|
+
emit("info", msg, fields);
|
|
779
|
+
},
|
|
780
|
+
warn(msg, fields) {
|
|
781
|
+
emit("warn", msg, fields);
|
|
782
|
+
},
|
|
783
|
+
error(msg, fields) {
|
|
784
|
+
emit("error", msg, fields);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
let bootVerbose = process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
|
|
788
|
+
const bootBuffer = [];
|
|
789
|
+
function setBootVerbose(enabled) {
|
|
790
|
+
bootVerbose = enabled;
|
|
791
|
+
}
|
|
792
|
+
function bootLog(msg) {
|
|
793
|
+
if (bootVerbose) {
|
|
794
|
+
try {
|
|
795
|
+
process.stderr.write(`[agentmemory] ${msg}\n`);
|
|
796
|
+
} catch {}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (bootBuffer.length < 500) bootBuffer.push(msg);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/version.ts
|
|
804
|
+
const VERSION = "0.9.15";
|
|
805
|
+
|
|
78
806
|
//#endregion
|
|
79
807
|
//#region src/cli.ts
|
|
80
808
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
81
809
|
const args = process.argv.slice(2);
|
|
82
810
|
const IS_WINDOWS = platform() === "win32";
|
|
83
|
-
const IS_VERBOSE = args.includes("--verbose") || args.includes("-v");
|
|
811
|
+
const IS_VERBOSE = args.includes("--verbose") || args.includes("-v") || process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
|
|
812
|
+
setBootVerbose(IS_VERBOSE);
|
|
813
|
+
const IS_RESET = args.includes("--reset");
|
|
84
814
|
const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || "0.11.2";
|
|
85
815
|
function iiiReleaseAsset() {
|
|
86
816
|
const p = platform();
|
|
@@ -111,10 +841,22 @@ Usage: agentmemory [command] [options]
|
|
|
111
841
|
Commands:
|
|
112
842
|
(default) Start agentmemory worker
|
|
113
843
|
init Copy bundled .env.example to ~/.agentmemory/.env if absent
|
|
844
|
+
connect [agent] Wire agentmemory into an installed agent (claude-code, codex,
|
|
845
|
+
cursor, gemini-cli, openclaw, hermes, pi, openhuman).
|
|
846
|
+
No arg = interactive picker. --all wires every detected agent.
|
|
847
|
+
--dry-run shows what would change. --force re-installs.
|
|
114
848
|
status Show connection status, memory count, flags, and health
|
|
115
|
-
doctor
|
|
849
|
+
doctor Interactive diagnostic + fixer. [F]ix · [S]kip · [?]more · [Q]uit
|
|
850
|
+
--all: apply every fix without prompting (CI)
|
|
851
|
+
--dry-run: show what each fix would do, don't execute
|
|
852
|
+
remove Cleanly uninstall agentmemory (pidfile, state, .env, binaries).
|
|
853
|
+
--force: skip confirmations · --keep-data: keep memory data
|
|
116
854
|
demo Seed sample sessions and show recall in action
|
|
117
855
|
upgrade Upgrade local deps + iii runtime (best effort)
|
|
856
|
+
stop [--force] Stop the running iii-engine started by this CLI.
|
|
857
|
+
--force bypasses the Docker-heuristic guard and signals
|
|
858
|
+
whatever pidfile+lsof report on the REST port (use when
|
|
859
|
+
the engine was started natively but state file is missing).
|
|
118
860
|
mcp Start standalone MCP server (no engine required)
|
|
119
861
|
import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
|
|
120
862
|
--max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
|
|
@@ -122,14 +864,18 @@ Commands:
|
|
|
122
864
|
|
|
123
865
|
Options:
|
|
124
866
|
--help, -h Show this help
|
|
125
|
-
--verbose, -v Show engine stderr and diagnostic info
|
|
867
|
+
--verbose, -v Show engine stderr, boot log, and diagnostic info
|
|
868
|
+
--reset Wipe ~/.agentmemory/preferences.json and re-run onboarding
|
|
126
869
|
--tools all|core Tool visibility (default: core = 7 tools)
|
|
127
870
|
--no-engine Skip auto-starting iii-engine
|
|
128
871
|
--port <N> Override REST port (default: 3111)
|
|
129
872
|
|
|
130
873
|
Environment:
|
|
131
|
-
AGENTMEMORY_URL
|
|
132
|
-
|
|
874
|
+
AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
|
|
875
|
+
Honored by status, doctor, and MCP shim commands.
|
|
876
|
+
AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
|
|
877
|
+
native iii-engine binary on first run.
|
|
878
|
+
AGENTMEMORY_III_VERSION Override pinned iii-engine version (default ${IIPINNED_VERSION}).
|
|
133
879
|
|
|
134
880
|
Quick start:
|
|
135
881
|
npx @agentmemory/agentmemory # start with local iii-engine or Docker
|
|
@@ -220,6 +966,179 @@ function fallbackIiiPaths() {
|
|
|
220
966
|
if (!home) return ["/usr/local/bin/iii"];
|
|
221
967
|
return [join(home, ".local", "bin", "iii"), "/usr/local/bin/iii"];
|
|
222
968
|
}
|
|
969
|
+
function iiiBinVersion(binPath) {
|
|
970
|
+
try {
|
|
971
|
+
const match = execFileSync(binPath, ["--version"], {
|
|
972
|
+
encoding: "utf-8",
|
|
973
|
+
stdio: [
|
|
974
|
+
"ignore",
|
|
975
|
+
"pipe",
|
|
976
|
+
"ignore"
|
|
977
|
+
],
|
|
978
|
+
timeout: 3e3
|
|
979
|
+
}).match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
|
|
980
|
+
return match ? match[1] : null;
|
|
981
|
+
} catch {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
let warnedVersionMismatch = false;
|
|
986
|
+
function warnIfEngineVersionMismatch(iiiBinPath) {
|
|
987
|
+
if (!iiiBinPath || warnedVersionMismatch) return;
|
|
988
|
+
const detected = iiiBinVersion(iiiBinPath);
|
|
989
|
+
if (!detected || detected === IIPINNED_VERSION) return;
|
|
990
|
+
warnedVersionMismatch = true;
|
|
991
|
+
const asset = iiiReleaseAsset();
|
|
992
|
+
const downloadHint = asset ? `curl -fsSL https://github.com/iii-hq/iii/releases/download/iii/v${IIPINNED_VERSION}/${asset} | tar -xz -C ~/.local/bin` : `download v${IIPINNED_VERSION} from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
|
|
993
|
+
p.log.warn(`iii-engine on PATH is v${detected} but agentmemory v0.9.14+ pins v${IIPINNED_VERSION}. Set AGENTMEMORY_III_VERSION=${detected} to silence, or downgrade with: \`${downloadHint}\``);
|
|
994
|
+
}
|
|
995
|
+
function enginePidfilePath() {
|
|
996
|
+
return join(homedir(), ".agentmemory", "iii.pid");
|
|
997
|
+
}
|
|
998
|
+
function engineStatePath() {
|
|
999
|
+
return join(homedir(), ".agentmemory", "engine-state.json");
|
|
1000
|
+
}
|
|
1001
|
+
function writeEnginePidfile(pid) {
|
|
1002
|
+
try {
|
|
1003
|
+
const pidPath = enginePidfilePath();
|
|
1004
|
+
mkdirSync(dirname(pidPath), { recursive: true });
|
|
1005
|
+
writeFileSync(pidPath, `${pid}\n`, { encoding: "utf-8" });
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
vlog(`writeEnginePidfile: ${err instanceof Error ? err.message : String(err)}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function readEnginePidfile() {
|
|
1011
|
+
try {
|
|
1012
|
+
const pidStr = readFileSync(enginePidfilePath(), "utf-8").trim();
|
|
1013
|
+
const pid = parseInt(pidStr, 10);
|
|
1014
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1015
|
+
} catch {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function clearEnginePidfile() {
|
|
1020
|
+
try {
|
|
1021
|
+
unlinkSync(enginePidfilePath());
|
|
1022
|
+
} catch {}
|
|
1023
|
+
}
|
|
1024
|
+
function writeEngineState(state) {
|
|
1025
|
+
try {
|
|
1026
|
+
const statePath = engineStatePath();
|
|
1027
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
1028
|
+
writeFileSync(statePath, `${JSON.stringify(state)}\n`, { encoding: "utf-8" });
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
vlog(`writeEngineState: ${err instanceof Error ? err.message : String(err)}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function readEngineState() {
|
|
1034
|
+
try {
|
|
1035
|
+
const raw = readFileSync(engineStatePath(), "utf-8");
|
|
1036
|
+
const parsed = JSON.parse(raw);
|
|
1037
|
+
if (parsed && (parsed.kind === "native" || parsed.kind === "docker")) return parsed;
|
|
1038
|
+
return null;
|
|
1039
|
+
} catch {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function clearEngineState() {
|
|
1044
|
+
try {
|
|
1045
|
+
unlinkSync(engineStatePath());
|
|
1046
|
+
} catch {}
|
|
1047
|
+
}
|
|
1048
|
+
function discoverComposeFile() {
|
|
1049
|
+
return [
|
|
1050
|
+
join(__dirname, "..", "docker-compose.yml"),
|
|
1051
|
+
join(__dirname, "docker-compose.yml"),
|
|
1052
|
+
join(process.cwd(), "docker-compose.yml")
|
|
1053
|
+
].find((c) => existsSync(c)) ?? null;
|
|
1054
|
+
}
|
|
1055
|
+
function isInvokedViaNpx() {
|
|
1056
|
+
if (process.env["npm_lifecycle_event"] === "npx") return true;
|
|
1057
|
+
if ((process.argv[1] ?? "").includes("_npx")) return true;
|
|
1058
|
+
const ua = process.env["npm_config_user_agent"] ?? "";
|
|
1059
|
+
if (ua.startsWith("npm/") || ua.includes(" npm/")) return true;
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
function shouldSkipNpxHint() {
|
|
1063
|
+
try {
|
|
1064
|
+
const prefsPath = join(homedir(), ".agentmemory", "preferences.json");
|
|
1065
|
+
if (!existsSync(prefsPath)) return false;
|
|
1066
|
+
const raw = readFileSync(prefsPath, "utf-8");
|
|
1067
|
+
return JSON.parse(raw)?.skipNpxHint === true;
|
|
1068
|
+
} catch {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function maybeEmitNpxHint() {
|
|
1073
|
+
if (!isInvokedViaNpx()) return;
|
|
1074
|
+
if (shouldSkipNpxHint()) return;
|
|
1075
|
+
p.log.info("Tip: install globally for the bare `agentmemory` command:\n npm install -g @agentmemory/agentmemory");
|
|
1076
|
+
}
|
|
1077
|
+
function adoptRunningEngine() {
|
|
1078
|
+
try {
|
|
1079
|
+
const existingState = readEngineState();
|
|
1080
|
+
const existingPid = readEnginePidfile();
|
|
1081
|
+
if (existingState && existingPid) return;
|
|
1082
|
+
const enginePid = findEnginePidsByPort(getRestPort())[0];
|
|
1083
|
+
if (enginePid && !existingPid) writeEnginePidfile(enginePid);
|
|
1084
|
+
if (!existingState) writeEngineState({
|
|
1085
|
+
kind: "native",
|
|
1086
|
+
configPath: findIiiConfig() || "",
|
|
1087
|
+
attached: true
|
|
1088
|
+
});
|
|
1089
|
+
if (enginePid && !existingPid) p.log.info(`Attached to existing iii-engine (pid ${enginePid})`);
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
vlog(`adoptRunningEngine: ${err instanceof Error ? err.message : String(err)}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function runIiiInstaller() {
|
|
1095
|
+
const releaseUrl = iiiReleaseUrl();
|
|
1096
|
+
const asset = iiiReleaseAsset();
|
|
1097
|
+
const isZipAsset = asset?.endsWith(".zip") === true;
|
|
1098
|
+
if (!releaseUrl) {
|
|
1099
|
+
p.log.warn(`iii-engine binary not available for ${platform()}/${process.arch}. Use Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
|
|
1100
|
+
return {
|
|
1101
|
+
ok: false,
|
|
1102
|
+
binPath: null
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
if (IS_WINDOWS || isZipAsset) {
|
|
1106
|
+
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}`);
|
|
1107
|
+
return {
|
|
1108
|
+
ok: false,
|
|
1109
|
+
binPath: null
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
const shBin = whichBinary("sh");
|
|
1113
|
+
const curlBin = whichBinary("curl");
|
|
1114
|
+
if (!shBin || !curlBin) {
|
|
1115
|
+
p.log.warn("curl or sh not found. Cannot auto-install iii-engine.");
|
|
1116
|
+
return {
|
|
1117
|
+
ok: false,
|
|
1118
|
+
binPath: null
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
const binDir = join(homedir(), ".local", "bin");
|
|
1122
|
+
const binPath = join(binDir, "iii");
|
|
1123
|
+
if (!runCommand(shBin, ["-c", [
|
|
1124
|
+
`mkdir -p "${binDir}"`,
|
|
1125
|
+
`curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
|
|
1126
|
+
`chmod +x "${binPath}"`
|
|
1127
|
+
].join(" && ")], {
|
|
1128
|
+
label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
|
|
1129
|
+
optional: true
|
|
1130
|
+
})) {
|
|
1131
|
+
p.log.warn(`iii-engine installer failed. Fallbacks: Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
binPath: null
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
ok: true,
|
|
1139
|
+
binPath
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
223
1142
|
let startupFailure = null;
|
|
224
1143
|
function spawnEngineBackground(bin, spawnArgs, label) {
|
|
225
1144
|
vlog(`spawn: ${bin} ${spawnArgs.join(" ")}`);
|
|
@@ -232,6 +1151,8 @@ function spawnEngineBackground(bin, spawnArgs, label) {
|
|
|
232
1151
|
],
|
|
233
1152
|
windowsHide: true
|
|
234
1153
|
});
|
|
1154
|
+
const isDocker = label.includes("Docker");
|
|
1155
|
+
if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
|
|
235
1156
|
const stderrChunks = [];
|
|
236
1157
|
let stderrBytes = 0;
|
|
237
1158
|
const MAX_STDERR_CAPTURE = 16 * 1024;
|
|
@@ -245,27 +1166,48 @@ function spawnEngineBackground(bin, spawnArgs, label) {
|
|
|
245
1166
|
if (code !== null && code !== 0 || code === null && signal !== null) {
|
|
246
1167
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
247
1168
|
startupFailure = {
|
|
248
|
-
kind:
|
|
1169
|
+
kind: isDocker ? "docker-crashed" : "engine-crashed",
|
|
249
1170
|
stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
|
|
250
1171
|
binary: bin
|
|
251
1172
|
};
|
|
252
1173
|
vlog(`engine exited early: code=${code} signal=${signal}`);
|
|
253
1174
|
if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
|
|
1175
|
+
if (!isDocker) clearEnginePidfile();
|
|
1176
|
+
clearEngineState();
|
|
254
1177
|
}
|
|
255
1178
|
});
|
|
256
1179
|
child.unref();
|
|
257
1180
|
return child;
|
|
258
1181
|
}
|
|
1182
|
+
function startIiiBin(iiiBin, configPath) {
|
|
1183
|
+
warnIfEngineVersionMismatch(iiiBin);
|
|
1184
|
+
const s = p.spinner();
|
|
1185
|
+
s.start(`Starting iii-engine: ${iiiBin}`);
|
|
1186
|
+
writeEngineState({
|
|
1187
|
+
kind: "native",
|
|
1188
|
+
configPath
|
|
1189
|
+
});
|
|
1190
|
+
spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
|
|
1191
|
+
s.stop("iii-engine process started");
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
259
1194
|
async function startEngine() {
|
|
260
1195
|
const configPath = findIiiConfig();
|
|
261
1196
|
let iiiBin = whichBinary("iii");
|
|
262
1197
|
vlog(`iii binary: ${iiiBin ?? "(not on PATH)"}, config: ${configPath || "(not found)"}`);
|
|
263
|
-
if (iiiBin && configPath)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
1198
|
+
if (iiiBin && configPath) return startIiiBin(iiiBin, configPath);
|
|
1199
|
+
for (const iiiPath of fallbackIiiPaths()) if (existsSync(iiiPath)) {
|
|
1200
|
+
const v = iiiBinVersion(iiiPath);
|
|
1201
|
+
vlog(`fallback iii at ${iiiPath} reports version: ${v ?? "unknown"}`);
|
|
1202
|
+
p.log.info(`Found iii at: ${iiiPath}${v ? ` (v${v})` : ""}`);
|
|
1203
|
+
process.env["PATH"] = `${dirname(iiiPath)}${delimiter}${process.env["PATH"] ?? ""}`;
|
|
1204
|
+
iiiBin = iiiPath;
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
if (iiiBin && configPath) return startIiiBin(iiiBin, configPath);
|
|
1208
|
+
if (!configPath) {
|
|
1209
|
+
startupFailure = { kind: "no-engine" };
|
|
1210
|
+
return false;
|
|
269
1211
|
}
|
|
270
1212
|
const dockerBin = whichBinary("docker");
|
|
271
1213
|
vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
|
|
@@ -275,9 +1217,73 @@ async function startEngine() {
|
|
|
275
1217
|
join(process.cwd(), "docker-compose.yml")
|
|
276
1218
|
].find((c) => existsSync(c));
|
|
277
1219
|
vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
|
|
278
|
-
|
|
1220
|
+
const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
|
|
1221
|
+
const interactive = !!process.stdin.isTTY && !process.env["CI"];
|
|
1222
|
+
let choice;
|
|
1223
|
+
if (dockerOptIn && dockerBin && composeFile) choice = "docker";
|
|
1224
|
+
else if (!interactive) {
|
|
1225
|
+
choice = "install";
|
|
1226
|
+
p.log.info("Non-interactive environment detected — auto-installing iii-engine.");
|
|
1227
|
+
} else {
|
|
1228
|
+
p.log.warn(`iii-engine binary not found locally.`);
|
|
1229
|
+
const options = [{
|
|
1230
|
+
value: "install",
|
|
1231
|
+
label: `Install iii v${IIPINNED_VERSION} to ~/.local/bin (~6MB, ~5s)`,
|
|
1232
|
+
hint: "recommended"
|
|
1233
|
+
}];
|
|
1234
|
+
if (dockerBin && composeFile) options.push({
|
|
1235
|
+
value: "docker",
|
|
1236
|
+
label: "Use Docker compose",
|
|
1237
|
+
hint: "advanced"
|
|
1238
|
+
});
|
|
1239
|
+
options.push({
|
|
1240
|
+
value: "manual",
|
|
1241
|
+
label: "Show manual install steps and exit"
|
|
1242
|
+
});
|
|
1243
|
+
const picked = await p.select({
|
|
1244
|
+
message: "How would you like to start iii-engine?",
|
|
1245
|
+
options,
|
|
1246
|
+
initialValue: "install"
|
|
1247
|
+
});
|
|
1248
|
+
if (p.isCancel(picked)) {
|
|
1249
|
+
startupFailure = { kind: "no-engine" };
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
choice = picked;
|
|
1253
|
+
}
|
|
1254
|
+
if (choice === "manual") {
|
|
1255
|
+
startupFailure = { kind: "no-engine" };
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
if (choice === "install") {
|
|
1259
|
+
const result = await runIiiInstaller();
|
|
1260
|
+
if (result.ok && result.binPath) {
|
|
1261
|
+
process.env["PATH"] = `${dirname(result.binPath)}${delimiter}${process.env["PATH"] ?? ""}`;
|
|
1262
|
+
iiiBin = result.binPath;
|
|
1263
|
+
return startIiiBin(iiiBin, configPath);
|
|
1264
|
+
}
|
|
1265
|
+
if (dockerBin && composeFile && interactive) {
|
|
1266
|
+
const fallback = await p.confirm({
|
|
1267
|
+
message: "Auto-install failed. Try Docker compose instead?",
|
|
1268
|
+
initialValue: true
|
|
1269
|
+
});
|
|
1270
|
+
if (p.isCancel(fallback) || fallback !== true) {
|
|
1271
|
+
startupFailure = { kind: "no-engine" };
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
choice = "docker";
|
|
1275
|
+
} else {
|
|
1276
|
+
startupFailure = { kind: "no-engine" };
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (choice === "docker" && dockerBin && composeFile) {
|
|
279
1281
|
const s = p.spinner();
|
|
280
1282
|
s.start("Starting iii-engine via Docker...");
|
|
1283
|
+
writeEngineState({
|
|
1284
|
+
kind: "docker",
|
|
1285
|
+
composeFile
|
|
1286
|
+
});
|
|
281
1287
|
spawnEngineBackground(dockerBin, [
|
|
282
1288
|
"compose",
|
|
283
1289
|
"-f",
|
|
@@ -288,21 +1294,8 @@ async function startEngine() {
|
|
|
288
1294
|
s.stop("Docker compose started");
|
|
289
1295
|
return true;
|
|
290
1296
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
process.env["PATH"] = `${dirname(iiiPath)}${delimiter}${process.env["PATH"] ?? ""}`;
|
|
294
|
-
iiiBin = iiiPath;
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
if (iiiBin && configPath) {
|
|
298
|
-
const s = p.spinner();
|
|
299
|
-
s.start(`Starting iii-engine: ${iiiBin}`);
|
|
300
|
-
spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
|
|
301
|
-
s.stop("iii-engine process started");
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
if (!iiiBin && (!dockerBin || !composeFile)) startupFailure = { kind: "no-engine" };
|
|
305
|
-
else if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
|
|
1297
|
+
if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
|
|
1298
|
+
else startupFailure = { kind: "no-engine" };
|
|
306
1299
|
return false;
|
|
307
1300
|
}
|
|
308
1301
|
async function waitForEngine(timeoutMs) {
|
|
@@ -316,58 +1309,70 @@ async function waitForEngine(timeoutMs) {
|
|
|
316
1309
|
function installInstructions() {
|
|
317
1310
|
const releaseUrl = iiiReleaseUrl();
|
|
318
1311
|
if (IS_WINDOWS) return [
|
|
319
|
-
`agentmemory
|
|
1312
|
+
`agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
|
|
320
1313
|
"",
|
|
321
1314
|
" A) Download the prebuilt Windows binary:",
|
|
322
1315
|
` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
|
|
323
|
-
` 2. Download iii-x86_64-pc-windows-msvc.zip`,
|
|
324
|
-
"
|
|
325
|
-
" 3. Extract iii.exe and either add its folder to PATH",
|
|
326
|
-
" or move it to %USERPROFILE%\\.local\\bin\\iii.exe",
|
|
1316
|
+
` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
|
|
1317
|
+
" 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
|
|
327
1318
|
" 4. Re-run: npx @agentmemory/agentmemory",
|
|
328
1319
|
"",
|
|
329
|
-
|
|
330
|
-
"
|
|
331
|
-
` 2. docker pull iiidev/iii:${IIPINNED_VERSION}`,
|
|
332
|
-
" 3. Start Docker Desktop (engine must be running)",
|
|
333
|
-
" 4. Re-run: npx @agentmemory/agentmemory",
|
|
1320
|
+
` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
|
|
1321
|
+
" Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
|
|
334
1322
|
"",
|
|
335
|
-
"Or skip the engine entirely
|
|
336
|
-
"
|
|
1323
|
+
"Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
|
|
1324
|
+
"",
|
|
1325
|
+
"Docs: https://iii.dev/docs"
|
|
337
1326
|
];
|
|
338
|
-
const linuxInstall = releaseUrl ? ` A)
|
|
1327
|
+
const linuxInstall = releaseUrl ? ` A) curl -fsSL "${releaseUrl}" | tar -xz -C ~/.local/bin && chmod +x ~/.local/bin/iii` : ` A) Manual download: https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
|
|
339
1328
|
return [
|
|
340
|
-
`agentmemory
|
|
1329
|
+
`agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
|
|
341
1330
|
"",
|
|
342
1331
|
linuxInstall,
|
|
343
|
-
|
|
1332
|
+
" Then re-run: npx @agentmemory/agentmemory",
|
|
344
1333
|
"",
|
|
345
|
-
` B) Docker:
|
|
1334
|
+
` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
|
|
1335
|
+
" Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
|
|
346
1336
|
"",
|
|
347
|
-
"Or skip the engine entirely
|
|
348
|
-
" npx @agentmemory/agentmemory mcp",
|
|
1337
|
+
"Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
|
|
349
1338
|
"",
|
|
350
|
-
"Docs: https://iii.dev/docs"
|
|
351
|
-
`Why pinned: iii v0.11.6 introduces the new sandbox-everything model`,
|
|
352
|
-
`(\`iii worker add\` registration). agentmemory still uses the older`,
|
|
353
|
-
`iii-exec config-file worker model and needs a refactor before it`,
|
|
354
|
-
`runs cleanly under the new engine. Override with`,
|
|
355
|
-
`AGENTMEMORY_III_VERSION=<version> when you've migrated manually.`
|
|
1339
|
+
"Docs: https://iii.dev/docs"
|
|
356
1340
|
];
|
|
357
1341
|
}
|
|
358
1342
|
function portInUseDiagnostic(port) {
|
|
359
1343
|
return IS_WINDOWS ? ` netstat -ano | findstr :${port}` : ` lsof -i :${port} # or: ss -tlnp | grep :${port}`;
|
|
360
1344
|
}
|
|
1345
|
+
async function waitForAgentmemoryReady(timeoutMs) {
|
|
1346
|
+
const start = Date.now();
|
|
1347
|
+
while (Date.now() - start < timeoutMs) {
|
|
1348
|
+
if (await isAgentmemoryReady()) return true;
|
|
1349
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1350
|
+
}
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
function printReadyHint() {
|
|
1354
|
+
const hint = `Memory ready on :${getRestPort()} · viewer on ${getViewerUrl()} · try: agentmemory demo`;
|
|
1355
|
+
process.stdout.write("\n" + hint + "\n");
|
|
1356
|
+
}
|
|
361
1357
|
async function main() {
|
|
362
|
-
|
|
1358
|
+
if (IS_RESET) resetPrefs();
|
|
1359
|
+
const firstRun = isFirstRun();
|
|
1360
|
+
const prefs = readPrefs();
|
|
1361
|
+
if (firstRun || IS_RESET || IS_VERBOSE || !prefs.skipSplash) renderSplash(VERSION);
|
|
1362
|
+
if (firstRun || IS_RESET) await runOnboarding();
|
|
363
1363
|
if (skipEngine) {
|
|
364
|
-
p.log.info("Skipping engine check (--no-engine)");
|
|
365
|
-
await import("./src-
|
|
1364
|
+
if (IS_VERBOSE) p.log.info("Skipping engine check (--no-engine)");
|
|
1365
|
+
await import("./src-BGcqJR1a.mjs");
|
|
1366
|
+
if (await waitForAgentmemoryReady(15e3)) printReadyHint();
|
|
366
1367
|
return;
|
|
367
1368
|
}
|
|
368
1369
|
if (await isEngineRunning()) {
|
|
369
|
-
p.log.success("iii-engine is running");
|
|
370
|
-
|
|
1370
|
+
if (IS_VERBOSE) p.log.success("iii-engine is running");
|
|
1371
|
+
warnIfEngineVersionMismatch(whichBinary("iii") ?? fallbackIiiPaths().find((p) => existsSync(p)) ?? null);
|
|
1372
|
+
adoptRunningEngine();
|
|
1373
|
+
maybeEmitNpxHint();
|
|
1374
|
+
await import("./src-BGcqJR1a.mjs");
|
|
1375
|
+
if (await waitForAgentmemoryReady(15e3)) printReadyHint();
|
|
371
1376
|
return;
|
|
372
1377
|
}
|
|
373
1378
|
if (!await startEngine()) {
|
|
@@ -411,7 +1416,10 @@ async function main() {
|
|
|
411
1416
|
process.exit(1);
|
|
412
1417
|
}
|
|
413
1418
|
s.stop("iii-engine is ready");
|
|
414
|
-
|
|
1419
|
+
maybeEmitNpxHint();
|
|
1420
|
+
await import("./src-BGcqJR1a.mjs");
|
|
1421
|
+
if (await waitForAgentmemoryReady(15e3)) printReadyHint();
|
|
1422
|
+
writePrefs({ skipSplash: true });
|
|
415
1423
|
}
|
|
416
1424
|
async function apiFetch(base, path, timeoutMs = 5e3) {
|
|
417
1425
|
try {
|
|
@@ -525,10 +1533,143 @@ function checkClaudeCodeHooks() {
|
|
|
525
1533
|
if (content.includes("Loading hooks from plugin: agentmemory")) return { state: "loaded" };
|
|
526
1534
|
return { state: "not-loaded" };
|
|
527
1535
|
}
|
|
528
|
-
|
|
529
|
-
|
|
1536
|
+
function buildDoctorContext() {
|
|
1537
|
+
return {
|
|
1538
|
+
baseUrl: getBaseUrl(),
|
|
1539
|
+
viewerUrl: getViewerUrl(),
|
|
1540
|
+
envPath: join(homedir(), ".agentmemory", ".env"),
|
|
1541
|
+
pidfilePath: enginePidfilePath(),
|
|
1542
|
+
enginePath: engineStatePath(),
|
|
1543
|
+
pinnedVersion: IIPINNED_VERSION
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function buildDoctorEffects() {
|
|
1547
|
+
return {
|
|
1548
|
+
envFileExists: () => existsSync(join(homedir(), ".agentmemory", ".env")),
|
|
1549
|
+
readEnvFile: () => {
|
|
1550
|
+
try {
|
|
1551
|
+
return parseEnvFile(readFileSync(join(homedir(), ".agentmemory", ".env"), "utf-8"));
|
|
1552
|
+
} catch {
|
|
1553
|
+
return {};
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
pidfileExists: () => existsSync(enginePidfilePath()),
|
|
1557
|
+
pidfilePidIsAlive: () => {
|
|
1558
|
+
const pid = readEnginePidfile();
|
|
1559
|
+
if (pid === null) return null;
|
|
1560
|
+
return pidAlive(pid);
|
|
1561
|
+
},
|
|
1562
|
+
findIiiBinary: () => whichBinary("iii"),
|
|
1563
|
+
localBinIiiPath: () => join(homedir(), ".local", "bin", IS_WINDOWS ? "iii.exe" : "iii"),
|
|
1564
|
+
iiiBinaryVersion: (binPath) => iiiBinVersion(binPath),
|
|
1565
|
+
viewerReachable: async (timeoutMs = 2e3) => {
|
|
1566
|
+
try {
|
|
1567
|
+
return (await fetch(getViewerUrl(), { signal: AbortSignal.timeout(timeoutMs) })).ok;
|
|
1568
|
+
} catch {
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
runInit: async () => {
|
|
1573
|
+
try {
|
|
1574
|
+
await runInit();
|
|
1575
|
+
return {
|
|
1576
|
+
ok: true,
|
|
1577
|
+
message: "Wrote ~/.agentmemory/.env"
|
|
1578
|
+
};
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
return {
|
|
1581
|
+
ok: false,
|
|
1582
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
},
|
|
1586
|
+
openEditor: async (path) => {
|
|
1587
|
+
const editor = process.env["EDITOR"] || process.env["VISUAL"] || "nano";
|
|
1588
|
+
p.log.info(`Opening ${path} in ${editor}…`);
|
|
1589
|
+
try {
|
|
1590
|
+
const result = spawnSync(editor, [path], { stdio: "inherit" });
|
|
1591
|
+
if (result.error) return {
|
|
1592
|
+
ok: false,
|
|
1593
|
+
message: `Failed to launch ${editor}: ${result.error.message}`
|
|
1594
|
+
};
|
|
1595
|
+
if ((result.status ?? 0) !== 0) return {
|
|
1596
|
+
ok: false,
|
|
1597
|
+
message: `${editor} exited with code ${result.status}`
|
|
1598
|
+
};
|
|
1599
|
+
return {
|
|
1600
|
+
ok: true,
|
|
1601
|
+
message: `Saved ${path}`
|
|
1602
|
+
};
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
return {
|
|
1605
|
+
ok: false,
|
|
1606
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
runIiiInstaller: async () => {
|
|
1611
|
+
const r = await runIiiInstaller();
|
|
1612
|
+
return {
|
|
1613
|
+
ok: r.ok,
|
|
1614
|
+
message: r.ok ? `Installed iii v${IIPINNED_VERSION} to ${r.binPath}` : "iii installer failed (see warnings above)"
|
|
1615
|
+
};
|
|
1616
|
+
},
|
|
1617
|
+
runStop: async () => {
|
|
1618
|
+
try {
|
|
1619
|
+
const portPids = findEnginePidsByPort(getRestPort());
|
|
1620
|
+
const pidfilePid = readEnginePidfile();
|
|
1621
|
+
if (portPids.length === 0 && pidfilePid === null) {
|
|
1622
|
+
clearEnginePidfile();
|
|
1623
|
+
clearEngineState();
|
|
1624
|
+
return {
|
|
1625
|
+
ok: true,
|
|
1626
|
+
message: "Nothing to stop."
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1630
|
+
if (pidfilePid) candidates.add(pidfilePid);
|
|
1631
|
+
for (const pid of portPids) candidates.add(pid);
|
|
1632
|
+
let allStopped = true;
|
|
1633
|
+
for (const pid of candidates) if (!await signalAndWait(pid, "SIGTERM", 3e3)) allStopped = false;
|
|
1634
|
+
clearEnginePidfile();
|
|
1635
|
+
clearEngineState();
|
|
1636
|
+
return {
|
|
1637
|
+
ok: allStopped,
|
|
1638
|
+
message: allStopped ? "Engine stopped." : "Some engine pids survived."
|
|
1639
|
+
};
|
|
1640
|
+
} catch (err) {
|
|
1641
|
+
return {
|
|
1642
|
+
ok: false,
|
|
1643
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
runStart: async () => {
|
|
1648
|
+
try {
|
|
1649
|
+
if (!await startEngine()) return {
|
|
1650
|
+
ok: false,
|
|
1651
|
+
message: "startEngine() returned false"
|
|
1652
|
+
};
|
|
1653
|
+
const ready = await waitForEngine(15e3);
|
|
1654
|
+
return {
|
|
1655
|
+
ok: ready,
|
|
1656
|
+
message: ready ? "Engine ready" : "Engine did not become ready within 15s"
|
|
1657
|
+
};
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
return {
|
|
1660
|
+
ok: false,
|
|
1661
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
clearEnginePidAndState: () => {
|
|
1666
|
+
clearEnginePidfile();
|
|
1667
|
+
clearEngineState();
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
async function passiveServerChecks() {
|
|
530
1672
|
const base = getBaseUrl();
|
|
531
|
-
const viewerUrl = getViewerUrl();
|
|
532
1673
|
const checks = [];
|
|
533
1674
|
const serverUp = await isEngineRunning();
|
|
534
1675
|
checks.push({
|
|
@@ -536,16 +1677,12 @@ async function runDoctor() {
|
|
|
536
1677
|
ok: serverUp,
|
|
537
1678
|
hint: serverUp ? void 0 : `Start with: npx @agentmemory/agentmemory (tried ${base})`
|
|
538
1679
|
});
|
|
539
|
-
if (!serverUp)
|
|
540
|
-
p.note(formatChecks(checks), "server unreachable");
|
|
541
|
-
process.exit(1);
|
|
542
|
-
}
|
|
1680
|
+
if (!serverUp) return checks;
|
|
543
1681
|
const [health, flags, graph] = await Promise.all([
|
|
544
1682
|
apiFetch(base, "health", 3e3),
|
|
545
1683
|
apiFetch(base, "config/flags", 3e3),
|
|
546
1684
|
apiFetch(base, "graph/stats", 3e3)
|
|
547
1685
|
]);
|
|
548
|
-
const viewerUp = await fetch(viewerUrl, { signal: AbortSignal.timeout(2e3) }).then((r) => r.ok).catch(() => false);
|
|
549
1686
|
const hasLlm = flags?.provider === "llm";
|
|
550
1687
|
const hasEmbed = flags?.embeddingProvider === "embeddings";
|
|
551
1688
|
const graphHas = Number(graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? 0) > 0;
|
|
@@ -553,18 +1690,14 @@ async function runDoctor() {
|
|
|
553
1690
|
name: "Health status",
|
|
554
1691
|
ok: health?.status === "healthy",
|
|
555
1692
|
hint: health?.status === "healthy" ? void 0 : `Status: ${health?.status || "unknown"}`
|
|
556
|
-
}, {
|
|
557
|
-
name: "Viewer reachable",
|
|
558
|
-
ok: viewerUp,
|
|
559
|
-
hint: viewerUp ? void 0 : `${viewerUrl} not responding`
|
|
560
1693
|
}, {
|
|
561
1694
|
name: "LLM provider",
|
|
562
1695
|
ok: hasLlm,
|
|
563
|
-
hint: hasLlm ? void 0 : "
|
|
1696
|
+
hint: hasLlm ? void 0 : "set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env"
|
|
564
1697
|
}, {
|
|
565
1698
|
name: "Embedding provider",
|
|
566
1699
|
ok: hasEmbed,
|
|
567
|
-
hint: hasEmbed ? void 0 : "Running BM25-only. Add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST
|
|
1700
|
+
hint: hasEmbed ? void 0 : "Running BM25-only. Add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST"
|
|
568
1701
|
});
|
|
569
1702
|
for (const f of flags?.flags || []) checks.push({
|
|
570
1703
|
name: f.label,
|
|
@@ -580,7 +1713,7 @@ async function runDoctor() {
|
|
|
580
1713
|
};
|
|
581
1714
|
case "not-loaded": return {
|
|
582
1715
|
ok: false,
|
|
583
|
-
hint: "Plugin enabled but hooks not loaded by Claude Code. Try: /plugin uninstall agentmemory@agentmemory && /plugin install agentmemory@agentmemory, then restart the session.
|
|
1716
|
+
hint: "Plugin enabled but hooks not loaded by Claude Code. Try: /plugin uninstall agentmemory@agentmemory && /plugin install agentmemory@agentmemory, then restart the session."
|
|
584
1717
|
};
|
|
585
1718
|
case "no-debug-log": return {
|
|
586
1719
|
ok: false,
|
|
@@ -596,16 +1729,136 @@ async function runDoctor() {
|
|
|
596
1729
|
checks.push({
|
|
597
1730
|
name: "Knowledge graph populated",
|
|
598
1731
|
ok: graphHas,
|
|
599
|
-
hint: graphHas ? void 0 : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true
|
|
1732
|
+
hint: graphHas ? void 0 : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true."
|
|
600
1733
|
});
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1734
|
+
return checks;
|
|
1735
|
+
}
|
|
1736
|
+
async function askFixAction(d) {
|
|
1737
|
+
const choice = await p.select({
|
|
1738
|
+
message: `[${d.id}] ${d.message}`,
|
|
1739
|
+
options: [
|
|
1740
|
+
{
|
|
1741
|
+
value: "fix",
|
|
1742
|
+
label: "F Fix",
|
|
1743
|
+
hint: d.fixPreview
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
value: "skip",
|
|
1747
|
+
label: "S Skip"
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
value: "more",
|
|
1751
|
+
label: "? More info"
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
value: "quit",
|
|
1755
|
+
label: "Q Quit doctor"
|
|
1756
|
+
}
|
|
1757
|
+
],
|
|
1758
|
+
initialValue: "fix"
|
|
1759
|
+
});
|
|
1760
|
+
if (p.isCancel(choice)) return "quit";
|
|
1761
|
+
return choice;
|
|
1762
|
+
}
|
|
1763
|
+
async function applyFixWithReport(d, ctx, dryRun) {
|
|
1764
|
+
if (dryRun) {
|
|
1765
|
+
p.log.info(`[dry-run] would: ${d.fixPreview}`);
|
|
1766
|
+
return {
|
|
1767
|
+
ok: true,
|
|
1768
|
+
message: "(dry-run)"
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const result = await d.fix(ctx);
|
|
1772
|
+
if (result.ok) p.log.success(result.message ?? `${d.id} fixed.`);
|
|
1773
|
+
else p.log.error(result.message ?? `${d.id} fix failed.`);
|
|
1774
|
+
return result;
|
|
1775
|
+
}
|
|
1776
|
+
async function runDoctor() {
|
|
1777
|
+
p.intro("agentmemory doctor");
|
|
1778
|
+
const applyAll = args.includes("--all");
|
|
1779
|
+
const dryRun = args.includes("--dry-run");
|
|
1780
|
+
if (applyAll && dryRun) {
|
|
1781
|
+
p.log.error("Cannot combine --all and --dry-run.");
|
|
1782
|
+
process.exit(2);
|
|
1783
|
+
}
|
|
1784
|
+
const passive = await passiveServerChecks();
|
|
1785
|
+
const passivePassed = passive.filter((c) => c.ok).length;
|
|
1786
|
+
p.note(formatChecks(passive), `server: ${passivePassed}/${passive.length} passing`);
|
|
1787
|
+
const ctx = buildDoctorContext();
|
|
1788
|
+
const diagnostics = buildDiagnostics(buildDoctorEffects());
|
|
1789
|
+
if (dryRun) {
|
|
1790
|
+
const results = [];
|
|
1791
|
+
for (const d of diagnostics) results.push({
|
|
1792
|
+
diagnostic: d,
|
|
1793
|
+
status: await d.check(ctx)
|
|
1794
|
+
});
|
|
1795
|
+
const lines = dryRunPlan(ctx, results);
|
|
1796
|
+
p.note(lines.join("\n"), "dry-run plan");
|
|
1797
|
+
p.outro("Dry-run complete. Re-run without --dry-run to apply.");
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
let failed = 0;
|
|
1801
|
+
let fixed = 0;
|
|
1802
|
+
let skipped = 0;
|
|
1803
|
+
let quit = false;
|
|
1804
|
+
for (const d of diagnostics) {
|
|
1805
|
+
if (quit) {
|
|
1806
|
+
skipped++;
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
const status = await d.check(ctx);
|
|
1810
|
+
if (status.ok) {
|
|
1811
|
+
p.log.success(`${d.id} ✓${status.detail ? ` (${status.detail})` : ""}`);
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
failed++;
|
|
1815
|
+
p.log.warn(`${d.id} ✗ ${status.detail ?? ""}`.trim());
|
|
1816
|
+
p.log.info(`why: ${d.fixPreview}`);
|
|
1817
|
+
if (d.manualOnly) p.log.info(`(manual fix only — see "${d.id}" docs)`);
|
|
1818
|
+
if (applyAll) {
|
|
1819
|
+
if ((await applyFixWithReport(d, ctx, false)).ok) fixed++;
|
|
1820
|
+
if (!(await d.check(ctx)).ok) p.log.warn(`${d.id} still failing after fix.`);
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
while (true) {
|
|
1824
|
+
const action = await askFixAction(d);
|
|
1825
|
+
if (action === "fix") {
|
|
1826
|
+
if ((await applyFixWithReport(d, ctx, false)).ok) {
|
|
1827
|
+
const after = await d.check(ctx);
|
|
1828
|
+
if (after.ok) fixed++;
|
|
1829
|
+
else p.log.warn(`${d.id} still failing after fix: ${after.detail ?? ""}`);
|
|
1830
|
+
}
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
if (action === "skip") {
|
|
1834
|
+
skipped++;
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
if (action === "more") {
|
|
1838
|
+
p.note(d.moreInfo, `[${d.id}] more info`);
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (action === "quit") {
|
|
1842
|
+
quit = true;
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
const summary = `${diagnostics.length} checks · ${failed} failing · ${fixed} fixed · ${skipped} skipped`;
|
|
1848
|
+
if (quit) {
|
|
1849
|
+
p.outro(`Quit early. ${summary}`);
|
|
607
1850
|
process.exit(1);
|
|
608
1851
|
}
|
|
1852
|
+
if (failed === 0) {
|
|
1853
|
+
p.outro("All diagnostics passing. agentmemory is healthy.");
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (failed - fixed === 0) {
|
|
1857
|
+
p.outro(`All fixes applied. ${summary}`);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
p.outro(summary);
|
|
1861
|
+
process.exit(1);
|
|
609
1862
|
}
|
|
610
1863
|
function buildDemoSessions() {
|
|
611
1864
|
return [
|
|
@@ -872,36 +2125,16 @@ async function runUpgrade() {
|
|
|
872
2125
|
});
|
|
873
2126
|
} else p.log.warn("No package manager found (pnpm/npm). Skipping JS dependency upgrade.");
|
|
874
2127
|
else p.log.warn("No package.json in current directory. Skipping JS dependency upgrade.");
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
if (upgradeEngine === true) {
|
|
887
|
-
const releaseUrl = iiiReleaseUrl();
|
|
888
|
-
const asset = iiiReleaseAsset();
|
|
889
|
-
const isZipAsset = asset?.endsWith(".zip") === true;
|
|
890
|
-
if (!releaseUrl) p.log.warn(`iii-engine binary not available for ${platform()}/${process.arch}. Use Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
|
|
891
|
-
else if (IS_WINDOWS || isZipAsset) p.log.info(`Skipping auto-install on ${platform()} — the ${asset} asset isn't tar-compatible. Install manually:\n 1. Download ${releaseUrl}\n 2. Extract iii.exe and place it on PATH (e.g. %USERPROFILE%\\.local\\bin)\nOr use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`);
|
|
892
|
-
else {
|
|
893
|
-
const binDir = join(homedir(), ".local", "bin");
|
|
894
|
-
if (!runCommand(shBin, ["-c", [
|
|
895
|
-
`mkdir -p "${binDir}"`,
|
|
896
|
-
`curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
|
|
897
|
-
`chmod +x "${binDir}/iii"`
|
|
898
|
-
].join(" && ")], {
|
|
899
|
-
label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
|
|
900
|
-
optional: true
|
|
901
|
-
})) p.log.warn(`iii-engine installer failed. Fallbacks: Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
|
|
902
|
-
}
|
|
903
|
-
} else p.log.info("Skipped iii-engine installer.");
|
|
904
|
-
} else p.log.warn("curl or sh not found. Skipping iii-engine installer.");
|
|
2128
|
+
const upgradeEngine = await p.confirm({
|
|
2129
|
+
message: "Re-run the iii-engine install script (curl | sh)?",
|
|
2130
|
+
initialValue: true
|
|
2131
|
+
});
|
|
2132
|
+
if (p.isCancel(upgradeEngine)) {
|
|
2133
|
+
p.cancel("Cancelled.");
|
|
2134
|
+
return process.exit(0);
|
|
2135
|
+
}
|
|
2136
|
+
if (upgradeEngine === true) await runIiiInstaller();
|
|
2137
|
+
else p.log.info("Skipped iii-engine installer.");
|
|
905
2138
|
if (dockerBin) runCommand(dockerBin, ["pull", `iiidev/iii:${IIPINNED_VERSION}`], {
|
|
906
2139
|
label: `Pulling iii Docker image v${IIPINNED_VERSION} (pinned)`,
|
|
907
2140
|
optional: true
|
|
@@ -916,8 +2149,160 @@ async function runUpgrade() {
|
|
|
916
2149
|
" 3) restart agentmemory process"
|
|
917
2150
|
].join("\n"), "agentmemory upgrade");
|
|
918
2151
|
}
|
|
2152
|
+
function pidAlive(pid) {
|
|
2153
|
+
try {
|
|
2154
|
+
process.kill(pid, 0);
|
|
2155
|
+
return true;
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
return err?.code === "EPERM";
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async function signalAndWait(pid, initialSignal, timeoutMs) {
|
|
2161
|
+
try {
|
|
2162
|
+
process.kill(pid, initialSignal);
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
const code = err?.code;
|
|
2165
|
+
if (code === "ESRCH") return true;
|
|
2166
|
+
if (code === "EPERM") {
|
|
2167
|
+
p.log.warn(`No permission to signal pid ${pid}. Try: kill ${pid}`);
|
|
2168
|
+
return false;
|
|
2169
|
+
}
|
|
2170
|
+
vlog(`${initialSignal} ${pid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
const deadline = Date.now() + timeoutMs;
|
|
2174
|
+
while (Date.now() < deadline) {
|
|
2175
|
+
if (!pidAlive(pid)) return true;
|
|
2176
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2177
|
+
}
|
|
2178
|
+
if (!pidAlive(pid)) return true;
|
|
2179
|
+
try {
|
|
2180
|
+
process.kill(pid, "SIGKILL");
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
if (err?.code === "ESRCH") return true;
|
|
2183
|
+
vlog(`SIGKILL ${pid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2187
|
+
return !pidAlive(pid);
|
|
2188
|
+
}
|
|
2189
|
+
function findEnginePidsByPort(port) {
|
|
2190
|
+
if (IS_WINDOWS) return [];
|
|
2191
|
+
const lsof = whichBinary("lsof");
|
|
2192
|
+
if (!lsof) return [];
|
|
2193
|
+
const selfPid = process.pid;
|
|
2194
|
+
try {
|
|
2195
|
+
return execFileSync(lsof, [
|
|
2196
|
+
"-i",
|
|
2197
|
+
`:${port}`,
|
|
2198
|
+
"-sTCP:LISTEN",
|
|
2199
|
+
"-t"
|
|
2200
|
+
], {
|
|
2201
|
+
encoding: "utf-8",
|
|
2202
|
+
stdio: [
|
|
2203
|
+
"ignore",
|
|
2204
|
+
"pipe",
|
|
2205
|
+
"ignore"
|
|
2206
|
+
]
|
|
2207
|
+
}).split(/\s+/).map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n) && n > 0 && n !== selfPid);
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
vlog(`lsof :${port}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2210
|
+
return [];
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
async function stopDockerEngine(composeFile, port) {
|
|
2214
|
+
const dockerBin = whichBinary("docker");
|
|
2215
|
+
if (!dockerBin) {
|
|
2216
|
+
p.log.error(`Engine was started via Docker compose, but \`docker\` is no longer on PATH. Stop it manually:\n docker compose -f ${composeFile} down`);
|
|
2217
|
+
process.exit(1);
|
|
2218
|
+
}
|
|
2219
|
+
if (!existsSync(composeFile)) {
|
|
2220
|
+
p.log.error(`Engine state references ${composeFile}, but the file is gone. Stop it manually:\n docker compose down (from the dir holding the original docker-compose.yml)`);
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
2223
|
+
const ok = runCommand(dockerBin, [
|
|
2224
|
+
"compose",
|
|
2225
|
+
"-f",
|
|
2226
|
+
composeFile,
|
|
2227
|
+
"down"
|
|
2228
|
+
], { label: `docker compose -f ${composeFile} down` });
|
|
2229
|
+
clearEnginePidfile();
|
|
2230
|
+
clearEngineState();
|
|
2231
|
+
if (!ok) {
|
|
2232
|
+
p.log.error(`docker compose down failed. The engine may still be running on :${port}. Inspect with:\n docker compose -f ${composeFile} ps`);
|
|
2233
|
+
process.exit(1);
|
|
2234
|
+
}
|
|
2235
|
+
p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
|
|
2236
|
+
}
|
|
2237
|
+
async function runStop() {
|
|
2238
|
+
p.intro("agentmemory stop");
|
|
2239
|
+
const port = getRestPort();
|
|
2240
|
+
const state = readEngineState();
|
|
2241
|
+
const running = await isEngineRunning();
|
|
2242
|
+
const force = args.includes("--force");
|
|
2243
|
+
if (state?.kind === "docker") {
|
|
2244
|
+
if (!running) {
|
|
2245
|
+
p.log.info(`No engine responding on port ${port}.`);
|
|
2246
|
+
clearEnginePidfile();
|
|
2247
|
+
clearEngineState();
|
|
2248
|
+
p.outro("Nothing to stop.");
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
await stopDockerEngine(state.composeFile, port);
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const portPids = findEnginePidsByPort(port);
|
|
2255
|
+
const pidfilePid = readEnginePidfile();
|
|
2256
|
+
if (!running) {
|
|
2257
|
+
if (portPids.length === 0 && pidfilePid === null) {
|
|
2258
|
+
clearEnginePidfile();
|
|
2259
|
+
clearEngineState();
|
|
2260
|
+
p.outro("Nothing to stop.");
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
const survivors = new Set(portPids);
|
|
2264
|
+
if (pidfilePid) survivors.add(pidfilePid);
|
|
2265
|
+
p.log.warn(`Engine not responding on :${port}, but ${survivors.size} process(es) still hold the port or pidfile: ${[...survivors].join(", ")}`);
|
|
2266
|
+
p.log.info(`Preserving ~/.agentmemory/iii.pid. Investigate before manual cleanup:\n ps -p ${[...survivors].join(",")} -o pid,ppid,comm,etime\n ${IS_WINDOWS ? "netstat -ano | findstr :" + port : "lsof -i :" + port}`);
|
|
2267
|
+
process.exit(1);
|
|
2268
|
+
}
|
|
2269
|
+
if (!state) {
|
|
2270
|
+
const compose = discoverComposeFile();
|
|
2271
|
+
if (compose && pidfilePid === null) if (force) p.log.warn(`--force: bypassing Docker-heuristic guard. Falling back to native pidfile + lsof on :${port}.`);
|
|
2272
|
+
else {
|
|
2273
|
+
p.log.error(`Engine is running on :${port} but no pidfile or state file is present. It may have been started via Docker compose by a different shell. Refusing to signal host PIDs.\n\nStop it with:\n docker compose -f ${compose} down\n\nOr re-run with --force to signal whatever lsof finds on :${port}, or AGENTMEMORY_USE_DOCKER=1 to record state next time.`);
|
|
2274
|
+
process.exit(1);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
2278
|
+
if (pidfilePid) candidates.add(pidfilePid);
|
|
2279
|
+
for (const pid of portPids) candidates.add(pid);
|
|
2280
|
+
if (candidates.size === 0) {
|
|
2281
|
+
p.log.error(`Could not locate engine process. Try:\n ${IS_WINDOWS ? "netstat -ano | findstr :" + port : "lsof -i :" + port + " -t | xargs kill -9"}`);
|
|
2282
|
+
process.exit(1);
|
|
2283
|
+
}
|
|
2284
|
+
let allStopped = true;
|
|
2285
|
+
for (const pid of candidates) {
|
|
2286
|
+
const s = p.spinner();
|
|
2287
|
+
s.start(`Stopping iii-engine (pid ${pid})...`);
|
|
2288
|
+
const ok = await signalAndWait(pid, "SIGTERM", 3e3);
|
|
2289
|
+
s.stop(ok ? `Stopped pid ${pid}` : `Failed to stop pid ${pid}`);
|
|
2290
|
+
if (!ok) allStopped = false;
|
|
2291
|
+
}
|
|
2292
|
+
clearEnginePidfile();
|
|
2293
|
+
clearEngineState();
|
|
2294
|
+
if (!allStopped) {
|
|
2295
|
+
p.log.error("One or more engine processes survived SIGKILL. Investigate with `ps`.");
|
|
2296
|
+
process.exit(1);
|
|
2297
|
+
}
|
|
2298
|
+
p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
|
|
2299
|
+
}
|
|
919
2300
|
async function runMcp() {
|
|
920
|
-
await import("./standalone-
|
|
2301
|
+
await import("./standalone-BQOaGF4z.mjs");
|
|
2302
|
+
}
|
|
2303
|
+
async function runConnectCmd() {
|
|
2304
|
+
const { runConnect } = await import("./connect-hRTF7E2c.mjs");
|
|
2305
|
+
await runConnect(args.slice(1));
|
|
921
2306
|
}
|
|
922
2307
|
async function runImportJsonl() {
|
|
923
2308
|
const VALUE_FLAGS = new Set(["--port", "--tools"]);
|
|
@@ -1021,12 +2406,126 @@ async function runImportJsonl() {
|
|
|
1021
2406
|
process.exit(1);
|
|
1022
2407
|
}
|
|
1023
2408
|
}
|
|
2409
|
+
function loadConnectManifest(home) {
|
|
2410
|
+
const path = join(home, ".agentmemory", "backups", "connect-manifest.json");
|
|
2411
|
+
try {
|
|
2412
|
+
const raw = readFileSync(path, "utf-8");
|
|
2413
|
+
const parsed = JSON.parse(raw);
|
|
2414
|
+
if (Array.isArray(parsed?.installed)) return { installed: parsed.installed };
|
|
2415
|
+
return null;
|
|
2416
|
+
} catch {
|
|
2417
|
+
return null;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
function probeLocalBinIiiVersion(home) {
|
|
2421
|
+
const path = localBinIii(home);
|
|
2422
|
+
if (!existsSync(path)) return null;
|
|
2423
|
+
return iiiBinVersion(path);
|
|
2424
|
+
}
|
|
2425
|
+
function safeDelete(path) {
|
|
2426
|
+
try {
|
|
2427
|
+
if (!existsSync(path)) return {
|
|
2428
|
+
ok: true,
|
|
2429
|
+
message: `not present (${path})`
|
|
2430
|
+
};
|
|
2431
|
+
if (statSync(path).isDirectory()) rmSync(path, {
|
|
2432
|
+
recursive: true,
|
|
2433
|
+
force: true
|
|
2434
|
+
});
|
|
2435
|
+
else unlinkSync(path);
|
|
2436
|
+
return {
|
|
2437
|
+
ok: true,
|
|
2438
|
+
message: `deleted ${path}`
|
|
2439
|
+
};
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
return {
|
|
2442
|
+
ok: false,
|
|
2443
|
+
message: `failed ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
async function runRemove() {
|
|
2448
|
+
p.intro("agentmemory remove");
|
|
2449
|
+
const force = args.includes("--force");
|
|
2450
|
+
const keepData = args.includes("--keep-data");
|
|
2451
|
+
const home = homedir();
|
|
2452
|
+
const connectManifest = loadConnectManifest(home);
|
|
2453
|
+
const plan = buildRemovePlan({
|
|
2454
|
+
home,
|
|
2455
|
+
pinnedVersion: IIPINNED_VERSION,
|
|
2456
|
+
localBinIiiVersion: probeLocalBinIiiVersion(home),
|
|
2457
|
+
connectManifest
|
|
2458
|
+
}, {
|
|
2459
|
+
force,
|
|
2460
|
+
keepData
|
|
2461
|
+
});
|
|
2462
|
+
if (plan.filter((it) => it.applicable).length === 0) {
|
|
2463
|
+
p.outro("Nothing to remove. agentmemory is already gone.");
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
p.note(formatPlan(plan), "destruction plan");
|
|
2467
|
+
if (!force) {
|
|
2468
|
+
const proceed = await p.confirm({
|
|
2469
|
+
message: "Proceed with these deletions?",
|
|
2470
|
+
initialValue: false
|
|
2471
|
+
});
|
|
2472
|
+
if (p.isCancel(proceed) || proceed !== true) {
|
|
2473
|
+
p.cancel("Cancelled. Nothing was deleted.");
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
const sure = await p.confirm({
|
|
2477
|
+
message: "This is irreversible. Continue?",
|
|
2478
|
+
initialValue: false
|
|
2479
|
+
});
|
|
2480
|
+
if (p.isCancel(sure) || sure !== true) {
|
|
2481
|
+
p.cancel("Cancelled. Nothing was deleted.");
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
for (const item of plan) {
|
|
2486
|
+
if (!item.applicable) continue;
|
|
2487
|
+
if (item.alwaysAsk) {
|
|
2488
|
+
const ok = await p.confirm({
|
|
2489
|
+
message: `${item.description} — really delete${item.path ? ` ${item.path}` : ""}?`,
|
|
2490
|
+
initialValue: false
|
|
2491
|
+
});
|
|
2492
|
+
if (p.isCancel(ok) || ok !== true) {
|
|
2493
|
+
p.log.info(`skipped: ${item.id}`);
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
if (item.id === "stop-engine") {
|
|
2498
|
+
try {
|
|
2499
|
+
const portPids = findEnginePidsByPort(getRestPort());
|
|
2500
|
+
const pidfilePid = readEnginePidfile();
|
|
2501
|
+
const cands = /* @__PURE__ */ new Set();
|
|
2502
|
+
if (pidfilePid) cands.add(pidfilePid);
|
|
2503
|
+
for (const pid of portPids) cands.add(pid);
|
|
2504
|
+
for (const pid of cands) await signalAndWait(pid, "SIGTERM", 3e3);
|
|
2505
|
+
clearEnginePidfile();
|
|
2506
|
+
clearEngineState();
|
|
2507
|
+
p.log.success(cands.size > 0 ? `stopped engine (${cands.size} pid${cands.size === 1 ? "" : "s"})` : "no engine running");
|
|
2508
|
+
} catch (err) {
|
|
2509
|
+
p.log.warn(`engine stop best-effort: ${err instanceof Error ? err.message : String(err)}`);
|
|
2510
|
+
}
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
if (!item.path) continue;
|
|
2514
|
+
const r = safeDelete(item.path);
|
|
2515
|
+
if (r.ok) p.log.success(r.message);
|
|
2516
|
+
else p.log.error(r.message);
|
|
2517
|
+
}
|
|
2518
|
+
p.outro("Done. agentmemory cleanly removed. The npm package itself: npm uninstall -g @agentmemory/agentmemory");
|
|
2519
|
+
}
|
|
1024
2520
|
({
|
|
1025
2521
|
init: runInit,
|
|
2522
|
+
connect: runConnectCmd,
|
|
1026
2523
|
status: runStatus,
|
|
1027
2524
|
doctor: runDoctor,
|
|
1028
2525
|
demo: runDemo,
|
|
1029
2526
|
upgrade: runUpgrade,
|
|
2527
|
+
stop: runStop,
|
|
2528
|
+
remove: runRemove,
|
|
1030
2529
|
mcp: runMcp,
|
|
1031
2530
|
"import-jsonl": runImportJsonl
|
|
1032
2531
|
}[args[0] ?? ""] ?? main)().catch((err) => {
|
|
@@ -1035,5 +2534,5 @@ async function runImportJsonl() {
|
|
|
1035
2534
|
});
|
|
1036
2535
|
|
|
1037
2536
|
//#endregion
|
|
1038
|
-
export {
|
|
2537
|
+
export { STREAM as a, jaccardSimilarity as c, KV as i, bootLog as n, fingerprintId as o, logger as r, generateId as s, VERSION as t };
|
|
1039
2538
|
//# sourceMappingURL=cli.mjs.map
|