@staix/agent-mem 0.7.0 → 0.9.1
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 +14 -4
- package/package.json +1 -1
- package/src/cli.ts +86 -17
- package/src/commands-table.ts +15 -9
- package/src/index.ts +6 -0
- package/src/mcp-memory.ts +28 -1
- package/src/mcp.ts +1 -0
- package/src/memory/candidates.ts +86 -2
- package/src/memory/capture.ts +1 -1
- package/src/memory/commands.ts +2 -2
- package/src/memory/consolidate.ts +78 -0
- package/src/memory/consolidation-clusters.ts +56 -0
- package/src/memory/consolidation-model.ts +97 -0
- package/src/memory/doctor.ts +11 -8
- package/src/memory/eval-fixture.ts +8 -0
- package/src/memory/eval-set-ko.json +90 -0
- package/src/memory/eval.ts +5 -3
- package/src/memory/evidence.ts +76 -0
- package/src/memory/external-import.ts +267 -0
- package/src/memory/import.ts +91 -0
- package/src/memory/platform.ts +17 -2
- package/src/memory/policy.ts +29 -9
- package/src/memory/project-export.ts +57 -22
- package/src/memory/project-import.ts +147 -0
- package/src/memory/shard-sync-format.ts +3 -4
- package/src/memory/shard-sync.ts +135 -17
- package/src/memory/store.ts +63 -9
- package/src/memory/sync-cadence.ts +15 -5
- package/src/memory/sync.ts +16 -4
- package/src/setup-toml.ts +5 -0
- package/src/setup.ts +6 -6
package/src/memory/store.ts
CHANGED
|
@@ -15,10 +15,10 @@ import { assertCuratedShardPaths, ensureShardDescriptor, readShardDescriptor, re
|
|
|
15
15
|
import { readCaptureConfig, type CaptureRow } from "./capture.ts";
|
|
16
16
|
import { assertSharedV1Op } from "./ops-contract.ts";
|
|
17
17
|
import { generationDescriptor, generationDir, fsyncDirectory, canonicalJSON, hashJSON, readProfileJSON, resolveProfile, writeProfileJSON, readSelector, type BoundProfile, type ProfileInput, validateVaultLinkRow, vaultLinkId, type VaultLink, type VaultLinkRow } from "./generation.ts";
|
|
18
|
-
import { GenerationIndex, explicitExpirySnapshot, GENERATION_TABLES } from "./generation-index.ts";
|
|
18
|
+
import { GenerationIndex, coalesceGenerationSource, explicitExpirySnapshot, GENERATION_TABLES } from "./generation-index.ts";
|
|
19
19
|
import { legacyEntries, readLegacySnapshot, SegmentReader, type LegacyCheckpoint } from "./bridge.ts";
|
|
20
20
|
import { readMigrationSources } from "./migrate.ts";
|
|
21
|
-
import type { PruneStorePort } from "./prune.ts";
|
|
21
|
+
import type { PruneSourceRow, PruneStorePort } from "./prune.ts";
|
|
22
22
|
import { isDeepStrictEqual } from "node:util";
|
|
23
23
|
import { registerReaderLease } from "./reader-lease.ts";
|
|
24
24
|
|
|
@@ -189,6 +189,8 @@ export interface StoreOptions { home?: string; memoryDir?: string; ttlMs?: numbe
|
|
|
189
189
|
lifecycle?: "enforce" | "ignore";
|
|
190
190
|
/** #27: read the `aliases` keys' curated rows as part of the `canonical` project (CLI and MCP only); the library default reads the raw key. Rows are presented under `canonical`. */
|
|
191
191
|
reconcile?: { canonical: string; aliases: readonly string[] };
|
|
192
|
+
/** Person-only project import may initialize configured alias shards; read-only reconciliation still never creates them. */
|
|
193
|
+
createAliases?: boolean;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
const MIGRATION_BOOTSTRAP = Symbol("migration index bootstrap");
|
|
@@ -382,6 +384,37 @@ export class MemoryStore {
|
|
|
382
384
|
return work(entries);
|
|
383
385
|
});
|
|
384
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Exact original transport placement; View.local describes tier, not a verified publication choice.
|
|
389
|
+
* ponytail: a legacy (shared-v1) home reads its whole snapshot under a 32 MiB byte budget on every call and refuses past it, so consolidation and its acceptance are unavailable for a legacy home that large. Upgrade: migrate the home to a generation (its placements come from the indexed `generation_rows`), or keep a placement index for legacy homes.
|
|
390
|
+
*/
|
|
391
|
+
entryPlacements(ids: readonly string[], project: string): ReadonlyMap<string, "local" | "synced"> {
|
|
392
|
+
if (!ids.length || ids.length > 256 || new Set(ids).size !== ids.length || ids.some(id => !/^[a-f0-9]{32}$/.test(id)) || !project.trim()) throw Error("invalid placement batch");
|
|
393
|
+
return this.withStoreFences(stores => {
|
|
394
|
+
const wanted = new Set(ids), found = new Map<string, "local" | "synced">(), reader = new SegmentReader(32 * 1024 * 1024);
|
|
395
|
+
const add = (id: string, placement: "local" | "synced") => { if (wanted.has(id) && (placement === "synced" || !found.has(id))) found.set(id, placement); };
|
|
396
|
+
assertProjectActive(this.profile.home, project);
|
|
397
|
+
for (const store of stores) {
|
|
398
|
+
if (store.generationIndex) {
|
|
399
|
+
store.generationIndex.refresh(0, reader);
|
|
400
|
+
const rows = store.db.query("SELECT id,local FROM generation_rows WHERE id IN (SELECT value FROM json_each(?))").all(JSON.stringify(ids)) as { id: string; local: number }[];
|
|
401
|
+
for (const row of rows) add(row.id, row.local ? "local" : "synced");
|
|
402
|
+
} else {
|
|
403
|
+
// Strict bounded verification includes locations, never guesses transport from tier or View.local.
|
|
404
|
+
const presence = new Map<string, PruneSourceRow>();
|
|
405
|
+
for (const segment of readLegacySnapshot(store.dir, { reader })) for (const row of legacyEntries([segment], { strict: true })) {
|
|
406
|
+
if (row.op === "note" && wanted.has(row.id)) {
|
|
407
|
+
const located = coalesceGenerationSource(presence.get(row.id), row, segment);
|
|
408
|
+
presence.set(row.id, located); add(row.id, located.placement);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
store.ensureFresh(); // Refresh only after the strict bounded source read succeeds.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const id of ids) { const view = this.get(id, project); if (!view || view.id !== id || view.project !== project || !found.has(id)) throw Error("entry placement unavailable"); }
|
|
415
|
+
return found;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
385
418
|
private withStoreFences<T>(work: (stores: readonly MemoryStore[]) => T): T {
|
|
386
419
|
const unique = new Map<string, MemoryStore>();
|
|
387
420
|
for (const store of this.readStores()) { const dir = realPath(store.dir); if (!unique.has(dir)) unique.set(dir, store); }
|
|
@@ -461,14 +494,19 @@ export class MemoryStore {
|
|
|
461
494
|
* An entry from another store with its own time (`ahub memory import claude-mem`): the same source row always gives
|
|
462
495
|
* the same id, so a second import writes nothing. Returns the id and whether it was new.
|
|
463
496
|
*/
|
|
464
|
-
importNote(n: NoteInput, at: string): { id: string; added: boolean } {
|
|
497
|
+
importNote(n: NoteInput, at: string, identity: { machine?: string; expectedId?: string } = {}): { id: string; added: boolean } {
|
|
465
498
|
if (this.bootstrapOnly) throw new Error("migration bootstrap cannot import memory");
|
|
466
|
-
if (this.userOverlay && n.scope === "shared") return this.userOverlay.importNote(n, at);
|
|
499
|
+
if (this.userOverlay && n.scope === "shared") return this.userOverlay.importNote(n, at, identity);
|
|
500
|
+
if (this.profile.sharded && !this.profile.shardUser && n.scope === "project" && n.project !== this.profile.shardKey) {
|
|
501
|
+
const owner = this.aliasStores.find(store => store.profile.shardKey === n.project);
|
|
502
|
+
if (owner) return owner.importNote(n, at, identity);
|
|
503
|
+
}
|
|
467
504
|
if (this.profile.sharded && (this.profile.shardUser ? n.scope !== "shared" : n.scope !== "project" || n.project !== this.profile.shardKey)) throw new Error("entry does not belong to this curated shard");
|
|
468
505
|
assertSharedV1Op(n.op);
|
|
469
|
-
// A fixed machine: the same observation imported on two machines is one entry once they sync.
|
|
470
|
-
const body = { schema: MEMORY_SCHEMA, at, machine: "claude-mem", ...n, text: stripPrivateSpans(n.text).slice(0, MAX_TEXT), ...(n.title ? { title: stripPrivateSpans(n.title).slice(0, MAX_TITLE) } : {}) } as Omit<Entry, "id">;
|
|
506
|
+
// A fixed machine: the same observation imported on two machines is one entry once they sync. A project import preserves the exported identity instead.
|
|
507
|
+
const body = { schema: MEMORY_SCHEMA, at, machine: identity.machine ?? "claude-mem", ...n, text: stripPrivateSpans(n.text).slice(0, MAX_TEXT), ...(n.title ? { title: stripPrivateSpans(n.title).slice(0, MAX_TITLE) } : {}) } as Omit<Entry, "id">;
|
|
471
508
|
const id = entryId(body);
|
|
509
|
+
if (identity.expectedId !== undefined && identity.expectedId !== id) throw new Error("project import entry identity changed");
|
|
472
510
|
return this.withMutation(() => {
|
|
473
511
|
this.generationIndex?.assertAppendable(n.tier === "short", this.machine.id);
|
|
474
512
|
// The source tuple also recognizes legacy ids whose nodes depended on the checkout or extractor version.
|
|
@@ -975,6 +1013,21 @@ export class MemoryStore {
|
|
|
975
1013
|
.sort((a, b) => b.at.localeCompare(a.at) || a.id.localeCompare(b.id)).slice(0, f.limit ?? 200);
|
|
976
1014
|
}
|
|
977
1015
|
|
|
1016
|
+
/** The verified authored log rows behind the current export selection, including legacy absent/null optional fields. */
|
|
1017
|
+
portableNotes(project: string): NoteEntry[] {
|
|
1018
|
+
const selected = this.list({ project, limit: Number.MAX_SAFE_INTEGER, includeSuperseded: true });
|
|
1019
|
+
const wanted = new Set(selected.map(view => view.id)), authored = new Map<string, NoteEntry>();
|
|
1020
|
+
for (const store of this.readStores()) {
|
|
1021
|
+
const rows = store.generationIndex ? store.generationIndex.snapshot(false).rows.map(item => item.row) : legacyEntries(readLegacySnapshot(store.dir));
|
|
1022
|
+
for (const row of rows) if (row.op === "note" && wanted.has(row.id) && !authored.has(row.id)) authored.set(row.id, row);
|
|
1023
|
+
}
|
|
1024
|
+
return selected.map(view => {
|
|
1025
|
+
const row = authored.get(view.id);
|
|
1026
|
+
if (!row) throw new Error("exported note's authored row is unavailable");
|
|
1027
|
+
return row;
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
978
1031
|
/** Exact-project body pages for policy-aware callers. Relations still fold over complete metadata. */
|
|
979
1032
|
readPage(window: ReadWindow & { query?: string; includeSuperseded?: boolean }): { rows: View[]; next?: ReadRef } {
|
|
980
1033
|
this.ensureFresh(); this.prune();
|
|
@@ -1507,12 +1560,13 @@ export function openShardStore(opts: StoreOptions & { project?: string; user?: b
|
|
|
1507
1560
|
const userOverlay = opts.user === true ? undefined : openShardStore({ ...plain, project: undefined, user: true });
|
|
1508
1561
|
const aliasStores: MemoryStore[] = [];
|
|
1509
1562
|
try {
|
|
1510
|
-
// #27:
|
|
1563
|
+
// #27: read-only reconciliation never creates an alias shard. A person-only project import explicitly can.
|
|
1511
1564
|
if (opts.user !== true && reconcile && reconcile.canonical === opts.project) {
|
|
1512
1565
|
for (const alias of reconcile.aliases) {
|
|
1513
|
-
if (alias === opts.project || !existsSync(join(shardDir(home, alias), "generation.json"))) continue;
|
|
1566
|
+
if (alias === opts.project || !opts.createAliases && !existsSync(join(shardDir(home, alias), "generation.json"))) continue;
|
|
1514
1567
|
assertCuratedShardPaths(home, { project: alias });
|
|
1515
|
-
aliasStores.push(
|
|
1568
|
+
aliasStores.push(opts.createAliases ? openShardStore({ ...plain, project: alias })
|
|
1569
|
+
: new MemoryStore(plain, { token: MIGRATION_BOOTSTRAP, profile: resolveShardProfile(home, { project: alias }), authoring: true }));
|
|
1516
1570
|
}
|
|
1517
1571
|
}
|
|
1518
1572
|
return new MemoryStore(opts, { token: MIGRATION_BOOTSTRAP, profile, authoring: true, userOverlay, aliasStores });
|
|
@@ -61,14 +61,24 @@ export const SYNC_IF_DUE_DEADLINE_MS = 30_000;
|
|
|
61
61
|
*/
|
|
62
62
|
export const STOP_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"] as const;
|
|
63
63
|
export type StopCause = "person" | "deadline";
|
|
64
|
-
export function stopSignal(ms: number, emitter: { on(name: string, fn: () => void): unknown; off(name: string, fn: () => void): unknown }, names: readonly string[] = STOP_SIGNALS): { signal: AbortSignal; dispose: () => void; cause: () => StopCause | undefined } {
|
|
64
|
+
export function stopSignal(ms: number, emitter: { on(name: string, fn: () => void): unknown; off(name: string, fn: () => void): unknown }, names: readonly string[] = STOP_SIGNALS): { signal: AbortSignal; dispose: () => void; cause: () => StopCause | undefined; signalName: () => string | undefined } {
|
|
65
65
|
const controller = new AbortController(), deadline = AbortSignal.timeout(ms);
|
|
66
|
-
let cause: StopCause | undefined;
|
|
66
|
+
let cause: StopCause | undefined, named: string | undefined;
|
|
67
67
|
// The first thing that ends the run is its cause: a person who closes the terminal after the deadline already fired did not stop it.
|
|
68
|
-
const
|
|
68
|
+
const handlers = names.map(name => [name, () => { if (cause === undefined) { cause = "person"; named = name; } controller.abort(); }] as const);
|
|
69
69
|
deadline.addEventListener("abort", () => { cause ??= "deadline"; }, { once: true });
|
|
70
|
-
for (const name of
|
|
71
|
-
return { signal: AbortSignal.any([controller.signal, deadline]), dispose: () => { for (const name of
|
|
70
|
+
for (const [name, handler] of handlers) emitter.on(name, handler);
|
|
71
|
+
return { signal: AbortSignal.any([controller.signal, deadline]), dispose: () => { for (const [name, handler] of handlers) emitter.off(name, handler); }, cause: () => cause, signalName: () => named };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* End this process the way the signal would have, once amem's own handlers are removed (a handler suppresses the default action, so a run a person ended would otherwise exit 1
|
|
76
|
+
* instead of 130, 143, 129). SIGQUIT's default action is a core dump, and Bun honours it: a re-raised SIGQUIT on Linux writes a core file the size of the process image (measured
|
|
77
|
+
* 5.9 GB, taking seconds, in a container with `ulimit -c unlimited`) while the shell waits for the exit, so it exits with the status that death would have had (128 + 3) at once.
|
|
78
|
+
*/
|
|
79
|
+
export function endLikeSignal(name: string): void {
|
|
80
|
+
if (name === "SIGQUIT") process.exit(128 + 3);
|
|
81
|
+
process.kill(process.pid, name as NodeJS.Signals);
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
/**
|
package/src/memory/sync.ts
CHANGED
|
@@ -45,13 +45,19 @@ async function git(dir: string, args: string[], signal?: AbortSignal, ms = STEP_
|
|
|
45
45
|
const unregister = group ? register(p.pid) : () => {};
|
|
46
46
|
// SIGTERM first, so git removes its lock files; SIGKILL only if it is still there after a grace period.
|
|
47
47
|
let hard: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
-
|
|
48
|
+
let signalled = false;
|
|
49
|
+
const kill = () => { signalled = true; send("SIGTERM"); hard ??= setTimeout(() => send("SIGKILL"), 2_000); };
|
|
49
50
|
const timer = setTimeout(kill, ms);
|
|
50
51
|
signal?.addEventListener("abort", kill, { once: true });
|
|
51
52
|
try {
|
|
52
53
|
const [code, out, err] = await Promise.all([p.exited, new Response(p.stdout).text(), new Response(p.stderr).text()]);
|
|
53
54
|
return { code, out: out.trim(), err: err.trim() };
|
|
54
|
-
} finally {
|
|
55
|
+
} finally {
|
|
56
|
+
// git is gone, but a transport child in its group that ignored SIGTERM (a credential helper, a proxy) may not be, and the timer that would have killed it is cleared below. Once the
|
|
57
|
+
// group was signalled, SIGKILL whatever is left of it (ESRCH when nothing is) before it is forgotten: it would otherwise live on at ppid 1 holding the repository.
|
|
58
|
+
if (group && signalled) try { process.kill(-p.pid, "SIGKILL"); } catch { /* the group is already gone */ }
|
|
59
|
+
unregister(); clearTimeout(timer); if (hard) clearTimeout(hard); signal?.removeEventListener("abort", kill);
|
|
60
|
+
}
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export { git as syncGit };
|
|
@@ -67,13 +73,19 @@ export async function generationSyncGit(dir: string, args: string[], signal?: Ab
|
|
|
67
73
|
const unregister = group ? register(p.pid) : () => {};
|
|
68
74
|
// SIGTERM first, so git removes its lock files; SIGKILL only if it is still there after a grace period.
|
|
69
75
|
let hard: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
-
|
|
76
|
+
let signalled = false;
|
|
77
|
+
const kill = () => { signalled = true; send("SIGTERM"); hard ??= setTimeout(() => send("SIGKILL"), 2_000); };
|
|
71
78
|
const timer = setTimeout(kill, ms);
|
|
72
79
|
signal?.addEventListener("abort", kill, { once: true });
|
|
73
80
|
try {
|
|
74
81
|
const [code, out, err] = await Promise.all([p.exited, new Response(p.stdout).text(), new Response(p.stderr).text()]);
|
|
75
82
|
return { code, out: out.trim(), err: err.trim() };
|
|
76
|
-
} finally {
|
|
83
|
+
} finally {
|
|
84
|
+
// git is gone, but a transport child in its group that ignored SIGTERM (a credential helper, a proxy) may not be, and the timer that would have killed it is cleared below. Once the
|
|
85
|
+
// group was signalled, SIGKILL whatever is left of it (ESRCH when nothing is) before it is forgotten: it would otherwise live on at ppid 1 holding the repository.
|
|
86
|
+
if (group && signalled) try { process.kill(-p.pid, "SIGKILL"); } catch { /* the group is already gone */ }
|
|
87
|
+
unregister(); clearTimeout(timer); if (hard) clearTimeout(hard); signal?.removeEventListener("abort", kill);
|
|
88
|
+
}
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
|
package/src/setup-toml.ts
CHANGED
|
@@ -263,6 +263,11 @@ export function updateTomlArray(text: string, path: readonly string[], expectedA
|
|
|
263
263
|
for (let index = 0; index < array.elements.length; index++) if (!keep.has(index)) {
|
|
264
264
|
const element = array.elements[index]!; changes.push({ start: element.removeFrom, end: element.end, text: "" });
|
|
265
265
|
if (element.comma !== undefined) changes.push({ start: element.comma, end: element.comma + 1, text: "" });
|
|
266
|
+
else {
|
|
267
|
+
// The last element has no comma of its own: drop the same-line comma that append inserted after the kept element before it, or `[a,]` remains. A comma followed by a comment or newline was the person's own and stays.
|
|
268
|
+
const before = [...keep].filter(kept => kept < index).at(-1), previous = before === undefined ? undefined : array.elements[before];
|
|
269
|
+
if (previous?.comma !== undefined && !append.length && /^[ \t]*$/.test(text.slice(previous.comma + 1, element.start))) changes.push({ start: previous.comma, end: previous.comma + 1, text: "" });
|
|
270
|
+
}
|
|
266
271
|
}
|
|
267
272
|
if (append.length) {
|
|
268
273
|
const last = [...keep].at(-1), element = last === undefined ? undefined : array.elements[last];
|
package/src/setup.ts
CHANGED
|
@@ -142,11 +142,11 @@ export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", o
|
|
|
142
142
|
const pluginNotes: string[] = [];
|
|
143
143
|
if (targets.includes("claude")) {
|
|
144
144
|
const settings = json(read(c.paths.claude[1]!));
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
// The settings-derived ids are computed outside the registry guard, so a broken installed_plugins.json cannot hide them.
|
|
146
|
+
const ids = new Set<string>();
|
|
147
|
+
try { for (const key of Object.keys(record(settings.enabledPlugins ?? {}))) ids.add(key); } catch { /* Optional plugin metadata must never prevent setup or recovery. */ }
|
|
148
|
+
try { for (const key of Object.keys(record(json(read(join(dirname(c.paths.claude[1]!), "plugins", "installed_plugins.json"))).plugins ?? {}))) ids.add(key); } catch { /* Optional plugin metadata must never prevent setup or recovery; authoritative settings/receipts keep their guards. */ }
|
|
149
|
+
for (const id of [...ids].filter(key => /^agent-mem(?:@[A-Za-z0-9._-]+)?$/.test(key))) pluginNotes.push(`Installed Claude plugin ${id} detected: if enabled, direct setup would duplicate its MCP server and seven hooks. Choose plugin installation or direct setup; disable/uninstall the plugin before applying direct setup.`);
|
|
150
150
|
}
|
|
151
151
|
return { targets, mode, actions: targets.map(target => {
|
|
152
152
|
const prior = receipt(c, target, false); for (const file of c.paths[target]) read(file);
|
|
@@ -156,7 +156,7 @@ export function planSetup(target: string, mode: SetupPlan["mode"] = "preview", o
|
|
|
156
156
|
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; } });
|
|
157
157
|
const add = command(target, c.bun, c.entry), remove = [target, "mcp", "remove", ...(target === "claude" ? ["--scope", "user"] : []), "amem"];
|
|
158
158
|
return { target, available: available(c, target, options), files: c.paths[target], receipt: journal(c, target), 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 || !available(c, target, options) ? [] : [...(hasAmem ? [remove] : []), add], state: prior && prior.intent !== intent(c, target) ? "runtime changed; managed entries will update" : prior?.phase ?? "not installed" };
|
|
159
|
-
}), notes: [...pluginNotes, ...(
|
|
159
|
+
}), 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 confirmed in a person's terminal.", "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."] };
|
|
160
160
|
}
|
|
161
161
|
function verifyNativeMcp(target: SetupTarget, env: Env, cwd: string): void {
|
|
162
162
|
// In older free-form Pi CLIs, an unknown subcommand can be treated as a model prompt.
|