minnimemory 1.0.0-beta.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.
Files changed (59) hide show
  1. package/LICENSE +39 -0
  2. package/README.md +824 -0
  3. package/dist/bench.d.ts +98 -0
  4. package/dist/bench.js +142 -0
  5. package/dist/benchReport.d.ts +12 -0
  6. package/dist/benchReport.js +128 -0
  7. package/dist/bounds.d.ts +40 -0
  8. package/dist/bounds.js +44 -0
  9. package/dist/cli.d.ts +15 -0
  10. package/dist/cli.js +503 -0
  11. package/dist/compile.d.ts +187 -0
  12. package/dist/compile.js +516 -0
  13. package/dist/discover.d.ts +125 -0
  14. package/dist/discover.js +520 -0
  15. package/dist/doctor.d.ts +9 -0
  16. package/dist/doctor.js +67 -0
  17. package/dist/episodic.d.ts +47 -0
  18. package/dist/episodic.js +130 -0
  19. package/dist/hook.d.ts +45 -0
  20. package/dist/hook.js +104 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +18 -0
  23. package/dist/init.d.ts +125 -0
  24. package/dist/init.js +475 -0
  25. package/dist/instructions.d.ts +60 -0
  26. package/dist/instructions.js +270 -0
  27. package/dist/mcp.d.ts +109 -0
  28. package/dist/mcp.js +252 -0
  29. package/dist/mcpServer.d.ts +136 -0
  30. package/dist/mcpServer.js +997 -0
  31. package/dist/paths.d.ts +25 -0
  32. package/dist/paths.js +47 -0
  33. package/dist/recall.d.ts +113 -0
  34. package/dist/recall.js +256 -0
  35. package/dist/recallDir.d.ts +50 -0
  36. package/dist/recallDir.js +187 -0
  37. package/dist/reorganize.d.ts +62 -0
  38. package/dist/reorganize.js +216 -0
  39. package/dist/report.d.ts +16 -0
  40. package/dist/report.js +204 -0
  41. package/dist/router.d.ts +141 -0
  42. package/dist/router.js +314 -0
  43. package/dist/rules.d.ts +32 -0
  44. package/dist/rules.js +651 -0
  45. package/dist/scan.d.ts +110 -0
  46. package/dist/scan.js +173 -0
  47. package/dist/text.d.ts +158 -0
  48. package/dist/text.js +395 -0
  49. package/dist/tokenizer.d.ts +26 -0
  50. package/dist/tokenizer.js +69 -0
  51. package/dist/types.d.ts +156 -0
  52. package/dist/types.js +17 -0
  53. package/dist/version.d.ts +7 -0
  54. package/dist/version.js +7 -0
  55. package/dist/writeProtocol.d.ts +19 -0
  56. package/dist/writeProtocol.js +45 -0
  57. package/examples/CLAUDE.md +75 -0
  58. package/examples/README.md +7 -0
  59. package/package.json +52 -0
