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
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `bench`: what a compiled workspace actually costs over a session.
3
+ *
4
+ * The number `init` prints is tokens *moved* out of the always-loaded prefix. That is not the
5
+ * same as tokens saved, and the difference is the whole reason this command exists (audit,
6
+ * 2026-09-01): an OnDemandMemory file the agent opens on turn 1 becomes a tool result in the
7
+ * conversation and is re-sent on every turn after it, so it costs the full session, not one
8
+ * turn. Real savings are the OnDemandMemory files a session never opens.
9
+ *
10
+ * Two things are computed here, and they are not the same kind of claim:
11
+ *
12
+ * bounds() exact arithmetic over the manifest, extracted to bounds.ts (2026-09-16, C3) so
13
+ * init.ts's compile-verdict check can depend on just that, not the router/recall
14
+ * machinery below. Re-exported here unchanged - see bounds.ts's own header.
15
+ *
16
+ * model() a session model over a caller-supplied workload. It routes each task with the
17
+ * same deterministic ranker `recall` ships, then carries every opened OnDemandMemory
18
+ * file forward for the rest of the session. This is a MODEL, and every number it
19
+ * produces is conditional on the workload being representative. It is never a
20
+ * measurement and must never be quoted as one.
21
+ *
22
+ * Neither is a live run. Observed token usage against a real agent loop is a separate rig
23
+ * (a real multi-turn session, with and without the compile) and is the only thing that may be called measured.
24
+ */
25
+ import type { Manifest } from "./compile.js";
26
+ import { BenchError, bounds, type BenchBounds } from "./bounds.js";
27
+ export { BenchError, bounds, type BenchBounds };
28
+ export interface BenchTurn {
29
+ turn: number;
30
+ task: string;
31
+ /** OnDemandMemory files opened on this turn, not counting ones already in context */
32
+ opened: string[];
33
+ /** every OnDemandMemory file in context at the end of this turn */
34
+ carried: string[];
35
+ compiledTokens: number;
36
+ baselineTokens: number;
37
+ }
38
+ export interface BenchModel {
39
+ /**
40
+ * What the router opened per turn: whole OnDemandMemory files (the agent-driven route, where
41
+ * an agent reads a file the OnDemandMemory list pointed at) or sections (the recall route,
42
+ * where the MCP tool returns units up to a token cap). The two are different sessions and must
43
+ * be labelled.
44
+ */
45
+ unit: "file" | "section";
46
+ turns: BenchTurn[];
47
+ baselineTotal: number;
48
+ compiledTotal: number;
49
+ saving: number;
50
+ savingPct: number;
51
+ onDemandOpened: string[];
52
+ onDemandNeverOpened: string[];
53
+ /** tasks that matched no OnDemandMemory file at all: the router had nothing to offer them */
54
+ unroutedTurns: number;
55
+ /**
56
+ * Cost of one more turn once every OnDemandMemory file this workload opens is already in
57
+ * context. This is the number that decides whether a longer session eventually wins: below
58
+ * the baseline the deficit is recoverable, at or above it the compiled shape is permanently
59
+ * more expensive for this workload no matter how long the session runs.
60
+ */
61
+ steadyStatePerTurn: number;
62
+ /** baseline minus steady state. Negative means every further turn loses more ground. */
63
+ steadyStateGainPerTurn: number;
64
+ /**
65
+ * The turn at which cumulative tokens first come out even or ahead, assuming no OnDemandMemory
66
+ * file opens after the workload ends. Undefined when the steady state never recovers.
67
+ */
68
+ breakEvenTurn?: number;
69
+ }
70
+ /**
71
+ * Parse a workload file: one task description per line, blank lines and `#` comments ignored.
72
+ * One line is one turn, and the count of lines is the session length - inventing extra turns by
73
+ * cycling the list would be fabricating session shape the caller did not describe.
74
+ */
75
+ export declare function parseWorkload(content: string): string[];
76
+ export declare function readWorkload(file: string): string[];
77
+ /**
78
+ * Walk a workload turn by turn, opening whatever the shipped router matches and carrying it for
79
+ * the rest of the session.
80
+ *
81
+ * Stated assumptions, all of which move the number:
82
+ * - one workload line is one turn, and the agent's own reasoning and tool results are not
83
+ * counted on either side. Only memory tokens are compared, so the ratio here is not a
84
+ * session bill.
85
+ * - an OnDemandMemory file whose triggers match the task is opened in full. A real agent may
86
+ * read one and stop, or skip one the list offered.
87
+ * - an opened OnDemandMemory file stays in context to the end of the session, which is what
88
+ * makes carried files expensive and is the effect this command exists to expose.
89
+ */
90
+ export interface ModelOptions {
91
+ /**
92
+ * The compiled workspace root. When given, the model routes at section level with the same
93
+ * ranker and token cap `recall` uses, reading OnDemandMemory files from disk. Without it only
94
+ * the manifest is available, so the model routes whole OnDemandMemory files by their triggers.
95
+ */
96
+ root?: string;
97
+ }
98
+ export declare function model(manifest: Manifest, tasks: string[], options?: ModelOptions): BenchModel;
package/dist/bench.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * `bench`: what a compiled workspace actually costs over a session.
3
+ *
4
+ * The number `init` prints is tokens *moved* out of the always-loaded prefix. That is not the
5
+ * same as tokens saved, and the difference is the whole reason this command exists (audit,
6
+ * 2026-09-01): an OnDemandMemory file the agent opens on turn 1 becomes a tool result in the
7
+ * conversation and is re-sent on every turn after it, so it costs the full session, not one
8
+ * turn. Real savings are the OnDemandMemory files a session never opens.
9
+ *
10
+ * Two things are computed here, and they are not the same kind of claim:
11
+ *
12
+ * bounds() exact arithmetic over the manifest, extracted to bounds.ts (2026-09-16, C3) so
13
+ * init.ts's compile-verdict check can depend on just that, not the router/recall
14
+ * machinery below. Re-exported here unchanged - see bounds.ts's own header.
15
+ *
16
+ * model() a session model over a caller-supplied workload. It routes each task with the
17
+ * same deterministic ranker `recall` ships, then carries every opened OnDemandMemory
18
+ * file forward for the rest of the session. This is a MODEL, and every number it
19
+ * produces is conditional on the workload being representative. It is never a
20
+ * measurement and must never be quoted as one.
21
+ *
22
+ * Neither is a live run. Observed token usage against a real agent loop is a separate rig
23
+ * (a real multi-turn session, with and without the compile) and is the only thing that may be called measured.
24
+ */
25
+ import fs from "node:fs";
26
+ import { loadOnDemandContent, rankOnDemandFiles } from "./recall.js";
27
+ import { buildSearchIndex, rankUnits, selectUnits, unitsOf } from "./router.js";
28
+ import { BenchError, bounds } from "./bounds.js";
29
+ export { BenchError, bounds };
30
+ /**
31
+ * Parse a workload file: one task description per line, blank lines and `#` comments ignored.
32
+ * One line is one turn, and the count of lines is the session length - inventing extra turns by
33
+ * cycling the list would be fabricating session shape the caller did not describe.
34
+ */
35
+ export function parseWorkload(content) {
36
+ return content
37
+ .split(/\r?\n/)
38
+ .map((l) => l.trim())
39
+ .filter((l) => l !== "" && !l.startsWith("#"));
40
+ }
41
+ export function readWorkload(file) {
42
+ let content;
43
+ try {
44
+ content = fs.readFileSync(file, "utf8");
45
+ }
46
+ catch {
47
+ throw new BenchError(`cannot read workload file: ${file}`);
48
+ }
49
+ const tasks = parseWorkload(content);
50
+ if (tasks.length === 0) {
51
+ throw new BenchError(`workload file has no tasks: ${file}\n one task description per line, # for comments`);
52
+ }
53
+ return tasks;
54
+ }
55
+ /** A unit's identity across turns: OnDemandMemory file plus start line. */
56
+ function unitId(u) {
57
+ return `${u.onDemandFile}:${u.startLine}`;
58
+ }
59
+ function unitLabel(u) {
60
+ return `${u.onDemandFile} > ${u.heading}`;
61
+ }
62
+ function buildIndex(manifest, root) {
63
+ const units = manifest.onDemandFiles.flatMap((m) => unitsOf({ name: m.name, file: m.file }, loadOnDemandContent(root, m.file)));
64
+ return buildSearchIndex(units, new Map(manifest.onDemandFiles.map((m) => [m.name, m.triggers])));
65
+ }
66
+ export function model(manifest, tasks, options = {}) {
67
+ const index = options.root ? buildIndex(manifest, options.root) : undefined;
68
+ const unit = index ? "section" : "file";
69
+ // Carried things are identified by id; their token cost and display label are looked up.
70
+ const carried = new Set();
71
+ const tokensOf = new Map();
72
+ const labelOf = new Map();
73
+ const onDemandFileOf = new Map();
74
+ for (const m of manifest.onDemandFiles) {
75
+ tokensOf.set(m.name, m.tokens);
76
+ labelOf.set(m.name, m.name);
77
+ onDemandFileOf.set(m.name, m.name);
78
+ }
79
+ if (index) {
80
+ for (const u of index.units) {
81
+ tokensOf.set(unitId(u), u.tokens);
82
+ labelOf.set(unitId(u), unitLabel(u));
83
+ onDemandFileOf.set(unitId(u), u.onDemandFile);
84
+ }
85
+ }
86
+ const turns = [];
87
+ let unroutedTurns = 0;
88
+ tasks.forEach((task, i) => {
89
+ const matched = index
90
+ ? selectUnits(rankUnits(index, task)).map((r) => unitId(r.unit))
91
+ : rankOnDemandFiles(manifest, task)
92
+ .filter((r) => r.score > 0)
93
+ .map((m) => m.name);
94
+ if (matched.length === 0)
95
+ unroutedTurns++;
96
+ const opened = matched.filter((id) => !carried.has(id));
97
+ for (const id of opened)
98
+ carried.add(id);
99
+ const carriedTokens = [...carried].reduce((n, id) => n + (tokensOf.get(id) ?? 0), 0);
100
+ turns.push({
101
+ turn: i + 1,
102
+ task,
103
+ opened: opened.map((id) => labelOf.get(id) ?? id),
104
+ carried: [...carried].map((id) => labelOf.get(id) ?? id),
105
+ compiledTokens: manifest.prefix.after + carriedTokens,
106
+ baselineTokens: manifest.prefix.before,
107
+ });
108
+ });
109
+ const compiledTotal = turns.reduce((n, t) => n + t.compiledTokens, 0);
110
+ const baselineTotal = turns.reduce((n, t) => n + t.baselineTokens, 0);
111
+ const touchedOnDemandFiles = new Set([...carried].map((id) => onDemandFileOf.get(id) ?? id));
112
+ const opened = manifest.onDemandFiles.map((m) => m.name).filter((n) => touchedOnDemandFiles.has(n));
113
+ const carriedTokens = [...carried].reduce((n, id) => n + (tokensOf.get(id) ?? 0), 0);
114
+ const steadyStatePerTurn = manifest.prefix.after + carriedTokens;
115
+ const steadyStateGainPerTurn = manifest.prefix.before - steadyStatePerTurn;
116
+ const saving = baselineTotal - compiledTotal;
117
+ let breakEvenTurn;
118
+ if (saving >= 0) {
119
+ breakEvenTurn = turns.findIndex((_, i) => {
120
+ const upTo = turns.slice(0, i + 1);
121
+ return upTo.reduce((n, t) => n + t.baselineTokens - t.compiledTokens, 0) >= 0;
122
+ });
123
+ breakEvenTurn = breakEvenTurn >= 0 ? breakEvenTurn + 1 : undefined;
124
+ }
125
+ else if (steadyStateGainPerTurn > 0) {
126
+ breakEvenTurn = turns.length + Math.ceil(-saving / steadyStateGainPerTurn);
127
+ }
128
+ return {
129
+ unit,
130
+ turns,
131
+ baselineTotal,
132
+ compiledTotal,
133
+ saving,
134
+ savingPct: baselineTotal === 0 ? 0 : (saving / baselineTotal) * 100,
135
+ onDemandOpened: opened,
136
+ onDemandNeverOpened: manifest.onDemandFiles.map((m) => m.name).filter((n) => !touchedOnDemandFiles.has(n)),
137
+ unroutedTurns,
138
+ steadyStatePerTurn,
139
+ steadyStateGainPerTurn,
140
+ breakEvenTurn,
141
+ };
142
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rendering for `bench`. Kept apart from bench.ts so the arithmetic stays testable without
3
+ * string matching, matching how report.ts sits beside the rule engine.
4
+ *
5
+ * Every rendered block states what kind of claim it is. A number here is either exact arithmetic
6
+ * over the manifest (the bounds) or a model conditional on a caller-supplied workload, and the
7
+ * output says which, every time, without the reader having to know the difference already.
8
+ */
9
+ import type { BenchBounds, BenchModel } from "./bench.js";
10
+ export declare function renderBounds(b: BenchBounds, tokenizer: string): string;
11
+ export declare function renderModel(m: BenchModel, tokenizer: string): string;
12
+ export declare function renderNoWorkload(): string;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Rendering for `bench`. Kept apart from bench.ts so the arithmetic stays testable without
3
+ * string matching, matching how report.ts sits beside the rule engine.
4
+ *
5
+ * Every rendered block states what kind of claim it is. A number here is either exact arithmetic
6
+ * over the manifest (the bounds) or a model conditional on a caller-supplied workload, and the
7
+ * output says which, every time, without the reader having to know the difference already.
8
+ */
9
+ import { formatTokens } from "./tokenizer.js";
10
+ function pct(part, whole) {
11
+ if (whole === 0)
12
+ return "0.0%";
13
+ return `${((part / whole) * 100).toFixed(1)}%`;
14
+ }
15
+ function signed(n) {
16
+ return n >= 0 ? `saves ${formatTokens(n)}` : `costs ${formatTokens(-n)} more`;
17
+ }
18
+ export function renderBounds(b, tokenizer) {
19
+ const out = [""];
20
+ out.push(` exact, over ${formatTokens(b.turns)} turns. No workload, no assumptions.`);
21
+ out.push("");
22
+ out.push(` original memory file ${formatTokens(b.baselinePerTurn).padStart(8)} tokens per turn`);
23
+ out.push(` compiled always-loaded ${formatTokens(b.compiledPerTurn).padStart(8)} tokens per turn`);
24
+ out.push(` OnDemandMemory, all of it ${formatTokens(b.totalOnDemandTokens).padStart(8)} tokens, on demand`);
25
+ out.push("");
26
+ out.push(` best case, no OnDemandMemory file ever opened`);
27
+ out.push(` ${formatTokens(b.baselineTotal)} -> ${formatTokens(b.bestCaseTotal)} tokens, ` +
28
+ `${signed(b.bestCaseSaving)} (${pct(b.bestCaseSaving, b.baselineTotal)})`);
29
+ out.push("");
30
+ out.push(` worst case, every OnDemandMemory file opened on turn 1 and carried`);
31
+ out.push(` ${formatTokens(b.baselineTotal)} -> ${formatTokens(b.worstCaseTotal)} tokens, ` +
32
+ `${signed(b.worstCaseSaving)}`);
33
+ out.push("");
34
+ out.push(` break-even: ${formatTokens(b.breakEvenCarriedTokens)} tokens of OnDemandMemory can sit in`);
35
+ out.push(` context from turn 1 to the end and come out even. Past that the compiled`);
36
+ out.push(` shape costs more than the file it replaced.`);
37
+ out.push("");
38
+ out.push(` tokenizer: ${tokenizer}, an offline estimate`);
39
+ out.push("");
40
+ return out.join("\n");
41
+ }
42
+ export function renderModel(m, tokenizer) {
43
+ const out = [""];
44
+ const turns = m.turns.length;
45
+ out.push(` MODEL, not a measurement. ${formatTokens(turns)} turns from the workload you supplied.`);
46
+ out.push(m.unit === "section"
47
+ ? ` routed at section level, as recall() does: a hit carries the section, not the file.`
48
+ : ` routed at file level, as an agent reading a whole file the OnDemandMemory list named would.`);
49
+ out.push("");
50
+ out.push(` memory tokens over the session ${formatTokens(m.baselineTotal)} -> ${formatTokens(m.compiledTotal)}`);
51
+ out.push(` ${signed(m.saving)}${m.saving >= 0 ? ` (${m.savingPct.toFixed(1)}%)` : ""}`);
52
+ out.push("");
53
+ if (m.onDemandOpened.length > 0) {
54
+ out.push(` opened and carried (${m.onDemandOpened.length}):`);
55
+ for (const name of m.onDemandOpened)
56
+ out.push(` ${name}`);
57
+ out.push("");
58
+ }
59
+ if (m.onDemandNeverOpened.length > 0) {
60
+ out.push(` never opened (${m.onDemandNeverOpened.length}) - this is where the saving comes from:`);
61
+ for (const name of m.onDemandNeverOpened)
62
+ out.push(` ${name}`);
63
+ out.push("");
64
+ }
65
+ out.push(` one more turn, once everything this workload opens is in context:`);
66
+ out.push(` ${formatTokens(m.steadyStatePerTurn)} vs ${formatTokens(m.turns[0]?.baselineTokens ?? 0)} baseline, ` +
67
+ `${m.steadyStateGainPerTurn >= 0 ? `saves ${formatTokens(m.steadyStateGainPerTurn)}` : `costs ${formatTokens(-m.steadyStateGainPerTurn)} more`} per turn`);
68
+ out.push("");
69
+ if (m.saving < 0 && m.steadyStateGainPerTurn <= 0) {
70
+ out.push(` this workload never breaks even. The OnDemandMemory files it opens are large enough that`);
71
+ out.push(` carrying them costs more every turn than the original file did, so a longer`);
72
+ out.push(` session loses more, not less. Splitting differently, or not compiling this`);
73
+ out.push(` file at all, is the honest answer here.`);
74
+ out.push("");
75
+ }
76
+ else if (m.saving < 0 && m.breakEvenTurn !== undefined) {
77
+ out.push(` behind by ${formatTokens(-m.saving)} at turn ${formatTokens(m.turns.length)}, ` +
78
+ `recovering ${formatTokens(m.steadyStateGainPerTurn)} per turn:`);
79
+ out.push(` breaks even around turn ${formatTokens(m.breakEvenTurn)} if nothing further opens.`);
80
+ out.push("");
81
+ }
82
+ else if (m.saving >= 0 && m.breakEvenTurn !== undefined) {
83
+ out.push(` ahead from turn ${formatTokens(m.breakEvenTurn)} onward.`);
84
+ out.push("");
85
+ }
86
+ if (m.unroutedTurns > 0) {
87
+ out.push(` ${formatTokens(m.unroutedTurns)} of ${formatTokens(turns)} turns matched no OnDemandMemory file at all.`);
88
+ out.push(` Those turns are cheap here, but a real agent may have needed something and`);
89
+ out.push(` not been routed to it. Check them before trusting the total.`);
90
+ out.push("");
91
+ }
92
+ out.push(` what this number is conditional on:`);
93
+ out.push(` - your workload being what a real session looks like. It is your file;`);
94
+ out.push(` the tool cannot tell whether it is representative.`);
95
+ if (m.unit === "section") {
96
+ out.push(` - every section recall() returns being carried for the rest of the session,`);
97
+ out.push(` up to its token cap per turn. A real agent may widen with outline() or`);
98
+ out.push(` read the whole file anyway.`);
99
+ }
100
+ else {
101
+ out.push(` - every OnDemandMemory file whose triggers match being opened in full. A real`);
102
+ out.push(` agent may read one and stop, or ignore one the list offered.`);
103
+ }
104
+ out.push(` - memory tokens only. Reasoning, tool results and conversation are excluded`);
105
+ out.push(` from both sides, so this ratio is not a session bill.`);
106
+ out.push("");
107
+ out.push(` For observed usage against a live agent, measure a real multi-turn session with`);
108
+ out.push(` and without the compile. That is the only path allowed to call a number measured.`);
109
+ out.push("");
110
+ out.push(` tokenizer: ${tokenizer}, an offline estimate`);
111
+ out.push("");
112
+ return out.join("\n");
113
+ }
114
+ export function renderNoWorkload() {
115
+ return [
116
+ "",
117
+ " No workload given, so only the exact bounds above are available.",
118
+ "",
119
+ " A session number needs a workload: a file with one task description per line,",
120
+ " describing what you actually do in this repo.",
121
+ "",
122
+ " minnimemory bench --workload tasks.txt",
123
+ "",
124
+ " minnimemory ships no default task list on purpose. A number produced from tasks",
125
+ " the tool invented for itself would say more about the tool than about your repo.",
126
+ "",
127
+ ].join("\n");
128
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `bounds`: exact arithmetic over a compiled manifest, no assumptions, no workload, nothing to
3
+ * disagree with. The best case (no OnDemandMemory file is ever opened), the worst case (every
4
+ * OnDemandMemory file is opened on turn 1 and carried for the rest of the session), and the
5
+ * break-even point between them.
6
+ *
7
+ * Extracted from bench.ts (2026-09-16, C3) so `init.ts`'s compile-verdict check - which only
8
+ * ever needed this exact arithmetic, never bench.ts's workload model - does not pull in the
9
+ * router/recall machinery `model()` needs. `bench.ts` re-exports everything here unchanged, so
10
+ * every existing `from "./bench.js"` import of `bounds`/`BenchBounds`/`BenchError` keeps working.
11
+ *
12
+ * See bench.ts's own header for the fuller picture: this is one of the two things it computes,
13
+ * and it is not the same kind of claim as `model()` - see that file.
14
+ */
15
+ import type { Manifest } from "./compile.js";
16
+ export declare class BenchError extends Error {
17
+ }
18
+ export interface BenchBounds {
19
+ turns: number;
20
+ /** the original memory file, re-sent whole on every turn */
21
+ baselinePerTurn: number;
22
+ /** the compiled stub as measured on disk: AlwaysOnMemory, the OnDemandMemory list, instruction
23
+ * block and assembly overhead */
24
+ compiledPerTurn: number;
25
+ baselineTotal: number;
26
+ /** no OnDemandMemory file is ever opened */
27
+ bestCaseTotal: number;
28
+ /** every OnDemandMemory file is opened on turn 1 and carried to the end */
29
+ worstCaseTotal: number;
30
+ bestCaseSaving: number;
31
+ /** negative when the compiled shape costs more than the original */
32
+ worstCaseSaving: number;
33
+ /**
34
+ * OnDemandMemory tokens that can sit in context from turn 1 to the end of the session and
35
+ * exactly break even. Equal to the per-turn prefix saving, because both are paid on every turn.
36
+ */
37
+ breakEvenCarriedTokens: number;
38
+ totalOnDemandTokens: number;
39
+ }
40
+ export declare function bounds(manifest: Manifest, turns: number): BenchBounds;
package/dist/bounds.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `bounds`: exact arithmetic over a compiled manifest, no assumptions, no workload, nothing to
3
+ * disagree with. The best case (no OnDemandMemory file is ever opened), the worst case (every
4
+ * OnDemandMemory file is opened on turn 1 and carried for the rest of the session), and the
5
+ * break-even point between them.
6
+ *
7
+ * Extracted from bench.ts (2026-09-16, C3) so `init.ts`'s compile-verdict check - which only
8
+ * ever needed this exact arithmetic, never bench.ts's workload model - does not pull in the
9
+ * router/recall machinery `model()` needs. `bench.ts` re-exports everything here unchanged, so
10
+ * every existing `from "./bench.js"` import of `bounds`/`BenchBounds`/`BenchError` keeps working.
11
+ *
12
+ * See bench.ts's own header for the fuller picture: this is one of the two things it computes,
13
+ * and it is not the same kind of claim as `model()` - see that file.
14
+ */
15
+ export class BenchError extends Error {
16
+ }
17
+ export function bounds(manifest, turns) {
18
+ if (!Number.isInteger(turns) || turns < 1) {
19
+ throw new BenchError(`--turns must be a positive whole number (got ${turns})`);
20
+ }
21
+ const baselinePerTurn = manifest.prefix.before;
22
+ // The measured stub, not alwaysOnBody.tokens + onDemandList.tokens. The stub also carries the
23
+ // marker fences, the routing instruction and the separators between the assembled parts, which
24
+ // belong to the always-loaded cost and are attributed to neither file. Summing the parts
25
+ // understates the real prefix (25 tokens on a 2,190-token stub, measured 2026-09-04) and so
26
+ // overstates the saving, which is the one direction this tool must never be wrong in.
27
+ const compiledPerTurn = manifest.prefix.after;
28
+ const totalOnDemandTokens = manifest.onDemandFiles.reduce((n, m) => n + m.tokens, 0);
29
+ const baselineTotal = baselinePerTurn * turns;
30
+ const bestCaseTotal = compiledPerTurn * turns;
31
+ const worstCaseTotal = (compiledPerTurn + totalOnDemandTokens) * turns;
32
+ return {
33
+ turns,
34
+ baselinePerTurn,
35
+ compiledPerTurn,
36
+ baselineTotal,
37
+ bestCaseTotal,
38
+ worstCaseTotal,
39
+ bestCaseSaving: baselineTotal - bestCaseTotal,
40
+ worstCaseSaving: baselineTotal - worstCaseTotal,
41
+ breakEvenCarriedTokens: baselinePerTurn - compiledPerTurn,
42
+ totalOnDemandTokens,
43
+ };
44
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * minnimemory CLI.
4
+ *
5
+ * Exit codes: 0 clean, 1 findings at or above the --fail-on threshold, 2 execution error.
6
+ */
7
+ /**
8
+ * Single switch for whether `mcp` starts the real server or the disabled placeholder. Exported
9
+ * so tests/cli.test.ts can pin it without needing a live stdio transport in the test process.
10
+ *
11
+ * Re-enabled 2026-09-10 for internal testing, then shipped publicly in the 1.0.0-beta.1 npm
12
+ * release (2026-09-15). Flip back to true to re-stash the server in a future release.
13
+ */
14
+ export declare const MCP_STASHED = false;
15
+ export declare function main(argv: string[]): number;