@staix/agent-mem 0.11.0 → 0.13.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +51 -42
- package/package.json +4 -2
- package/src/cli-agent-reads.ts +105 -0
- package/src/cli.ts +220 -58
- package/src/commands-table.ts +9 -9
- package/src/index.ts +1 -0
- package/src/ingest.ts +21 -4
- package/src/mcp.ts +8 -8
- package/src/memory/candidates.ts +15 -13
- package/src/memory/capacity-split.ts +4 -1
- package/src/memory/capacity.ts +48 -4
- package/src/memory/cli-attribution.ts +23 -0
- package/src/memory/commands.ts +59 -11
- package/src/memory/consolidate.ts +3 -2
- package/src/memory/decay-review.ts +4 -4
- package/src/memory/doctor.ts +63 -10
- package/src/memory/metrics.ts +20 -8
- package/src/memory/migrate.ts +30 -0
- package/src/memory/note-add.ts +4 -4
- package/src/memory/prune.ts +28 -1
- package/src/memory/shard-migration.ts +38 -0
- package/src/memory/status-text.ts +47 -0
- package/src/memory/store.ts +5 -4
- package/src/memory/style.ts +40 -0
- package/src/memory/vault.ts +31 -0
- package/src/setup-toml.ts +5 -5
- package/src/setup.ts +130 -40
package/src/setup.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
+
import { cliAttribution } from "./memory/cli-attribution.ts";
|
|
2
|
+
import { configuredPolicy } from "./memory/config.ts";
|
|
1
3
|
import { SETUP_CONFIG_BYTES, updateTomlArray } from "./setup-toml.ts";
|
|
2
4
|
import { managedAfter, mcpRecord, putMcpRecord, restoreOwned, sameImage, type ConfigImage } from "./setup-owned.ts";
|
|
3
5
|
import { spawnSync } from "node:child_process";
|
|
4
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
7
|
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
8
|
import { homedir, tmpdir } from "node:os";
|
|
7
|
-
import { createInterface } from "node:readline/promises";
|
|
8
9
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { isDeepStrictEqual } from "node:util";
|
|
11
12
|
import { HOOK_EVENTS } from "./hook-contract.ts";
|
|
12
13
|
import { memoryHome } from "./memory/home.ts";
|
|
14
|
+
import { label, PLAIN, singleLine, type Style } from "./memory/style.ts";
|
|
13
15
|
import { canonicalPath, memoryGitEnv, memoryGitSync, processLiveness, processSignature, realPath } from "./memory/platform.ts";
|
|
14
16
|
|
|
15
17
|
export const SETUP_TARGETS = ["claude", "codex", "kimi", "pi", "qwen", "jcode", "agy"] as const;
|
|
16
18
|
/** Workspace-scoped Antigravity setup is always selected explicitly. */
|
|
17
19
|
export const SETUP_ALL_TARGETS = SETUP_TARGETS.filter(target => target !== "agy");
|
|
18
20
|
export type SetupTarget = typeof SETUP_TARGETS[number];
|
|
21
|
+
// Only recorded layout verification versions belong here; null means no version was recorded.
|
|
22
|
+
const SETUP_VERIFIED_VERSIONS: Readonly<Record<SetupTarget, string | null>> = {
|
|
23
|
+
claude: null, codex: "0.165.0", kimi: null, pi: null, qwen: "0.26.1", jcode: "0.99.1", agy: null,
|
|
24
|
+
};
|
|
25
|
+
interface ClientVersion { output: string | null; verified: string | null; matches: boolean | null }
|
|
19
26
|
type Env = Readonly<Record<string, string | undefined>>;
|
|
20
27
|
type Image = ConfigImage;
|
|
21
28
|
interface PlannedHook { event: string; command: string; timeout: number }
|
|
@@ -24,7 +31,7 @@ interface DirectoryIdentity { dev: number; ino: number }
|
|
|
24
31
|
interface Receipt { createdDirectory?: DirectoryIdentity; version: 1; target: SetupTarget; intent: string; phase: "prepared" | "applied"; changes: Change[] }
|
|
25
32
|
export interface SetupContext {
|
|
26
33
|
env?: Env; bun?: string; entry?: string; stdinTTY?: boolean; cwd?: string;
|
|
27
|
-
/**
|
|
34
|
+
/** @deprecated Retained for source compatibility; explicit apply/undo modes do not prompt. */
|
|
28
35
|
confirm?: (plan: SetupPlan) => Promise<boolean>;
|
|
29
36
|
/** Host-only race fixture, called after temporary-file fsync and before the final comparison/rename. */
|
|
30
37
|
beforePublish?: (file: string) => void;
|
|
@@ -33,7 +40,7 @@ export interface SetupContext {
|
|
|
33
40
|
}
|
|
34
41
|
export interface SetupPlan {
|
|
35
42
|
targets: SetupTarget[]; mode: "preview" | "apply" | "undo";
|
|
36
|
-
actions: { target: SetupTarget; files: string[]; receipt: string; cwd?: string; registration: string[] | null; lifecycle: string; available: boolean; hooks: PlannedHook[]; mcpChange: "add" | "replace" | "restore" | "unchanged"; commands: string[][]; state: string }[];
|
|
43
|
+
actions: { target: SetupTarget; version?: ClientVersion; files: string[]; receipt: string; cwd?: string; registration: string[] | null; lifecycle: string; available: boolean; hooks: PlannedHook[]; mcpChange: "add" | "replace" | "restore" | "unchanged"; commands: string[][]; state: string }[];
|
|
37
44
|
notes: string[];
|
|
38
45
|
}
|
|
39
46
|
const MAX = SETUP_CONFIG_BYTES, JOURNAL_MAX = 12 * MAX;
|
|
@@ -152,7 +159,8 @@ function context(options: SetupContext, workspace?: WorkspaceBinding) {
|
|
|
152
159
|
}
|
|
153
160
|
function command(target: SetupTarget, bun: string, entry: string): string[] | null {
|
|
154
161
|
if (target === "kimi" || target === "jcode" || target === "agy") return null;
|
|
155
|
-
return [target, "mcp", "add",
|
|
162
|
+
if (target === "qwen") return [target, "mcp", "add", "--scope", "user", "--transport", "stdio", "amem", bun, "--", "--smol", entry, "mcp"];
|
|
163
|
+
return [target, "mcp", "add", ...(target === "claude" ? ["--scope", "user", "--transport", "stdio"] : []), "amem", "--", bun, "--smol", entry, "mcp"];
|
|
156
164
|
}
|
|
157
165
|
function assertJcodeHome(env: Env): void {
|
|
158
166
|
if (env.JCODE_HOME !== undefined && !isAbsolute(env.JCODE_HOME)) throw Error("setup requires an absolute JCODE_HOME; unset it to use the default");
|
|
@@ -161,7 +169,7 @@ function assertJcodeRuntime(c: ReturnType<typeof context>): void {
|
|
|
161
169
|
assertJcodeHome(c.env);
|
|
162
170
|
if ([c.bun, c.entry].some(value => value.includes("${"))) throw Error("Jcode interpolates environment placeholders in MCP command paths; setup cannot register this runtime exactly");
|
|
163
171
|
}
|
|
164
|
-
function intent(c: ReturnType<typeof context>, target: SetupTarget): string { return digest([1, target, c.paths[target], c.bun, c.entry, ...(target === "agy" ? [c.workspace!.root] : [])]); }
|
|
172
|
+
function intent(c: ReturnType<typeof context>, target: SetupTarget): string { return digest([1, target, c.paths[target], c.bun, c.entry, ...(["codex", "kimi", "qwen"].includes(target) ? ["capture-json-v1"] : []), ...(target === "agy" ? [c.workspace!.root] : [])]); }
|
|
165
173
|
function journal(c: ReturnType<typeof context>, target: SetupTarget): string { return join(c.state, target === "agy" ? `agy-${digest(c.workspace!.root)}.json` : `${target}.json`); }
|
|
166
174
|
function receipt(c: ReturnType<typeof context>, target: SetupTarget, requireRuntime = false): Receipt | undefined {
|
|
167
175
|
const raw = read(journal(c, target), JOURNAL_MAX); if (raw.bytes === null) return;
|
|
@@ -184,7 +192,7 @@ function plannedHooks(c: ReturnType<typeof context>, target: SetupTarget): Plann
|
|
|
184
192
|
if (target === "jcode" || target === "agy") return [];
|
|
185
193
|
const launch = `${quote(c.bun)} --smol ${quote(c.entry)}`;
|
|
186
194
|
return target === "claude" ? HOOK_EVENTS.map(event => ({ event, command: `${launch} hook ${event}`, timeout: 1 }))
|
|
187
|
-
: [{ event: target === "pi" ? "session_shutdown" : "SessionEnd", command: `${launch} ingest ${target}`, timeout: target === "pi" ? 2.5 : 3 }];
|
|
195
|
+
: [{ event: target === "pi" ? "session_shutdown" : "SessionEnd", command: `${launch} ingest ${target}${target === "pi" ? "" : " --json"}`, timeout: target === "pi" ? 2.5 : 3 }];
|
|
188
196
|
}
|
|
189
197
|
const lifecycle = (target: SetupTarget) => target === "jcode" || target === "agy" ? "MCP only; native capture unavailable" : target === "claude" ? "seven bounded native hooks" : target === "pi" ? "session_shutdown incremental ingest" : "SessionEnd incremental ingest";
|
|
190
198
|
export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", options: SetupContext = {}): SetupPlan {
|
|
@@ -217,8 +225,43 @@ export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", o
|
|
|
217
225
|
const hasAmem = target === "jcode" || target === "agy" ? mcpRecord(target, original) !== undefined : Object.hasOwn(record(document[target === "codex" ? "mcp_servers" : "mcpServers"] ?? {}), "amem");
|
|
218
226
|
const unchanged = !!prior && prior.phase === "applied" && prior.intent === intent(c, target) && prior.changes.every((change, index) => { try { const live = read(change.path); restoreChange(prior, change, index, live); return managedAfter(target, index, change.before, change.after, live); } catch { return false; } });
|
|
219
227
|
const add = command(target, c.bun, c.entry), remove = [target, "mcp", "remove", ...(target === "claude" || target === "qwen" ? ["--scope", "user"] : []), "amem"];
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
const installed = available(c, target, options), version = installed && target !== "agy" && mode !== "undo" ? probeClientVersion(c, target, options) : undefined;
|
|
229
|
+
if (version?.matches === false) pluginNotes.push(`WARNING: ${target} version ${version.output}; layout verified on ${version.verified}. Apply remains guarded by native configuration validation.`);
|
|
230
|
+
return { target, available: installed, ...(version ? { version } : {}), files: c.paths[target], receipt: journal(c, target), ...(target === "agy" ? { cwd: c.workspace!.root } : {}), registration: mode === "undo" ? null : add, lifecycle: mode === "undo" ? "restore original MCP/hook files" : lifecycle(target), hooks: mode === "undo" || unchanged ? [] : plannedHooks(c, target), mcpChange: mode === "undo" ? prior ? "restore" : "unchanged" : unchanged ? "unchanged" : hasAmem ? "replace" : "add", commands: mode === "undo" || prior?.intent === intent(c, target) || !add || !installed ? [] : [...(hasAmem ? [remove] : []), add], state: prior && prior.intent !== intent(c, target) ? "runtime changed; managed entries will update" : prior?.phase ?? "not installed" };
|
|
231
|
+
}), notes: [...pluginNotes, ...(/\/[^/]*\d+\.\d+[^/]*\/(?:bin\/)?bun$/.test(c.bun) ? ["this bun path is version-specific; rerun setup --apply after changing Bun"] : []), "Preview only unless --apply or --undo is passed; explicit modes apply without a prompt.", "Native registration commands run in a private staging HOME; only the listed configuration files are published.", "Original file bytes restore exactly when untouched; otherwise undo preserves CLI-owned changes and restores only managed entries, refusing edits to those entries.", "No AGENTS.md, CLAUDE.md or rule files are edited.", `A private ${join(c.state, "setup.lock")} serializes mutations; temporary ${join(realPath(tmpdir()), "amem-setup-*")} staging homes are removed.`, "Codex requires its native /hooks review before newly registered hooks run; setup does not grant hook trust. Runtime registrations use the PATH-resolved absolute Bun path without resolving its stable symlink; setup all reports and skips missing clients."] };
|
|
232
|
+
}
|
|
233
|
+
/** Native probes and registration share one private HOME, cwd and configuration layout. */
|
|
234
|
+
function stagingEnvironment(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext, staging: string) {
|
|
235
|
+
const env: Record<string, string | undefined> = { ...c.env, HOME: staging, AMEM_HOME: join(staging, "memory"), AGENTHUB_HOME: join(staging, "hub"), CODEX_HOME: join(staging, ".codex"), KIMI_CODE_HOME: join(staging, ".kimi-code"), PI_CODING_AGENT_DIR: join(staging, ".pi", "agent"), QWEN_HOME: join(staging, ".qwen"), JCODE_HOME: join(staging, ".jcode") };
|
|
236
|
+
Object.assign(env, { XDG_CONFIG_HOME: join(staging, ".config"), XDG_DATA_HOME: join(staging, ".local", "share"), XDG_CACHE_HOME: join(staging, ".cache"), XDG_STATE_HOME: join(staging, ".local", "state"), TMPDIR: join(staging, "tmp"), TMP: join(staging, "tmp"), TEMP: join(staging, "tmp") });
|
|
237
|
+
env.QWEN_RUNTIME_DIR = join(staging, "qwen-runtime");
|
|
238
|
+
env.QWEN_CODE_SYSTEM_SETTINGS_PATH = join(staging, "qwen-system-settings.json");
|
|
239
|
+
env.QWEN_CODE_SYSTEM_DEFAULTS_PATH = join(staging, "qwen-system-defaults.json");
|
|
240
|
+
mkdirSync(env.TMPDIR!, { mode: 0o700 });
|
|
241
|
+
if (c.env.CLAUDE_CONFIG_DIR?.trim()) env.CLAUDE_CONFIG_DIR = join(staging, ".claude"); else delete env.CLAUDE_CONFIG_DIR;
|
|
242
|
+
// The staging workspace is a trusted private binding, not a Git repository or the real caller cwd.
|
|
243
|
+
const staged = context({ ...options, env, cwd: staging }, target === "agy" ? { root: staging, ignored: true, assertCurrent() {} } : undefined);
|
|
244
|
+
// Codex refuses an explicit CODEX_HOME that does not exist, even for mcp add.
|
|
245
|
+
mkdirSync(dirname(staged.paths[target][0]!), { recursive: true, mode: 0o700 });
|
|
246
|
+
return { env, staged };
|
|
247
|
+
}
|
|
248
|
+
/** Advisory only: never queried by apply/undo or persisted in the ownership receipt. */
|
|
249
|
+
function probeClientVersion(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext): ClientVersion {
|
|
250
|
+
const verified = SETUP_VERIFIED_VERSIONS[target];
|
|
251
|
+
let output: string | null = null;
|
|
252
|
+
// Like the help probe, trusted native fixtures do not invoke installed executables.
|
|
253
|
+
if (!options.native) {
|
|
254
|
+
let staging: string | undefined;
|
|
255
|
+
try {
|
|
256
|
+
staging = mkdtempSync(join(realPath(tmpdir()), "amem-setup-"));
|
|
257
|
+
const { env } = stagingEnvironment(c, target, options, staging);
|
|
258
|
+
const result = spawnSync(target, ["--version"], { cwd: staging, env: { ...env }, encoding: "utf8", timeout: 1000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"] });
|
|
259
|
+
if (result.status === 0 && !result.error) output = Array.from(singleLine(result.stdout).trim()).slice(0, 256).join("") || null;
|
|
260
|
+
} catch { /* An absent, failed or timed-out native version never prevents setup. */ }
|
|
261
|
+
finally { if (staging) try { rmSync(staging, { recursive: true, force: true }); } catch { output = null; } }
|
|
262
|
+
}
|
|
263
|
+
const observed = output?.match(/(?:^|\s)v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?=\s|$)/)?.[1];
|
|
264
|
+
return { output, verified, matches: observed === undefined || verified === null ? null : observed === verified };
|
|
222
265
|
}
|
|
223
266
|
function verifyNativeMcp(target: SetupTarget, env: Env, cwd: string): void {
|
|
224
267
|
// In older free-form Pi CLIs, an unknown subcommand can be treated as a model prompt.
|
|
@@ -227,7 +270,12 @@ function verifyNativeMcp(target: SetupTarget, env: Env, cwd: string): void {
|
|
|
227
270
|
}
|
|
228
271
|
function native(argv: readonly string[], env: Env, cwd: string): void {
|
|
229
272
|
const result = spawnSync(argv[0]!, argv.slice(1), { cwd, env: { ...env }, encoding: "utf8", timeout: 10_000, maxBuffer: MAX, stdio: ["ignore", "pipe", "pipe"] });
|
|
230
|
-
if (result.status !== 0)
|
|
273
|
+
if (result.status !== 0) {
|
|
274
|
+
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
|
|
275
|
+
const argumentsRejected = argv[0] === "qwen" && `${result.stderr ?? ""}\n${result.stdout ?? ""}`.includes("Not enough non-option arguments:");
|
|
276
|
+
const reason = timedOut ? "timed out" : argumentsRejected ? "rejected command arguments" : "failed";
|
|
277
|
+
throw Error(`${argv[0]} MCP registration ${reason} in staging; no native output disclosed`);
|
|
278
|
+
}
|
|
231
279
|
}
|
|
232
280
|
function hookDocument(before: Image, events: readonly string[], commandFor: (event: string) => string, timeout = 1): Image {
|
|
233
281
|
const document = json(before), hooks = document.hooks === undefined ? {} : record(document.hooks);
|
|
@@ -261,17 +309,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
261
309
|
const changes: Change[] = c.paths[target].map((path, i) => ({ key: `${target}:${i}`, path, before: restored[i]!, after: restored[i]!, start: expected[i]! }));
|
|
262
310
|
const staging = mkdtempSync(join(realPath(tmpdir()), "amem-setup-"));
|
|
263
311
|
try {
|
|
264
|
-
const
|
|
265
|
-
Object.assign(env, { XDG_CONFIG_HOME: join(staging, ".config"), XDG_DATA_HOME: join(staging, ".local", "share"), XDG_CACHE_HOME: join(staging, ".cache"), XDG_STATE_HOME: join(staging, ".local", "state"), TMPDIR: join(staging, "tmp"), TMP: join(staging, "tmp"), TEMP: join(staging, "tmp") });
|
|
266
|
-
env.QWEN_RUNTIME_DIR = join(staging, "qwen-runtime");
|
|
267
|
-
env.QWEN_CODE_SYSTEM_SETTINGS_PATH = join(staging, "qwen-system-settings.json");
|
|
268
|
-
env.QWEN_CODE_SYSTEM_DEFAULTS_PATH = join(staging, "qwen-system-defaults.json");
|
|
269
|
-
mkdirSync(env.TMPDIR!, { mode: 0o700 });
|
|
270
|
-
if (c.env.CLAUDE_CONFIG_DIR?.trim()) env.CLAUDE_CONFIG_DIR = join(staging, ".claude"); else delete env.CLAUDE_CONFIG_DIR;
|
|
271
|
-
// The staging workspace is a trusted private binding, not a Git repository or the real caller cwd.
|
|
272
|
-
const staged = context({ ...options, env, cwd: staging }, target === "agy" ? { root: staging, ignored: true, assertCurrent() {} } : undefined);
|
|
273
|
-
// Codex refuses an explicit CODEX_HOME that does not exist, even for mcp add.
|
|
274
|
-
mkdirSync(dirname(staged.paths[target][0]!), { recursive: true, mode: 0o700 });
|
|
312
|
+
const { env, staged } = stagingEnvironment(c, target, options, staging);
|
|
275
313
|
for (let i = 0; i < changes.length; i++) if (expected[i]!.bytes !== null) write(staged.paths[target][i]!, expected[i]!);
|
|
276
314
|
const argv = command(target, c.bun, c.entry);
|
|
277
315
|
if (argv && prior?.intent === intent(c, target)) {
|
|
@@ -284,16 +322,33 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
284
322
|
if (servers && Object.hasOwn(record(servers), "amem")) (options.native ?? native)([target, "mcp", "remove", ...(target === "claude" || target === "qwen" ? ["--scope", "user"] : []), "amem"], env, staging);
|
|
285
323
|
(options.native ?? native)(argv, env, staging);
|
|
286
324
|
changes[0]!.after = { ...read(staged.paths[target][0]!), mode: changes[0]!.before.mode };
|
|
287
|
-
if (changes[0]!.after.bytes === null) throw Error(
|
|
325
|
+
if (changes[0]!.after.bytes === null) throw Error(`${target} native registration produced no configuration`);
|
|
288
326
|
const installedText = Buffer.from(changes[0]!.after.bytes!, "base64").toString("utf8");
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
327
|
+
let installed: Record<string, unknown>, installedServer: Record<string, unknown>;
|
|
328
|
+
try {
|
|
329
|
+
installed = target === "codex" ? toml(installedText) : json(changes[0]!.after);
|
|
330
|
+
installedServer = record(record(installed[target === "codex" ? "mcp_servers" : "mcpServers"]).amem);
|
|
331
|
+
} catch { throw Error(`${target} native registration produced invalid configuration; no configuration published`); }
|
|
332
|
+
if (installedServer.command !== c.bun || JSON.stringify(installedServer.args) !== JSON.stringify(["--smol", c.entry, "mcp"]) || installedServer.enabled === false) throw Error(`${target} native registration did not produce the planned amem server`);
|
|
292
333
|
const serverKey = target === "codex" ? "mcp_servers" : "mcpServers";
|
|
293
334
|
const withoutAmem = (value: Record<string, unknown>) => { const copy = { ...value }, servers = { ...record(value[serverKey] ?? {}) }; delete servers.amem; copy[serverKey] = servers; return copy; };
|
|
294
|
-
|
|
335
|
+
const originalUnrelated = withoutAmem(document), installedUnrelated = withoutAmem(installed);
|
|
295
336
|
if (target === "codex") {
|
|
296
|
-
|
|
337
|
+
// Restore the omitted default only in this detached comparison, never in native output.
|
|
338
|
+
const originalServers = record(originalUnrelated[serverKey]), installedServers = record(installedUnrelated[serverKey]);
|
|
339
|
+
for (const [name, value] of Object.entries(originalServers)) {
|
|
340
|
+
if (!Object.hasOwn(installedServers, name)) continue;
|
|
341
|
+
const before = record(value), after = installedServers[name];
|
|
342
|
+
if (typeof before.command === "string" && Object.hasOwn(before, "args") && Array.isArray(before.args) && before.args.length === 0
|
|
343
|
+
&& after && typeof after === "object" && !Array.isArray(after) && !Object.hasOwn(after, "args")) {
|
|
344
|
+
installedServers[name] = { ...after, args: [] };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!isDeepStrictEqual(originalUnrelated[serverKey], installedUnrelated[serverKey])) throw Error(`${target} native registration changed unrelated MCP servers; no configuration published`);
|
|
349
|
+
if (target === "codex") {
|
|
350
|
+
if (!isDeepStrictEqual(originalUnrelated, installedUnrelated)) throw Error("codex native registration changed unrelated settings; no configuration published");
|
|
351
|
+
changes[0]!.after = putMcpRecord(target, changes[0]!.before, installedServer);
|
|
297
352
|
} else {
|
|
298
353
|
// Keep all unrelated JSON settings from the original; use only the CLI-generated server record.
|
|
299
354
|
// Qwen's restored image also retires its previous managed hook on a runtime upgrade.
|
|
@@ -311,7 +366,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
311
366
|
servers.amem = { command: c.bun, args: ["--smol", c.entry, "mcp"] }; document.mcpServers = servers;
|
|
312
367
|
changes[0]!.after = objectImage(document, changes[0]!.before);
|
|
313
368
|
}
|
|
314
|
-
const capture = (kind: string) => `${quote(c.bun)} --smol ${quote(c.entry)} ingest ${kind}`;
|
|
369
|
+
const capture = (kind: string) => `${quote(c.bun)} --smol ${quote(c.entry)} ingest ${kind} --json`;
|
|
315
370
|
if (target === "claude") changes[1]!.after = hookDocument(changes[1]!.before, HOOK_EVENTS, event => `${quote(c.bun)} --smol ${quote(c.entry)} hook ${event}`);
|
|
316
371
|
if (target === "codex") changes[1]!.after = hookDocument(changes[1]!.before, ["SessionEnd"], () => capture("codex"), 3);
|
|
317
372
|
// Qwen's user MCP and hooks share settings.json: publish and journal one combined image.
|
|
@@ -367,7 +422,7 @@ function contextForPlan(plan: SetupPlan, options: SetupContext) {
|
|
|
367
422
|
function publishGuard(c: ReturnType<typeof context>, target: SetupTarget, options: SetupContext) {
|
|
368
423
|
return target === "agy" ? (file: string) => { options.beforePublish?.(file); c.workspace!.assertCurrent(); } : options.beforePublish;
|
|
369
424
|
}
|
|
370
|
-
/**
|
|
425
|
+
/** CLI-owned unrelated fields remain live. */
|
|
371
426
|
function applyUnlocked(plan: SetupPlan, options: SetupContext = {}): SetupApplyResult {
|
|
372
427
|
const c = contextForPlan(plan, options), skipped: { target: SetupTarget; reason: string }[] = [];
|
|
373
428
|
const targets = plan.targets.filter(target => { if (available(c, target, options)) return true; if (plan.targets.length === 1) throw Error(`${target} CLI is not installed`); skipped.push({ target, reason: "CLI not installed" }); return false; });
|
|
@@ -425,16 +480,51 @@ export function undoSetup(plan: SetupPlan, options: SetupContext = {}): { restor
|
|
|
425
480
|
return withSetupLock(options, () => undoUnlocked(plan, options));
|
|
426
481
|
}
|
|
427
482
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
483
|
+
/** Person-facing setup plan (#125): each target once, with its effective files, MCP and hook changes, then skipped clients and notes. `--json` keeps the raw plan. */
|
|
484
|
+
export function setupPlanText(plan: SetupPlan, s: Style = PLAIN): string {
|
|
485
|
+
const verb = plan.mode === "undo" ? "undo" : plan.mode === "apply" ? "apply" : "preview";
|
|
486
|
+
// Undo restores every target with a receipt whether or not its CLI is still installed (undoUnlocked), so undo never collapses an action.
|
|
487
|
+
const full = (action: SetupPlan["actions"][number]) => plan.mode === "undo" || action.available;
|
|
488
|
+
const skipped = plan.actions.filter(action => !full(action)), changing = plan.actions.filter(action => full(action) && action.mcpChange !== "unchanged");
|
|
489
|
+
const lines = [`${s.bold(`amem setup ${verb}:`)} ${plan.targets.length} target${plan.targets.length === 1 ? "" : "s"}, ${changing.length} with ${plan.mode === "undo" ? "restores" : "MCP changes"}${skipped.length ? `, ${skipped.length} skipped` : ""}`, ""];
|
|
490
|
+
for (const action of plan.actions) {
|
|
491
|
+
if (!full(action)) { lines.push(`${label(s, "skipped")} ${s.bold(action.target)}: CLI not installed`); continue; }
|
|
492
|
+
const state = action.mcpChange === "unchanged" ? "ok" : "attention";
|
|
493
|
+
const what = action.mcpChange === "unchanged" ? plan.mode === "undo" ? "no setup receipt; nothing to restore" : "already set up; no change" : action.mcpChange === "restore" ? `restore original MCP/hook files${action.available ? "" : " (CLI not installed)"}` : `MCP ${action.mcpChange}`;
|
|
494
|
+
lines.push(`${label(s, state)} ${s.bold(action.target)}: ${what} ${s.dim(`(${action.state})`)}`);
|
|
495
|
+
if (action.version) {
|
|
496
|
+
const reported = action.version.output === null ? "unknown" : action.version.verified && action.version.matches === null ? `unrecognised (${action.version.output})` : action.version.output;
|
|
497
|
+
lines.push(` version ${reported}; ${action.version.verified ? `verified ${action.version.verified}` : "unverified (no verified version recorded)"}`);
|
|
498
|
+
}
|
|
499
|
+
for (const file of action.files) lines.push(` file ${file}`);
|
|
500
|
+
if (action.cwd) lines.push(` cwd ${action.cwd}`);
|
|
501
|
+
lines.push(` receipt ${action.receipt}`);
|
|
502
|
+
if (action.available && action.registration) lines.push(` command ${action.registration.map(quote).join(" ")}`);
|
|
503
|
+
for (const hook of action.hooks) lines.push(` hook ${hook.event}: ${hook.command} (timeout ${hook.timeout})`);
|
|
504
|
+
if (action.available && !action.hooks.length && plan.mode !== "undo") lines.push(` hook ${s.dim("none")}`);
|
|
505
|
+
lines.push(` ${s.dim(action.lifecycle)}`);
|
|
506
|
+
}
|
|
507
|
+
const warnings = plan.notes.filter(note => /^WARNING|detected|version-specific/.test(note)), notes = plan.notes.filter(note => !warnings.includes(note));
|
|
508
|
+
if (skipped.length) lines.push("", `Skipped clients: ${skipped.map(action => action.target).join(", ")} (not installed)`);
|
|
509
|
+
if (warnings.length) lines.push("", "Warnings:", ...warnings.map(note => ` - ${s.attention(note)}`));
|
|
510
|
+
lines.push("", "Notes:", ...notes.map(note => ` - ${note}`));
|
|
511
|
+
if (plan.mode === "preview") lines.push("", `Next: amem setup ${plan.targets.length === 1 ? plan.targets[0] : "all"} --apply writes without a prompt; --json prints the full plan`);
|
|
512
|
+
return lines.join("\n");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function setupResultText(result: SetupApplyResult | { restored: SetupTarget[] }, s: Style): string {
|
|
516
|
+
if ("restored" in result) return `${label(s, "ok")} restored: ${result.restored.join(", ") || "none"}`;
|
|
517
|
+
return [`${label(s, "ok")} applied: ${result.applied.join(", ") || "none"}`, `${label(s, "ok")} unchanged: ${result.unchanged.join(", ") || "none"}`,
|
|
518
|
+
...(result.skipped ?? []).map(item => `${label(s, "skipped")} ${item.target}: ${item.reason}`)].join("\n");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Compatibility formatter for confirmation-style callers: the raw plan under `--json`, the readable plan otherwise. */
|
|
522
|
+
export function setupConfirmationText(plan: SetupPlan, json: boolean, s: Style = PLAIN): string { return json ? JSON.stringify(plan, null, 2) : setupPlanText(plan, s); }
|
|
523
|
+
|
|
524
|
+
export async function runSetup(target: string, mode: SetupPlan["mode"], options: SetupContext & { json?: boolean; style?: Style } = {}): Promise<string> {
|
|
525
|
+
const plan = planSetup(target, mode, options), s = options.json ? PLAIN : options.style ?? PLAIN;
|
|
526
|
+
if (cliAttribution()?.actor === "agent" && !configuredPolicy(memoryHome(options.env ?? process.env), options.cwd ?? process.cwd()).screenStandalone(JSON.parse(JSON.stringify(plan)))) throw Error("Memory operation unavailable or refused; no partial result is shown.");
|
|
527
|
+
if (mode === "preview") return options.json ? JSON.stringify(plan, null, 2) : setupPlanText(plan, s);
|
|
528
|
+
const result = mode === "apply" ? applySetup(plan, options) : undoSetup(plan, options);
|
|
529
|
+
return options.json ? JSON.stringify(result, null, 2) : setupResultText(result, s);
|
|
440
530
|
}
|