package/dist/init.js ADDED
@@ -0,0 +1,475 @@
1
+ /**
2
+ * `init`: compile a memory file into .minnimemory/ and rewrite the host file as a stub.
3
+ * `init --update`: recompile an already-compiled workspace from its current pieces.
4
+ *
5
+ * Dry-run by default. Nothing is written unless `write` is set, and the original is copied
6
+ * verbatim into .minnimemory/original/ before any host file is touched.
7
+ *
8
+ * Safety (security audit 2026-09-02): the source and every path written are checked with
9
+ * `lstat` and refused when they are symlinks, because a cloned repo can ship a link that
10
+ * points at a private file; and a source holding a credential-shaped string is refused unless
11
+ * the user explicitly allows copying it into the compiled tree.
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { buildManifest, compile, DEFAULT_BUDGET, isStub, STUB_MARKER, COMPILED_DIR_NAME, ALWAYS_ON_FILE_NAME, ON_DEMAND_DIR_NAME } from "./compile.js";
16
+ import { discover, HOST_FILES, isPlainFile } from "./discover.js";
17
+ import { episodicJsonToMarkdown } from "./episodic.js";
18
+ import { containedIn } from "./paths.js";
19
+ import { loadManifest, McpTargetError } from "./recall.js";
20
+ import { WRITE_PROTOCOL_FILE_NAME } from "./writeProtocol.js";
21
+ import { RULES, secretFindings } from "./rules.js";
22
+ import { bounds } from "./bounds.js";
23
+ import { generatedRegion, LIST_END, LIST_START, INSTRUCTIONS_END, INSTRUCTIONS_START, keywords, sections, splitFrontmatter, } from "./text.js";
24
+ import { estimateTokens, formatTokens, TOKENIZER_ID } from "./tokenizer.js";
25
+ import { DEFAULT_CONFIG } from "./types.js";
26
+ export const COMPILED_DIR = COMPILED_DIR_NAME;
27
+ export class InitError extends Error {
28
+ }
29
+ function resolveProfile(choice, sourceTokens, budget) {
30
+ if (choice !== "auto")
31
+ return { name: choice };
32
+ if (sourceTokens < budget) {
33
+ return {
34
+ name: "none",
35
+ note: `auto: source is under the ${formatTokens(budget)} token budget, so no discipline block is embedded`,
36
+ };
37
+ }
38
+ return { name: "routing", note: `auto: source is over the ${formatTokens(budget)} token budget` };
39
+ }
40
+ /**
41
+ * Find the memory file to compile: an explicit file, or the host file in a repo.
42
+ *
43
+ * `includeAutoMemory` defaults true (the CLI's existing two-location behavior); the MCP `plan`/
44
+ * `apply` tools pass false unless launched with `--follow-external-imports`, so a directory
45
+ * target never triggers a read of the operator's OS-level auto-memory folder just to locate the
46
+ * host file - the auto-memory files are never selected here anyway (tagged `onDemandList`/`loose`,
47
+ * never `host`), but reading them at all violates "confined to this server's root" (2026-09-10 audit,
48
+ * second pass: the first fix left this path unaudited on the mistaken belief it only ever saw a
49
+ * single file).
50
+ */
51
+ function locateSource(target, includeAutoMemory = true) {
52
+ const resolved = path.resolve(target);
53
+ if (fs.existsSync(resolved) && fs.lstatSync(resolved).isFile()) {
54
+ return { root: path.dirname(resolved), abs: resolved, rel: path.basename(resolved) };
55
+ }
56
+ if (fs.existsSync(resolved) && fs.lstatSync(resolved).isSymbolicLink()) {
57
+ throw new InitError(`${resolved} is a symlink; init only compiles regular files`);
58
+ }
59
+ const ws = discover(resolved, { includeAutoMemory });
60
+ const host = ws.files.find((f) => f.kind === "host");
61
+ if (host && ws.pointerTarget) {
62
+ const abs = path.join(ws.root, ws.pointerTarget);
63
+ if (!isPlainFile(abs))
64
+ throw new InitError(`${ws.pointerTarget} is not a regular file`);
65
+ return { root: ws.root, abs, rel: ws.pointerTarget, pointerFrom: host.rel };
66
+ }
67
+ if (host)
68
+ return { root: ws.root, abs: host.abs, rel: host.rel };
69
+ throw new InitError(`nothing to compile in ${resolved}\n` +
70
+ ` init needs a host memory file (${HOST_FILES.join(", ")}) or an explicit file path`);
71
+ }
72
+ const RE_LIST_LINE = /^\s*[-*+]\s*\[[^\]]+\]\(([^)\s]+\.md)\)/i;
73
+ /**
74
+ * Refuse to compile a memory OnDemandMemory list. `init` compiles one host file into
75
+ * AlwaysOnMemory and OnDemandMemory files; a list (Claude Code's `MEMORY.md`, or any file that
76
+ * is mostly links to sibling topic files) routes to files that already exist. Compiling one
77
+ * re-files its hook lines as content and orphans every topic file: found live on 2026-09-03
78
+ * against a real auto-memory folder, where `init --write` mangled the list and the only guard
79
+ * was a note in the maintainer's memory. There is no override flag on purpose. A memory
80
+ * directory is `doctor`'s and the reorganizer's job (`mcp --allow-write`, scan then reorganize,
81
+ * backup first).
82
+ */
83
+ function refuseList(abs, rel, source) {
84
+ const dir = path.dirname(abs);
85
+ const named = path.basename(abs).toLowerCase() === "memory.md";
86
+ const lines = source.split(/\r?\n/).filter((l) => l.trim() !== "" && !/^\s*#/.test(l));
87
+ let links = 0;
88
+ for (const l of lines) {
89
+ const m = RE_LIST_LINE.exec(l);
90
+ if (m && fs.existsSync(path.join(dir, m[1])))
91
+ links++;
92
+ }
93
+ const mostlyLinks = links >= 3 && links * 2 >= lines.length;
94
+ if (!named && !mostlyLinks)
95
+ return;
96
+ const relDir = path.relative(process.cwd(), dir);
97
+ const shown = relDir === "" ? "." : relDir.startsWith("..") ? dir : relDir;
98
+ throw new InitError(`${rel} is a memory OnDemandMemory list, not a memory file\n` +
99
+ ` init compiles one host file into AlwaysOnMemory and OnDemandMemory files; a list routes\n` +
100
+ ` to topic files that already exist beside it. Compiling it would re-file its hook lines\n` +
101
+ ` as content and orphan every topic file, so init refuses. For a memory directory:\n` +
102
+ ` minnimemory doctor "${shown}" audit it, change nothing\n` +
103
+ ` minnimemory mcp "${shown}" --allow-write scan, then reorganize (agent-driven, backup first)`);
104
+ }
105
+ function secretsIn(rel, source) {
106
+ return secretFindings({
107
+ abs: rel,
108
+ rel,
109
+ content: source,
110
+ lines: source.split(/\r?\n/),
111
+ tokens: 0,
112
+ alwaysLoaded: true,
113
+ kind: "host",
114
+ });
115
+ }
116
+ function refuseSymlink(abs, what) {
117
+ try {
118
+ if (fs.lstatSync(abs).isSymbolicLink()) {
119
+ throw new InitError(`${what} is a symlink (${abs}); refusing to read or write through it`);
120
+ }
121
+ }
122
+ catch (err) {
123
+ if (err instanceof InitError)
124
+ throw err;
125
+ // does not exist yet: fine
126
+ }
127
+ }
128
+ /** The gitignore dropped into original/: the backup is local safety, not repo content. */
129
+ const ORIGINAL_GITIGNORE = "# minnimemory keeps a verbatim copy of the pre-compile file here. Local only.\n*\n!.gitignore\n";
130
+ export function planInit(target, options) {
131
+ if (options.update)
132
+ return planUpdate(target, options);
133
+ const { root, abs, rel, pointerFrom } = locateSource(target, options.includeAutoMemory !== false);
134
+ refuseSymlink(abs, rel);
135
+ const source = fs.readFileSync(abs, "utf8");
136
+ refuseList(abs, rel, source);
137
+ if (isStub(source)) {
138
+ throw new InitError(`${rel} is already a minnimemory stub\n` +
139
+ ` to recompile a compiled workspace run: minnimemory init --update`);
140
+ }
141
+ const compiledDir = path.join(root, COMPILED_DIR);
142
+ refuseSymlink(compiledDir, COMPILED_DIR);
143
+ const existing = fs.existsSync(compiledDir);
144
+ if (existing && !options.force) {
145
+ throw new InitError(`${COMPILED_DIR}/ already exists in ${root}\n` + ` pass --force to overwrite it, or --update to recompile it`);
146
+ }
147
+ const budget = options.budget ?? DEFAULT_BUDGET;
148
+ const { name: profile, note: profileNote } = resolveProfile(options.profile ?? "auto", estimateTokens(source), budget);
149
+ const compiled = compile(source, { sourceName: rel, profileName: profile, budget, episodicJson: options.episodicJson });
150
+ const manifest = buildManifest(compiled, TOKENIZER_ID);
151
+ const secrets = secretsIn(rel, source);
152
+ const files = [
153
+ { rel: `${COMPILED_DIR}/original/${rel}.bak`, contents: source, action: "backup" },
154
+ { rel: `${COMPILED_DIR}/original/.gitignore`, contents: ORIGINAL_GITIGNORE, action: existing ? "overwrite" : "create" },
155
+ { rel: `${COMPILED_DIR}/${ALWAYS_ON_FILE_NAME}`, contents: compiled.alwaysOn, action: existing ? "overwrite" : "create" },
156
+ {
157
+ rel: `${COMPILED_DIR}/manifest.json`,
158
+ contents: `${JSON.stringify(manifest, null, 2)}\n`,
159
+ action: existing ? "overwrite" : "create",
160
+ },
161
+ ];
162
+ for (const m of compiled.onDemandFiles) {
163
+ files.push({
164
+ rel: `${COMPILED_DIR}/${m.file}`,
165
+ contents: `${m.content.trimEnd()}\n`,
166
+ action: existing ? "overwrite" : "create",
167
+ });
168
+ }
169
+ files.push({ rel, contents: compiled.stub, action: "overwrite" });
170
+ const sessionBounds = bounds(manifest, VERDICT_TURNS);
171
+ const verdict = compiled.after < compiled.before ? "compile" : "leave";
172
+ const advice = adviceOn(abs);
173
+ return {
174
+ root,
175
+ sourceRel: rel,
176
+ compiled,
177
+ files,
178
+ existing,
179
+ profile,
180
+ profileNote,
181
+ pointerFrom,
182
+ secrets,
183
+ bounds: sessionBounds,
184
+ verdict,
185
+ advice,
186
+ };
187
+ }
188
+ /** Session length the plan's bounds are quoted over; the same default bench uses. */
189
+ const VERDICT_TURNS = 50;
190
+ /** Rules whose findings init cannot resolve, because resolving them would delete content. */
191
+ const ADVICE_RULES = new Set(["MM002", "MM005"]);
192
+ function adviceOn(abs) {
193
+ const workspace = discover(abs);
194
+ const ctx = { workspace, config: DEFAULT_CONFIG };
195
+ return RULES.filter((r) => ADVICE_RULES.has(r.id)).flatMap((r) => r.run(ctx));
196
+ }
197
+ /**
198
+ * `init --update`. After the first compile the source of truth is no longer the original file:
199
+ * it is the stub's own AlwaysOnMemory text plus the OnDemandMemory files on disk, both of which
200
+ * the user (and the agent) edit directly. So an update does not need a three-way merge against
201
+ * the original. It reads the stub, strips the generated regions, recompiles what is left
202
+ * (anything the user appended to the stub gets routed like a fresh section), keeps every
203
+ * OnDemandMemory file verbatim, recomputes triggers and hashes, and rewrites the OnDemandMemory
204
+ * list, manifest, and stub.
205
+ */
206
+ export function planUpdate(target, options) {
207
+ const resolved = path.resolve(target);
208
+ const root = fs.existsSync(resolved) && fs.lstatSync(resolved).isFile() ? path.dirname(resolved) : resolved;
209
+ const compiledDir = path.join(root, COMPILED_DIR);
210
+ refuseSymlink(compiledDir, COMPILED_DIR);
211
+ if (!fs.existsSync(path.join(compiledDir, "manifest.json"))) {
212
+ throw new InitError(`no compiled workspace at ${root}\n --update needs ${COMPILED_DIR}/manifest.json; run init first`);
213
+ }
214
+ let manifest;
215
+ try {
216
+ manifest = loadManifest(root);
217
+ }
218
+ catch (err) {
219
+ if (err instanceof McpTargetError)
220
+ throw new InitError(err.message);
221
+ throw err;
222
+ }
223
+ const rel = manifest.generatedFrom;
224
+ const hostAbs = path.join(root, rel);
225
+ refuseSymlink(hostAbs, rel);
226
+ if (!fs.existsSync(hostAbs))
227
+ throw new InitError(`${rel} named in the manifest is missing`);
228
+ const stub = fs.readFileSync(hostAbs, "utf8");
229
+ if (!isStub(stub)) {
230
+ throw new InitError(`${rel} is not a minnimemory stub; run init (with --force) to compile it from scratch`);
231
+ }
232
+ // Strip the generated parts. What remains is the user's AlwaysOnMemory text plus anything appended.
233
+ const lines = stub.split(/\r?\n/);
234
+ const { bodyStartLine } = splitFrontmatter(lines);
235
+ const drop = new Set();
236
+ drop.add(bodyStartLine - 1);
237
+ if ((lines[bodyStartLine] ?? "").trim().startsWith("<!-- generated by minnimemory"))
238
+ drop.add(bodyStartLine);
239
+ for (const [s, e] of [
240
+ [INSTRUCTIONS_START, INSTRUCTIONS_END],
241
+ [LIST_START, LIST_END],
242
+ ]) {
243
+ const r = generatedRegion(lines, s, e);
244
+ if (r)
245
+ for (let i = r.from; i <= r.to; i++)
246
+ drop.add(i);
247
+ }
248
+ const userSource = lines.filter((_, i) => !drop.has(i)).join("\n");
249
+ const budget = options.budget ?? DEFAULT_BUDGET;
250
+ const { name: profile, note: profileNote } = resolveProfile(options.profile ?? "auto", estimateTokens(userSource), budget);
251
+ const fresh = compile(userSource, { sourceName: rel, profileName: profile, budget, episodicJson: options.episodicJson });
252
+ // OnDemandMemory files on disk are kept verbatim. Manifest order first, new files after, then
253
+ // any OnDemandMemory files the recompile produced from content appended to the stub.
254
+ const onDemandDir = path.join(compiledDir, ON_DEMAND_DIR_NAME);
255
+ const onDisk = fs.existsSync(onDemandDir)
256
+ ? fs
257
+ .readdirSync(onDemandDir, { withFileTypes: true })
258
+ .filter((e) => e.isFile() && /\.(md|json)$/.test(e.name))
259
+ .map((e) => e.name)
260
+ .filter((f) => f.replace(/\.(md|json)$/, "") !== WRITE_PROTOCOL_FILE_NAME)
261
+ : [];
262
+ const stem = (f) => f.replace(/\.(md|json)$/, "");
263
+ const byName = new Map(manifest.onDemandFiles.map((m) => [m.name, m]));
264
+ const fileOf = new Map(onDisk.map((f) => [stem(f), f]));
265
+ const ordered = [
266
+ ...manifest.order.filter((n) => fileOf.has(n)),
267
+ ...onDisk.map(stem).filter((n) => !manifest.order.includes(n)).sort(),
268
+ ];
269
+ const kept = ordered.map((name) => {
270
+ const file = fileOf.get(name);
271
+ const content = fs.readFileSync(path.join(onDemandDir, file), "utf8").trimEnd();
272
+ const isJson = file.endsWith(".json");
273
+ // A JSON (episodic, O3) OnDemandMemory file is kept as the bytes on disk; its heading and
274
+ // triggers come from the markdown it stands for.
275
+ const markdown = isJson ? episodicJsonToMarkdown(content) : content;
276
+ const heading = sections(markdown.split("\n")).find((s) => s.level > 0)?.heading ?? name.replace(/_/g, " ");
277
+ const prev = byName.get(name);
278
+ return {
279
+ name,
280
+ file: `${ON_DEMAND_DIR_NAME}/${file}`,
281
+ heading,
282
+ content,
283
+ tokens: estimateTokens(content),
284
+ triggers: keywords(heading, markdown),
285
+ sourceStartLine: prev?.sourceLines[0] ?? 0,
286
+ sourceEndLine: prev?.sourceLines[1] ?? 0,
287
+ reason: prev?.reason ?? "task-specific",
288
+ kind: isJson ? "episodic" : prev?.kind,
289
+ };
290
+ });
291
+ const added = fresh.onDemandFiles.filter((m) => !ordered.includes(m.name));
292
+ const onDemandFiles = [...kept, ...added];
293
+ // Re-render with the merged OnDemandMemory file list. compile() already rendered the
294
+ // AlwaysOnMemory body; only the OnDemandMemory list and stub depend on the file set, and
295
+ // buildManifest reads the file list.
296
+ const merged = { ...fresh, onDemandFiles };
297
+ const rerendered = compile(userSource, {
298
+ sourceName: rel,
299
+ profileName: profile,
300
+ budget,
301
+ episodicJson: options.episodicJson,
302
+ onDemandFilesOverride: onDemandFiles,
303
+ });
304
+ merged.onDemandList = rerendered.onDemandList;
305
+ merged.alwaysOn = rerendered.alwaysOn;
306
+ merged.stub = rerendered.stub;
307
+ merged.after = rerendered.after;
308
+ // "before" is what the agent pays today: the current stub, not the stripped source.
309
+ merged.before = estimateTokens(stub);
310
+ const newManifest = buildManifest(merged, TOKENIZER_ID);
311
+ const secrets = secretsIn(rel, userSource);
312
+ const files = [
313
+ { rel: `${COMPILED_DIR}/previous/${rel}.bak`, contents: stub, action: "backup" },
314
+ { rel: `${COMPILED_DIR}/${ALWAYS_ON_FILE_NAME}`, contents: merged.alwaysOn, action: "overwrite" },
315
+ { rel: `${COMPILED_DIR}/manifest.json`, contents: `${JSON.stringify(newManifest, null, 2)}\n`, action: "overwrite" },
316
+ ];
317
+ for (const m of added) {
318
+ const exists = fs.existsSync(path.join(compiledDir, m.file));
319
+ files.push({ rel: `${COMPILED_DIR}/${m.file}`, contents: `${m.content.trimEnd()}\n`, action: exists ? "overwrite" : "create" });
320
+ }
321
+ files.push({ rel, contents: merged.stub, action: "overwrite" });
322
+ return {
323
+ root,
324
+ sourceRel: rel,
325
+ compiled: merged,
326
+ files,
327
+ existing: true,
328
+ profile,
329
+ profileNote,
330
+ secrets,
331
+ // The generated protocol OnDemandMemory file is re-emitted on every update; it is not something the user added.
332
+ updated: { kept: kept.length, added: added.filter((m) => !m.generated).length },
333
+ };
334
+ }
335
+ export function applyPlan(plan, options = {}) {
336
+ if (plan.secrets.length > 0 && !options.allowSecrets) {
337
+ throw new InitError(`${plan.sourceRel} holds ${plan.secrets.length} credential-shaped string(s); refusing to copy them into ${COMPILED_DIR}/\n` +
338
+ ` rotate and remove them, or pass --allow-secrets to proceed anyway`);
339
+ }
340
+ for (const file of plan.files) {
341
+ const abs = path.join(plan.root, file.rel);
342
+ // Symlink first: a linked write target is refused by name, which says more than the
343
+ // containment error it would otherwise trip on the way out of the workspace.
344
+ refuseSymlink(abs, file.rel);
345
+ if (!containedIn(plan.root, abs)) {
346
+ throw new InitError(`refusing to write outside the workspace: ${file.rel}`);
347
+ }
348
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
349
+ fs.writeFileSync(abs, file.contents, "utf8");
350
+ }
351
+ }
352
+ /** A compact, machine-readable shape of a plan (the MCP `plan`/`doctor` tools' `format: "json"`),
353
+ * built from the same InitPlan renderPlan renders as text - no separate code path to drift. */
354
+ export function planSummary(plan) {
355
+ return {
356
+ source: plan.sourceRel,
357
+ before: plan.compiled.before,
358
+ after: plan.compiled.after,
359
+ verdict: plan.verdict,
360
+ profile: plan.profile,
361
+ ...(plan.bounds
362
+ ? {
363
+ bounds: {
364
+ turns: plan.bounds.turns,
365
+ baselineTotal: plan.bounds.baselineTotal,
366
+ bestCaseTotal: plan.bounds.bestCaseTotal,
367
+ breakEvenCarriedTokens: plan.bounds.breakEvenCarriedTokens,
368
+ },
369
+ }
370
+ : {}),
371
+ onDemandFiles: plan.compiled.onDemandFiles.map((m) => ({ name: m.name, file: m.file, tokens: m.tokens, triggers: m.triggers })),
372
+ demoted: plan.compiled.demoted,
373
+ adviceCount: plan.advice?.length ?? 0,
374
+ secretsCount: plan.secrets.length,
375
+ files: plan.files.map((f) => ({ action: f.action, rel: f.rel })),
376
+ };
377
+ }
378
+ /** Human-readable summary of what a plan would do. */
379
+ export function renderPlan(plan, options) {
380
+ const { compiled } = plan;
381
+ const lines = [""];
382
+ if (plan.pointerFrom) {
383
+ lines.push(` ${plan.pointerFrom} is a pointer to ${plan.sourceRel}; compiling ${plan.sourceRel}`);
384
+ }
385
+ if (plan.updated) {
386
+ lines.push(` update: ${plan.updated.kept} OnDemandMemory file(s) kept from disk, ${plan.updated.added} new from the stub`);
387
+ }
388
+ lines.push(` source: ${plan.sourceRel} (${formatTokens(compiled.before)} tokens)`);
389
+ lines.push("");
390
+ const onDemandTokens = compiled.onDemandFiles.reduce((n, m) => n + m.tokens, 0);
391
+ lines.push(` ${ALWAYS_ON_FILE_NAME.padEnd(34)}${formatTokens(compiled.after).padStart(8)} tokens always loaded`);
392
+ lines.push(` ${`${ON_DEMAND_DIR_NAME}/`.padEnd(34)}${formatTokens(onDemandTokens).padStart(8)} tokens on demand`);
393
+ lines.push("");
394
+ for (const m of compiled.onDemandFiles) {
395
+ const trig = m.triggers.slice(0, 4).join(", ");
396
+ lines.push(` ${m.file.padEnd(32)}${formatTokens(m.tokens).padStart(8)} ${trig}`);
397
+ }
398
+ if (compiled.onDemandFiles.length)
399
+ lines.push("");
400
+ if (compiled.demoted.length > 0) {
401
+ lines.push(` kept out of AlwaysOnMemory to fit the ${formatTokens(compiled.budget)} token budget (raise with --budget):`);
402
+ for (const d of compiled.demoted)
403
+ lines.push(` "${d.heading}" (${formatTokens(d.tokens)} tokens)`);
404
+ lines.push("");
405
+ }
406
+ const saved = compiled.before - compiled.after;
407
+ const pct = compiled.before > 0 ? ((Math.abs(saved) / compiled.before) * 100).toFixed(1) : "0.0";
408
+ const direction = saved >= 0 ? `-${pct}%` : `+${pct}%`;
409
+ lines.push(` always-loaded prefix: ${formatTokens(compiled.before)} -> ${formatTokens(compiled.after)} tokens (${direction})`);
410
+ lines.push(` of which ${formatTokens(compiled.instructionTokens)} tokens are the added token-discipline block (--profile ${plan.profile})`);
411
+ if (plan.profileNote)
412
+ lines.push(` ${plan.profileNote}`);
413
+ lines.push(` tokenizer: ${TOKENIZER_ID}, an offline estimate`);
414
+ lines.push("");
415
+ if (plan.bounds) {
416
+ const b = plan.bounds;
417
+ const pctBest = b.baselineTotal > 0 ? ((b.bestCaseSaving / b.baselineTotal) * 100).toFixed(1) : "0.0";
418
+ lines.push(` over a ${formatTokens(b.turns)}-turn session, exact bounds, no workload assumed:`);
419
+ lines.push(` best case (no OnDemandMemory file opened) ${formatTokens(b.baselineTotal)} -> ${formatTokens(b.bestCaseTotal)}, ` +
420
+ `${b.bestCaseSaving >= 0 ? `saves ${formatTokens(b.bestCaseSaving)} (${pctBest}%)` : `costs ${formatTokens(-b.bestCaseSaving)} more`}`);
421
+ lines.push(` break-even ${formatTokens(Math.max(0, b.breakEvenCarriedTokens))} tokens of OnDemandMemory can stay open all session`);
422
+ lines.push("");
423
+ }
424
+ // Only a fresh compile gets a verdict. An update recompiles a workspace that already exists,
425
+ // so there is no compile-or-not decision to report and no refusal to warn about: printing one
426
+ // told the user "--write refuses unless you pass --force" directly above the list of files it
427
+ // had just written.
428
+ if (plan.updated) {
429
+ // an update has no compile-or-not decision to report
430
+ }
431
+ else if (plan.verdict === "leave" || saved <= 0) {
432
+ lines.push(" verdict: leave this file as it is.", " The compile does not shrink the always-loaded prefix: the source is small enough that", " the routing overhead costs more than routing saves. --write refuses unless you pass", " --force, for when you want the routing discipline and accept the token cost.", "");
433
+ }
434
+ else if (plan.verdict === "compile") {
435
+ lines.push(" verdict: compile. The prefix shrinks on every turn; OnDemandMemory files cost only when opened.", "");
436
+ }
437
+ if (plan.advice && plan.advice.length > 0) {
438
+ lines.push(" doctor findings init does not apply (init moves content, never deletes it):");
439
+ for (const a of plan.advice) {
440
+ const where = a.startLine ? `lines ${a.startLine}-${a.endLine ?? a.startLine}` : "";
441
+ lines.push(` ${a.rule} ${a.message}${where ? ` (${where})` : ""}`);
442
+ if (a.hint)
443
+ lines.push(` -> ${a.hint}`);
444
+ }
445
+ lines.push("");
446
+ }
447
+ if (plan.secrets.length > 0) {
448
+ lines.push(` warning: ${plan.secrets.length} credential-shaped string(s) in the source:`);
449
+ for (const s of plan.secrets)
450
+ lines.push(` line ${s.startLine}: ${s.message}`);
451
+ lines.push(" --write will refuse unless you rotate and remove them, or pass --allow-secrets.", "");
452
+ }
453
+ lines.push(" files:");
454
+ for (const f of plan.files) {
455
+ lines.push(` ${f.action.padEnd(10)}${f.rel}`);
456
+ }
457
+ lines.push("");
458
+ if (!options.write) {
459
+ lines.push(" dry run, nothing written. Pass --write to apply.");
460
+ lines.push("");
461
+ }
462
+ return lines.join("\n");
463
+ }
464
+ export function init(target, options) {
465
+ const plan = planInit(target, options);
466
+ if (options.write && plan.verdict === "leave" && !options.force) {
467
+ throw new InitError(`${plan.sourceRel} would not get cheaper: always-loaded ${formatTokens(plan.compiled.before)} -> ${formatTokens(plan.compiled.after)} tokens\n` +
468
+ ` init refuses to write a compile that costs more on every turn than the file it replaces.\n` +
469
+ ` Pass --force to compile anyway (you want the routing discipline and accept the cost).`);
470
+ }
471
+ if (options.write)
472
+ applyPlan(plan, options);
473
+ return plan;
474
+ }
475
+ export { STUB_MARKER };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The token-reduction instruction set.
3
+ *
4
+ * Ported from the MinniMemory v1.0 research file. These are the behavioural half of the
5
+ * product: restructuring a memory file into AlwaysOnMemory plus OnDemandMemory only pays off
6
+ * if the agent is actually told to route rather than read everything, so candidates 15-25 in
7
+ * particular are what make the emitted structure do anything at all.
8
+ *
9
+ * IMPORTANT, and it must stay in the generated output: this set is reasoned and audited but
10
+ * NOT measured. The source research states plainly that no live measurement pass has run, and
11
+ * that candidates 5 and 15-25 cannot be measured by the current single-turn harness at all.
12
+ * Nothing here may be presented to a user as a proven saving.
13
+ */
14
+ export type InstructionCategory =
15
+ /** shapes how the model writes its answer; targets output tokens */
16
+ "output"
17
+ /** shapes what gets loaded, kept, and re-sent; targets input and context growth */
18
+ | "context";
19
+ export interface Instruction {
20
+ /** stable id, numbered to match the source research candidates */
21
+ id: string;
22
+ title: string;
23
+ /** the instruction text, written to be dropped into a memory file verbatim */
24
+ text: string;
25
+ category: InstructionCategory;
26
+ /** in the default profile */
27
+ defaultOn: boolean;
28
+ /** why it is off by default, or what it replaces */
29
+ note?: string;
30
+ }
31
+ /**
32
+ * The default profile resolves the redundancies the source research documents rather than
33
+ * emitting all 25 and letting them contradict each other:
34
+ *
35
+ * - #1 subsumes #6, #7, #10 and #12, so those four are off by default.
36
+ * - #3 and #11 directly conflict, so both are off and MERGED-3-11 replaces them.
37
+ * - #13 is off by default because it trades a clarifying question for an assumption, which is
38
+ * a judgement call a tool should not make for someone silently.
39
+ */
40
+ export declare const INSTRUCTIONS: Instruction[];
41
+ /**
42
+ * Which instructions to embed.
43
+ *
44
+ * The CLI default is `auto`, resolved in `init`: `none` when the source is under the budget,
45
+ * `routing` at or over it. `routing` is the library fallback (`defaultProfile()`), and the reason
46
+ * is a measured one: the full block costs roughly 660 tokens (approx-v2) of always-loaded prefix,
47
+ * which is larger than many real memory files. Embedding all of it by default would make `init`
48
+ * a net loss on an ordinary repo, so the fallback is the minimum set that makes the emitted
49
+ * structure actually function. The rest is a deliberate opt-in.
50
+ */
51
+ export type ProfileName = "none" | "routing" | "full";
52
+ export declare function profile(name: ProfileName): Instruction[];
53
+ export declare function isProfileName(value: string): value is ProfileName;
54
+ export declare function defaultProfile(): Instruction[];
55
+ export declare function instructionById(id: string): Instruction | undefined;
56
+ /**
57
+ * Render the selected instructions as the block that goes into an emitted AlwaysOnMemory file.
58
+ * Context rules come first because they govern what gets loaded at all.
59
+ */
60
+ export declare function renderInstructions(selected: Instruction[]): string;