@staix/agent-mem 0.20.1 → 0.20.3
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 +1 -1
- package/package.json +1 -1
- package/src/cli.ts +7 -1
- package/src/commands-table.ts +2 -1
- package/src/setup-paths.ts +4 -1
- package/src/setup.ts +74 -14
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Curated memory with append-only JSONL logs, a derived SQLite index, short- and long-term tiers, a typed graph, goals with leases, and git sync between machines. Extracted from [agent-hub](https://github.com/STAIxBWLB/agent-hub), with compatible storage and entry IDs.
|
|
4
4
|
|
|
5
|
-
Version: 0.20.
|
|
5
|
+
Version: 0.20.3. Requires Bun 1.4.2 or later; no runtime dependencies.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -248,6 +248,10 @@ function markedSetupApplyError(parsed: ReturnType<typeof parseCommand>, error: u
|
|
|
248
248
|
cause = "native_result_mismatch"; next = "Review the installed client's configuration format and version, then rerun setup preview."; partial = false;
|
|
249
249
|
} else if (text === "setup receipt version is unsupported; run setup or undo with the release that wrote it") {
|
|
250
250
|
cause = "setup_receipt_version_unsupported"; next = "Run setup or undo with the amem release that wrote the receipt."; partial = false;
|
|
251
|
+
} else if (text === "setup receipt was written from an Orca Claude profile; rerun setup from that Orca profile") {
|
|
252
|
+
cause = "setup_receipt_orca_profile"; next = "The receipt was written from an Orca Claude profile; rerun setup from that Orca profile."; partial = false;
|
|
253
|
+
} else if (text === "setup receipt is stale: its managed entries were replaced at another amem install root; rerun with --adopt-current to adopt the current wiring") {
|
|
254
|
+
cause = "setup_receipt_stale"; next = "The receipt's managed entries were replaced at another amem install root outside setup; rerun with --apply --adopt-current to keep the receipt aside and adopt the current wiring, which --undo then restores."; partial = false;
|
|
251
255
|
} else if (/^setup-owned .+ changed; refusing to overwrite it$/.test(text) || text === "Pi amem-session extension already exists and is not this setup's file"
|
|
252
256
|
|| text === "previous-runtime hook settings are customized; review hook configuration before retrying setup"
|
|
253
257
|
|| /^setup receipt (?:does not match this installation|target mismatch|invalid|directory ownership invalid)$/.test(text)) {
|
|
@@ -373,7 +377,8 @@ async function runCliCommand(argv: readonly string[], context: CliContext): Prom
|
|
|
373
377
|
const { definition, options, rawArgs: args } = parsed;
|
|
374
378
|
const identity = cliAttribution() ?? { actor: "person" as const, peer: "person" }, actor = identity.actor;
|
|
375
379
|
if (parsed.path[0] === "setup") {
|
|
376
|
-
if (parsed.positionals.length !== 1 || options["--apply"] === true && options["--undo"] === true
|
|
380
|
+
if (parsed.positionals.length !== 1 || options["--apply"] === true && options["--undo"] === true
|
|
381
|
+
|| options["--adopt-current"] === true && (options["--apply"] !== true || parsed.positionals[0] === "console")) throw new Error("usage: amem setup claude|codex|kimi|pi|qwen|jcode|opencode|gemini|agy|all|console [--root <abs path>] [--apply [--adopt-current]|--undo]");
|
|
377
382
|
if (parsed.positionals[0] === "console") {
|
|
378
383
|
const { consoleSetup, consoleSetupText } = await import("./setup-console.ts");
|
|
379
384
|
const mode = options["--apply"] === true ? "apply" : options["--undo"] === true ? "undo" : "preview";
|
|
@@ -385,6 +390,7 @@ async function runCliCommand(argv: readonly string[], context: CliContext): Prom
|
|
|
385
390
|
return runSetup(parsed.positionals[0]!, options["--apply"] === true ? "apply" : options["--undo"] === true ? "undo" : "preview", {
|
|
386
391
|
env: context.env ?? process.env, stdinTTY: context.stdinTTY ?? process.stdin.isTTY, cwd: context.cwd ?? process.cwd(),
|
|
387
392
|
...(typeof options["--root"] === "string" ? { root: options["--root"] } : {}),
|
|
393
|
+
...(options["--adopt-current"] === true ? { adoptCurrent: true } : {}),
|
|
388
394
|
...(context.setupConfirm ? { confirm: context.setupConfirm } : {}), json: options["--json"] === true, style: outputStyle(context),
|
|
389
395
|
});
|
|
390
396
|
}
|
package/src/commands-table.ts
CHANGED
|
@@ -26,7 +26,8 @@ export const ROOT_OPTIONS = { "--version": flag("Show version"), ...json };
|
|
|
26
26
|
export const COMMANDS: Record<string, CommandDefinition> = {
|
|
27
27
|
consolidate: { description: "Queue deterministic project-local consolidation candidates; curated notes change only after explicit acceptance", mutates: true, options: { ...json, "--project": text("The current project's key; another project is refused (run from its directory)") } },
|
|
28
28
|
setup: { description: "Preview reversible MCP setup and supported capture hooks for native coding CLIs, or the acon console plugin manifest (console); apply and undo use explicit flags", options: { ...json, "--apply": flag("Apply the previewed setup"), "--undo": flag("Restore the original configuration"),
|
|
29
|
-
"--root": { description: "Register this absolute install root (such as <install>/releases/<version>/, resolved to its real path) instead of the running one", value: "path" }
|
|
29
|
+
"--root": { description: "Register this absolute install root (such as <install>/releases/<version>/, resolved to its real path) instead of the running one", value: "path" },
|
|
30
|
+
"--adopt-current": flag("With --apply: keep a stale setup receipt aside (its entries were replaced at another install root) and adopt the current wiring, which --undo restores") }, args: [{ name: "target", values: ["claude", "codex", "kimi", "pi", "qwen", "jcode", "opencode", "gemini", "agy", "all", "console"] }] },
|
|
30
31
|
mcp: { description: "Run the project-bound MCP stdio server for this session" },
|
|
31
32
|
manifest: { description: "Describe this install: package name, version, MCP server and tools (read-only ones marked), commands", options: json },
|
|
32
33
|
hook: { description: "Run a fail-open, context-only Claude Code hook", args: [{ name: "event", values: [...HOOK_EVENTS] }] },
|
package/src/setup-paths.ts
CHANGED
|
@@ -67,7 +67,10 @@ export function resolveSetupPaths(env: SetupEnvironment = process.env, givenHome
|
|
|
67
67
|
paths[target][index] = to;
|
|
68
68
|
redirects.push({ target, from, to, reason: "Orca re-syncs this managed file from the personal source" });
|
|
69
69
|
};
|
|
70
|
-
if (orcaHome(claude, "claude", env, home))
|
|
70
|
+
if (orcaHome(claude, "claude", env, home)) {
|
|
71
|
+
redirect("claude", 0, join(home, ".claude.json"));
|
|
72
|
+
redirect("claude", 1, join(home, ".claude", "settings.json"));
|
|
73
|
+
}
|
|
71
74
|
if (orcaHome(codex, "codex", env, home)) {
|
|
72
75
|
redirect("codex", 0, join(home, ".codex", "config.toml"));
|
|
73
76
|
redirect("codex", 1, join(home, ".codex", "hooks.json"));
|
package/src/setup.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface SetupContext {
|
|
|
39
39
|
env?: Env; bun?: string; entry?: string; stdinTTY?: boolean; cwd?: string;
|
|
40
40
|
/** `--root`: an absolute amem install root to register instead of the running one (#199). Ignored when `entry` is given. */
|
|
41
41
|
root?: string;
|
|
42
|
+
/** `--adopt-current` (#241): with apply, set a stale receipt aside and adopt the live wiring as the new baseline. A receipt that is not stale is kept. */
|
|
43
|
+
adoptCurrent?: boolean;
|
|
42
44
|
/** @deprecated Retained for source compatibility; explicit apply/undo modes do not prompt. */
|
|
43
45
|
confirm?: (plan: SetupPlan) => Promise<boolean>;
|
|
44
46
|
/** Host-only race fixture, called after temporary-file fsync and before the final comparison/rename. */
|
|
@@ -214,6 +216,9 @@ function receipt(c: ReturnType<typeof context>, target: SetupTarget, requireRunt
|
|
|
214
216
|
if (value.target !== target || typeof value.intent !== "string" || !/^[a-f0-9]{64}$/.test(value.intent) || requireRuntime && value.intent !== intent(c, target) || !["prepared", "applied"].includes(String(value.phase)) || !Array.isArray(value.changes) || value.changes.length !== c.paths[target].length && !(target === "jcode" && value.changes.length === 1)) throw Error("setup receipt does not match this installation");
|
|
215
217
|
for (let i = 0; i < value.changes.length; i++) {
|
|
216
218
|
const change = record(value.changes[i]); const currentPath = c.paths[target][i], legacyRedirect = c.redirects.some(row => row.target === target && row.from === change.path && row.to === currentPath);
|
|
219
|
+
if (target === "claude" && i === 0 && typeof change.path === "string" && change.path !== currentPath && !legacyRedirect
|
|
220
|
+
&& resolveSetupPaths({ ...c.env, CLAUDE_CONFIG_DIR: dirname(change.path) }, c.home).redirects.some(row => row.from === change.path && row.to === currentPath))
|
|
221
|
+
throw Error("setup receipt was written from an Orca Claude profile; rerun setup from that Orca profile");
|
|
217
222
|
if (change.path !== currentPath && !legacyRedirect || change.key !== `${target}:${i}`) throw Error("setup receipt target mismatch");
|
|
218
223
|
for (const key of ["before", "after", ...(change.start !== undefined ? ["start"] : [])]) {
|
|
219
224
|
const v = record(change[key]);
|
|
@@ -268,8 +273,14 @@ export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", o
|
|
|
268
273
|
if (target === "all") pluginNotes.push("Antigravity is repository-scoped and excluded from all; run amem setup agy explicitly inside its repository.");
|
|
269
274
|
if (targets.includes("jcode")) pluginNotes.push("Launch Jcode with JCODE_DISABLE_CLAUDE_MCP=1 and without a same-name project override: imported Claude stdio entries can replace this private amem binding with a shared daemon-cwd server. Use a new workspace-bound session when changing projects.");
|
|
270
275
|
return { targets, mode, ...(c.root ? { root: c.root } : {}), actions: targets.map(target => {
|
|
271
|
-
const
|
|
276
|
+
const receipted = receipt(c, target, false), files = mode === "undo" && receipted ? receipted.changes.map(change => change.path) : c.paths[target];
|
|
272
277
|
for (const file of files) read(file);
|
|
278
|
+
const stale = mode !== "undo" && !!receipted && staleReceipt(target, receipted);
|
|
279
|
+
if (stale) pluginNotes.push(options.adoptCurrent
|
|
280
|
+
? `${target}: the setup receipt is stale (its managed entries were replaced at another amem install root outside setup). Apply keeps it as ${journal(c, target)}.stale-<UTC time> and adopts the current wiring; --undo then restores that wiring.`
|
|
281
|
+
: `${target}: the setup receipt is stale (its managed entries were replaced at another amem install root outside setup). Apply refuses; rerun with --apply --adopt-current to keep the receipt aside and adopt the current wiring, which --undo then restores.`);
|
|
282
|
+
// An adopted stale receipt plans like the receipt-less path that apply takes.
|
|
283
|
+
const prior = stale && options.adoptCurrent ? undefined : receipted;
|
|
273
284
|
if (mode === "undo" && prior?.changes.some((change, index) => change.path !== c.paths[target][index])) {
|
|
274
285
|
pluginNotes.push(`${target}: undo restores the historical receipt files, not the new personal source: ${prior.changes.map(change => change.path).join(", ")}`);
|
|
275
286
|
}
|
|
@@ -292,7 +303,7 @@ export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", o
|
|
|
292
303
|
const add = command(target, c.bun, c.entry), remove = target === "gemini" ? [...GEMINI_REMOVE] : [target, "mcp", "remove", ...(target === "claude" || target === "qwen" ? ["--scope", "user"] : []), "amem"];
|
|
293
304
|
const installed = available(c, target, options), version = installed && target !== "agy" && mode !== "undo" ? probeClientVersion(c, target, options) : undefined;
|
|
294
305
|
if (version?.matches === false) pluginNotes.push(`WARNING: ${target} version ${version.output}; layout verified on ${version.verified}. Apply remains guarded by native configuration validation.`);
|
|
295
|
-
return { target, available: installed, ...(version ? { version } : {}), files, 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" };
|
|
306
|
+
return { target, available: installed, ...(version ? { version } : {}), files, 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: stale ? options.adoptCurrent ? "stale receipt; current wiring will be adopted" : "stale receipt; apply needs --adopt-current" : prior && prior.intent !== intent(c, target) ? "runtime changed; managed entries will update" : prior?.phase ?? "not installed" };
|
|
296
307
|
}), 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."] };
|
|
297
308
|
}
|
|
298
309
|
/** Native probes and registration share one private HOME, cwd and configuration layout. */
|
|
@@ -390,16 +401,56 @@ function piExtension(bun: string, entry: string): string {
|
|
|
390
401
|
function previousRuntimes(target: SetupTarget, images: readonly Image[], bun: string, entry: string): { bun: string; entry: string }[] {
|
|
391
402
|
const found = new Map<string, { bun: string; entry: string }>();
|
|
392
403
|
for (const image of images) {
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
const server = value as Record<string, unknown>, args = server.args;
|
|
396
|
-
if (typeof server.command === "string" && isAbsolute(server.command) && Array.isArray(args) && args.length === 3
|
|
397
|
-
&& args[0] === "--smol" && typeof args[1] === "string" && isAbsolute(args[1]) && args[2] === "mcp"
|
|
398
|
-
&& (server.command !== bun || args[1] !== entry)) found.set(JSON.stringify([server.command, args[1]]), { bun: server.command, entry: args[1] });
|
|
404
|
+
const runtime = launcher(mcpRecord(target, image));
|
|
405
|
+
if (runtime && (runtime.bun !== bun || runtime.entry !== entry)) found.set(JSON.stringify([runtime.bun, runtime.entry]), runtime);
|
|
399
406
|
}
|
|
400
407
|
return [...found.values()];
|
|
401
408
|
}
|
|
402
|
-
|
|
409
|
+
/** The runtime of a generated `<bun> --smol <entry> mcp` server record, if the record is exactly that shape. */
|
|
410
|
+
function launcher(value: unknown): { bun: string; entry: string } | undefined {
|
|
411
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
412
|
+
const server = value as Record<string, unknown>, args = server.args;
|
|
413
|
+
return typeof server.command === "string" && isAbsolute(server.command) && Array.isArray(args) && args.length === 3
|
|
414
|
+
&& args[0] === "--smol" && typeof args[1] === "string" && isAbsolute(args[1]) && args[2] === "mcp" ? { bun: server.command, entry: args[1] } : undefined;
|
|
415
|
+
}
|
|
416
|
+
/** Generated capture commands of a prior runtime, current and pre-#145 (no `--json`) forms. */
|
|
417
|
+
const priorCaptures = (launches: readonly string[], kind: string) => launches.flatMap(launch => [`${launch} ingest ${kind} --json`, `${launch} ingest ${kind}`]);
|
|
418
|
+
const STALE_RECEIPT = "setup receipt is stale: its managed entries were replaced at another amem install root; rerun with --adopt-current to adopt the current wiring";
|
|
419
|
+
/**
|
|
420
|
+
* #241: the receipt cannot restore, none of its runtime's entries remain in its files, and the live MCP entry launches another install root.
|
|
421
|
+
* The wiring was moved outside setup, so the receipt no longer owns anything live. Any trace of the receipt's runtime keeps it authoritative.
|
|
422
|
+
*/
|
|
423
|
+
function staleReceipt(target: SetupTarget, prior: Receipt): boolean {
|
|
424
|
+
let files: Image[];
|
|
425
|
+
try {
|
|
426
|
+
files = prior.changes.map(change => read(change.path));
|
|
427
|
+
const owned = launcher(mcpRecord(target, prior.changes[0]!.after)), live = launcher(mcpRecord(target, files[0]!));
|
|
428
|
+
if (!owned || !live || live.entry === owned.entry) return false;
|
|
429
|
+
// Raw and shell-quoted entry, searched in the raw text (with JSON escaping, for the Pi extension source)
|
|
430
|
+
// and in every decoded JSON/TOML string value, so escapes such as `\/` cannot hide a live entry.
|
|
431
|
+
const traces = [owned.entry, quote(owned.entry)], escaped = traces.map(value => JSON.stringify(value).slice(1, -1));
|
|
432
|
+
if (files.some(file => file.bytes !== null && decodedTexts(Buffer.from(file.bytes, "base64").toString("utf8"))
|
|
433
|
+
.some((value, index) => (index === 0 ? [...traces, ...escaped] : traces).some(trace => value.includes(trace))))) return false;
|
|
434
|
+
} catch { return false; }
|
|
435
|
+
try { prior.changes.forEach((change, index) => restoreChange(prior, change, index, files[index]!)); return false; } catch { return true; }
|
|
436
|
+
}
|
|
437
|
+
/** The raw text first, then every string value it decodes to as JSON, else as TOML. */
|
|
438
|
+
function decodedTexts(raw: string): string[] {
|
|
439
|
+
const out = [raw], walk = (value: unknown): void => {
|
|
440
|
+
if (typeof value === "string") out.push(value);
|
|
441
|
+
else if (value && typeof value === "object") for (const item of Object.values(value)) walk(item);
|
|
442
|
+
};
|
|
443
|
+
try { walk(JSON.parse(raw)); } catch { try { walk(Bun.TOML.parse(raw)); } catch { /* Not JSON or TOML (the Pi extension): the raw text alone. */ } }
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
/** Keep a stale receipt beside the new one instead of deleting it; the person can inspect or restore it. */
|
|
447
|
+
function setReceiptAside(file: string): void {
|
|
448
|
+
const aside = `${file}.stale-${new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+/, "")}`;
|
|
449
|
+
names(aside);
|
|
450
|
+
if (lstatSync(aside, { throwIfNoEntry: false })) throw Error("setup is busy; retry after the active setup exits");
|
|
451
|
+
renameSync(file, aside); const parent = openSync(dirname(file), constants.O_RDONLY); try { fsyncSync(parent); } finally { closeSync(parent); }
|
|
452
|
+
}
|
|
453
|
+
interface Prepared { value: Receipt; expected: Image[]; unchanged: boolean; setAside?: string }
|
|
403
454
|
function restoreChange(value: Receipt, change: Change, index: number, live: Image): Image {
|
|
404
455
|
const start = value.phase === "prepared" && change.start && !sameImage(change.start, change.before) ? change.start : undefined;
|
|
405
456
|
if (start) {
|
|
@@ -414,10 +465,18 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
414
465
|
if (target === "jcode") assertJcodeRuntime(c);
|
|
415
466
|
if (target === "opencode") assertOpencodeLayout(c, "apply");
|
|
416
467
|
if (target === "gemini") assertGeminiHome(c.env);
|
|
417
|
-
|
|
468
|
+
let prior = receipt(c, target, false), setAside: string | undefined;
|
|
469
|
+
if (prior && staleReceipt(target, prior)) {
|
|
470
|
+
if (!options.adoptCurrent) throw Error(STALE_RECEIPT);
|
|
471
|
+
// Adopt the live wiring through the receipt-less path (#238); the receipt is set aside only when this target publishes.
|
|
472
|
+
setAside = journal(c, target); prior = undefined;
|
|
473
|
+
}
|
|
474
|
+
const expected = c.paths[target].map(path => read(path));
|
|
418
475
|
// Only a same-file before-image can restore a target. A newly authoritative personal source owns its current bytes.
|
|
419
476
|
// Mapped per current path: a pre-#203 jcode receipt covers mcp.json only, so config.toml starts from its live image.
|
|
420
477
|
const restored = c.paths[target].map((path, index) => { const change = prior?.changes[index];
|
|
478
|
+
// Validate ownership at the historical path before adopting a new source, without publishing to the copy.
|
|
479
|
+
if (prior && change && change.path !== path) restoreChange(prior, change, index, read(change.path));
|
|
421
480
|
return prior && change && change.path === path ? restoreChange(prior, change, index, expected[index]!) : expected[index]!; });
|
|
422
481
|
if (prior?.phase === "applied" && prior.intent === intent(c, target) && prior.changes.every((change, index) => managedAfter(target, index, change.before, change.after, expected[index]!))) return { value: prior, expected, unchanged: true };
|
|
423
482
|
const changes: Change[] = c.paths[target].map((path, i) => ({ key: `${target}:${i}`, path, before: restored[i]!, after: restored[i]!, start: expected[i]! }));
|
|
@@ -489,9 +548,9 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
489
548
|
}
|
|
490
549
|
const capture = (kind: string) => `${quote(c.bun)} --smol ${quote(c.entry)} ingest ${kind} --json`;
|
|
491
550
|
if (target === "claude") changes[1]!.after = hookDocument(changes[1]!.before, HOOK_EVENTS, event => `${quote(c.bun)} --smol ${quote(c.entry)} hook ${event}`, 1, event => oldLaunches.map(launch => `${launch} hook ${event}`));
|
|
492
|
-
if (target === "codex") changes[1]!.after = hookDocument(changes[1]!.before, ["SessionEnd"], () => capture("codex"), 3, () => oldLaunches
|
|
551
|
+
if (target === "codex") changes[1]!.after = hookDocument(changes[1]!.before, ["SessionEnd"], () => capture("codex"), 3, () => priorCaptures(oldLaunches, "codex"));
|
|
493
552
|
// Qwen's user MCP and hooks share settings.json: publish and journal one combined image.
|
|
494
|
-
if (target === "qwen") changes[0]!.after = hookDocument(changes[0]!.after, ["SessionEnd"], () => capture("qwen"), 3, () => oldLaunches
|
|
553
|
+
if (target === "qwen") changes[0]!.after = hookDocument(changes[0]!.after, ["SessionEnd"], () => capture("qwen"), 3, () => priorCaptures(oldLaunches, "qwen"));
|
|
495
554
|
if (target === "kimi") {
|
|
496
555
|
const old = changes[1]!.before.bytes === null ? "" : Buffer.from(changes[1]!.before.bytes!, "base64").toString("utf8");
|
|
497
556
|
const parsed = toml(old), rawHooks = parsed.hooks ?? [];
|
|
@@ -499,7 +558,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
499
558
|
let hooks: unknown[] = rawHooks;
|
|
500
559
|
if (oldLaunches.length) hooks = hooks.filter(h => {
|
|
501
560
|
const row = record(h);
|
|
502
|
-
if (row.event !== "SessionEnd" || !oldLaunches.
|
|
561
|
+
if (row.event !== "SessionEnd" || !priorCaptures(oldLaunches, "kimi").includes(String(row.command))) return true;
|
|
503
562
|
if (![undefined, "", "*"].includes(row.matcher as string | undefined) || row.timeout !== 3
|
|
504
563
|
|| Object.keys(row).some(key => !["event", "command", "matcher", "timeout"].includes(key)))
|
|
505
564
|
throw Error("previous-runtime hook settings are customized; review hook configuration before retrying setup");
|
|
@@ -517,7 +576,7 @@ function prepare(c: ReturnType<typeof context>, target: SetupTarget, options: Se
|
|
|
517
576
|
}
|
|
518
577
|
// Never publish an image that this installer's own read/undo boundary cannot reopen.
|
|
519
578
|
if (changes.some(change => change.after.bytes !== null && Buffer.from(change.after.bytes, "base64").byteLength > MAX)) throw Error("planned setup configuration exceeds the size bound; no configuration published");
|
|
520
|
-
return { value: { version: previous.length && changes.some((change, index) => retiresOriginalHook(target, index, change.before, change.after)) ? 2 : 1, target, intent: intent(c, target), phase: "prepared", changes, ...(prior?.createdDirectory ? { createdDirectory: prior.createdDirectory } : {}) }, expected, unchanged: false };
|
|
579
|
+
return { value: { version: previous.length && changes.some((change, index) => retiresOriginalHook(target, index, change.before, change.after)) ? 2 : 1, target, intent: intent(c, target), phase: "prepared", changes, ...(prior?.createdDirectory ? { createdDirectory: prior.createdDirectory } : {}) }, expected, unchanged: false, ...(setAside ? { setAside } : {}) };
|
|
521
580
|
} finally { rmSync(staging, { recursive: true, force: true }); }
|
|
522
581
|
}
|
|
523
582
|
function save(c: ReturnType<typeof context>, value: Receipt): void { write(journal(c, value.target), image(JSON.stringify(value) + "\n")); }
|
|
@@ -565,6 +624,7 @@ function applyUnlocked(plan: SetupPlan, options: SetupContext = {}): SetupApplyR
|
|
|
565
624
|
const value = item.value;
|
|
566
625
|
if (value.target === "agy") c.workspace!.assertCurrent();
|
|
567
626
|
if (item.unchanged) { unchanged.push(value.target); continue; }
|
|
627
|
+
if (item.setAside) setReceiptAside(item.setAside);
|
|
568
628
|
save(c, value);
|
|
569
629
|
prepareCreatedDirectory(c, value);
|
|
570
630
|
for (let i = 0; i < value.changes.length; i++) { const change = value.changes[i]!; if (!sameImage(item.expected[i]!, change.after)) write(change.path, change.after, item.expected[i]!, publishGuard(c, value.target, options)); }
|