bantamkit-mcp 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. package/package.json +45 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: readonly string[]): number;
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The operator's half of the lifecycle position, on the Node side: `bantamkit-memory`.
4
+ *
5
+ * `docs/memory.md` states, as a design decision, that `lint`, `archived` and `restore`
6
+ * are **not** agent tools — "lifecycle is an operator decision, not a model decision" —
7
+ * and the eleven-tool surface holds to it; `compact` is the one exception since job42
8
+ * (`memory_compact`, the on-refusal path the model reaches itself). `runtime-py` made that
9
+ * position coherent in August 2026 by giving the operator `python -m bantamkit.memory`.
10
+ * `runtime-ts` did not. MEASURED 2026-08-24 before this file existed: `node dist/cli.js -h`
11
+ * and `python -m bantamkit.mcpserver -h` print byte-identical help, neither offers a
12
+ * subcommand, and the maintenance CLI existed only in Python — so an operator who ran
13
+ * `npx bantamkit-mcp` and nothing else could not compact, could not lint, and could not
14
+ * restore an archived fact. Not a documentation gap: there was no code path.
15
+ *
16
+ * WHY A SECOND `bin` AND NOT A FLAG ON `bantamkit-mcp`
17
+ * ---------------------------------------------------
18
+ * The reference's reason for a separate entry point (`bantamkit-mcp` speaks JSON-RPC over
19
+ * stdout, so a lifecycle report printed there corrupts the wire) does NOT decide it here:
20
+ * `--assets-root`, `--mcp-report` and `--statusline` all print and return before a transport
21
+ * exists, and `src/cli.ts` says so at each of them. The reason a subcommand is refused is
22
+ * different and it is measured: `bantamkit-mcp`'s help is a BYTE-COMPARED artifact against
23
+ * `python -m bantamkit.mcpserver -h` (`tools/conformance/suites/cli.mjs`). Hanging a
24
+ * subparsers action off that parser would add `{memory} ...` to its usage line and a whole
25
+ * `positional arguments:` section to its help, against a reference that has neither. The
26
+ * parity that already holds would have to be broken to reach the surface this file adds.
27
+ *
28
+ * A second `bin` costs nothing on either install route. `npm i -g bantamkit-mcp` puts both
29
+ * commands on PATH; without an install, `npx -p bantamkit-mcp bantamkit-memory status` runs
30
+ * this file straight out of the registry. `package.json` used to declare exactly one bin,
31
+ * and `src/cli.ts` still reasons from that — see the `--statusline` comment there, which is
32
+ * about a Claude Code `statusLine` registration and is unaffected: a host that names one
33
+ * command still names one command.
34
+ *
35
+ * THE ONE STRING THAT CANNOT BE THE SAME
36
+ * --------------------------------------
37
+ * The Python operator types `python -m bantamkit.memory`; the Node operator types
38
+ * `bantamkit-memory`. There is no third spelling that both could use: the reference's `prog`
39
+ * is a Python `-m` invocation and a pure-npm install has no Python in it at all, while
40
+ * `bantamkit-memory` names a console script that CPython does not install. Everything
41
+ * downstream of `prog` moves with it — the usage line, every `…: error:` prefix, and the two
42
+ * remediation sentences that name a command to run (`lint`'s `try:` line and `compact`'s
43
+ * `restore one with:`). Those two are the reason this is a substitution and not a cosmetic
44
+ * difference: printing `python -m bantamkit.memory compact …` to someone holding an npx
45
+ * install would be an instruction they cannot follow.
46
+ *
47
+ * The rule, stated so a conformance case can rule on it and MEASURED at 2026-08-24: the two
48
+ * CLIs produce IDENTICAL BYTES after substituting `python -m bantamkit.memory` ->
49
+ * `bantamkit-memory`, EXCEPT that a usage line long enough to wrap wraps at different
50
+ * points, because the hanging indent argparse computes is `len(prefix) + len(prog) + 1`.
51
+ * Verified both halves: at `COLUMNS=200` all four help forms (`-h`, `status -h`,
52
+ * `restore -h`, `compact -h`) are byte-identical after the substitution and nothing else; at
53
+ * `COLUMNS=80` the ONLY diff in each is the continuation indent (31 columns here against 41
54
+ * there) and, for the top parser, whether the line needed to wrap at all. So the divergence
55
+ * is `prog` and the arithmetic downstream of `prog`, not a second rendering algorithm — a
56
+ * ruling should compare at a width where the line fits, and pin the wrap separately.
57
+ * `PROG` below is the only place the Node spelling appears.
58
+ *
59
+ * Scope is deliberately the writable project layer only, exactly as the reference has it.
60
+ * `Memory.layered()` also mounts read-only grants and the profile store, and
61
+ * `MemoryStore.compact()` touches neither; resolving through `discoverProjectStore` keeps
62
+ * this CLI unable to archive a store it does not own, rather than merely unlikely to.
63
+ *
64
+ * Exit codes: 0 success, 1 an operational failure the operator must act on (over budget,
65
+ * malformed fact, refused restore), 2 argparse usage error.
66
+ */
67
+ import { BantamError } from '../errors.js';
68
+ import { ArgumentTypeError, ArgvError, formatHelp, formatUsageBlock, HelpRequested, helpWidth, parseArgs, pyIntStrict, } from '../pyargparse.js';
69
+ import { discoverProjectStore } from './layers.js';
70
+ import { pyJoin } from './pyfs.js';
71
+ import { DEFAULT_INDEX_BUDGET, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyText, } from './store.js';
72
+ /** The Node operator's spelling of the reference's `python -m bantamkit.memory`. */
73
+ const PROG = 'bantamkit-memory';
74
+ /** `SystemExit("...")`: the message on stderr, exit 1. */
75
+ class Refusal extends Error {
76
+ }
77
+ /**
78
+ * `_positive`: a budget of 0 or less silently makes every save fail, so refuse it at the edge.
79
+ *
80
+ * The TWO failure sentences are both the reference's and they are different on purpose:
81
+ * `--budget x` never reaches this body (`int()` raises and argparse prints
82
+ * `invalid _positive value: 'x'`), while `--budget 0` gets this `ArgumentTypeError`'s own
83
+ * text. `typeName` below is `_positive` because argparse interpolates the CALLABLE's
84
+ * `__name__`, and the reference's callable is named `_positive`.
85
+ */
86
+ function positive(text) {
87
+ const value = pyIntStrict(text);
88
+ if (value < 1)
89
+ throw new ArgumentTypeError('must be >= 1');
90
+ return value;
91
+ }
92
+ /**
93
+ * `common(sub)`: the store selector and the budget, on every subcommand.
94
+ *
95
+ * ORDER IS WIRE-VISIBLE. argparse prints optionals in the order they were added, so this
96
+ * array — not any format string — decides that `[--store STORE | --start START]` precedes
97
+ * `[--budget BYTES]` in every sub-usage, and that `--reserve` follows both under `compact`.
98
+ * The mutually exclusive group is `[1, 2]` because `-h` is added first by the parser itself.
99
+ */
100
+ const common = (extra = []) => [
101
+ {
102
+ optionStrings: ['-h', '--help'],
103
+ dest: 'help',
104
+ kind: 'help',
105
+ help: 'show this help message and exit',
106
+ defaultValue: false,
107
+ },
108
+ {
109
+ optionStrings: ['--store'],
110
+ dest: 'store',
111
+ kind: 'store',
112
+ help: 'memory store path (skips project discovery)',
113
+ defaultValue: null,
114
+ },
115
+ {
116
+ optionStrings: ['--start'],
117
+ dest: 'start',
118
+ kind: 'store',
119
+ help: 'directory to start project-store discovery from (default: cwd)',
120
+ defaultValue: null,
121
+ },
122
+ {
123
+ optionStrings: ['--budget'],
124
+ dest: 'budget',
125
+ metavar: 'BYTES',
126
+ kind: 'store',
127
+ help: `index byte budget (default: ${DEFAULT_INDEX_BUDGET})`,
128
+ convert: positive,
129
+ typeName: '_positive',
130
+ defaultValue: DEFAULT_INDEX_BUDGET,
131
+ },
132
+ ...extra,
133
+ ];
134
+ const sub = (name, actions) => ({
135
+ prog: `${PROG} ${name}`,
136
+ // `add_parser` takes no `description=` in the reference, so the sub-help has no paragraph
137
+ // between its usage line and its first section. An empty string is that absence.
138
+ description: '',
139
+ actions,
140
+ groups: [[1, 2]],
141
+ });
142
+ /** `_parse_args`'s parser, as DATA — the one description of this command line. */
143
+ const PARSER = {
144
+ prog: PROG,
145
+ description: 'Operator lifecycle for a bantamkit memory store: inspect, lint, compact, ' +
146
+ 'archive and restore. Not an agent surface.',
147
+ actions: [
148
+ {
149
+ optionStrings: ['-h', '--help'],
150
+ dest: 'help',
151
+ kind: 'help',
152
+ help: 'show this help message and exit',
153
+ defaultValue: false,
154
+ },
155
+ {
156
+ // `add_subparsers(dest="command", required=True)` — no `help=`, which is why the
157
+ // `{status,…}` row prints bare above its five indented children.
158
+ optionStrings: [],
159
+ dest: 'command',
160
+ kind: 'subparsers',
161
+ help: '',
162
+ defaultValue: null,
163
+ subcommands: [
164
+ { name: 'status', help: 'index size, budget, headroom, archive count', parser: sub('status', common()) },
165
+ {
166
+ name: 'lint',
167
+ help: 'exit 1 if the store is malformed or over budget',
168
+ parser: sub('lint', common()),
169
+ },
170
+ {
171
+ name: 'compact',
172
+ help: 'archive the stalest facts',
173
+ parser: sub('compact', common([
174
+ {
175
+ optionStrings: ['--reserve'],
176
+ dest: 'reserve',
177
+ metavar: 'BYTES',
178
+ kind: 'store',
179
+ help: 'headroom to leave below the budget (default: the largest index line kept)',
180
+ convert: positive,
181
+ typeName: '_positive',
182
+ defaultValue: null,
183
+ },
184
+ ])),
185
+ },
186
+ {
187
+ name: 'archived',
188
+ help: 'list what compaction has moved out',
189
+ parser: sub('archived', common()),
190
+ },
191
+ {
192
+ name: 'archive',
193
+ help: 'move one named fact out',
194
+ parser: sub('archive', [
195
+ ...common(),
196
+ {
197
+ optionStrings: [],
198
+ dest: 'name',
199
+ kind: 'positional',
200
+ help: 'name of the fact to archive',
201
+ defaultValue: null,
202
+ },
203
+ ]),
204
+ },
205
+ {
206
+ name: 'restore',
207
+ help: 'move an archived fact back',
208
+ parser: sub('restore', [
209
+ ...common(),
210
+ {
211
+ optionStrings: [],
212
+ dest: 'name',
213
+ kind: 'positional',
214
+ help: 'name of the archived fact',
215
+ defaultValue: null,
216
+ },
217
+ ]),
218
+ },
219
+ ],
220
+ },
221
+ ],
222
+ groups: [],
223
+ };
224
+ function parse(argv) {
225
+ const values = parseArgs(PARSER, argv);
226
+ return {
227
+ command: values['command'],
228
+ store: values['store'],
229
+ start: values['start'],
230
+ budget: values['budget'],
231
+ reserve: (values['reserve'] ?? null),
232
+ name: values['name'],
233
+ };
234
+ }
235
+ /** `_open`: the writable project store, and nothing else this CLI could reach. */
236
+ function open(args) {
237
+ if (args.store !== null) {
238
+ if (args.store === '')
239
+ throw new Refusal('--store requires a non-empty path');
240
+ return new MemoryStore(args.store, { indexBudget: args.budget });
241
+ }
242
+ return new MemoryStore(discoverProjectStore(args.start), { indexBudget: args.budget });
243
+ }
244
+ /**
245
+ * `len(store.index_text().encode())` — the number the budget is compared against.
246
+ *
247
+ * It is the LF text's length on both runtimes. `runtime-py` writes `index.md` through
248
+ * `write_text` with `newline=None`, so on Windows the file on disk is longer than the number
249
+ * measured here; that disagreement is a runtime-py defect and is not reproduced.
250
+ */
251
+ const size = (store) => Buffer.byteLength(store.indexText(), 'utf8');
252
+ const out = (text) => {
253
+ process.stdout.write(`${text}\n`);
254
+ };
255
+ const err = (text) => {
256
+ process.stderr.write(`${text}\n`);
257
+ };
258
+ function cmdStatus(store) {
259
+ const bytes = size(store);
260
+ out(`store: ${store.root}`);
261
+ out(`facts: ${store.factCount()}`);
262
+ out(`index: ${bytes} bytes`);
263
+ out(`budget: ${store.indexBudget}`);
264
+ out(`headroom: ${store.indexBudget - bytes}`);
265
+ out(`archived: ${store.archived().length}`);
266
+ return 0;
267
+ }
268
+ function cmdLint(store) {
269
+ try {
270
+ store.lint();
271
+ }
272
+ catch (error) {
273
+ if (error instanceof MemoryBudgetExceeded) {
274
+ // Not the store's own message: that one names `compact()`, which is a call in a
275
+ // language and the wrong remedy to hand someone holding a shell.
276
+ const where = `--store ${store.root}`;
277
+ err(`lint: FAIL — index is ${size(store)} bytes, budget is ${store.indexBudget}\n` +
278
+ ` try: ${PROG} compact ${where} --budget ${store.indexBudget}`);
279
+ return 1;
280
+ }
281
+ if (error instanceof MemoryValidationError) {
282
+ err(`lint: FAIL — ${error.message}`);
283
+ return 1;
284
+ }
285
+ throw error;
286
+ }
287
+ out(`lint: ok — ${store.factCount()} facts, ${size(store)}/${store.indexBudget} bytes`);
288
+ return 0;
289
+ }
290
+ function cmdCompact(store, args) {
291
+ const before = size(store);
292
+ const result = store.compact(args.reserve);
293
+ out(`compacted ${result.archived.length} fact(s)`);
294
+ out(`index: ${before} -> ${result.indexAfter} bytes ` +
295
+ `(budget ${result.budget}, target ${result.target}, ` +
296
+ `reserve ${result.reserve}, headroom ${result.headroom})`);
297
+ if (result.archived.length === 0) {
298
+ out('nothing to archive — the index is already at or below the target');
299
+ return 0;
300
+ }
301
+ // `archive/` is on disk and nothing reads it back on its own, so the names have to land
302
+ // here or the operator never learns what left.
303
+ out(`archived -> ${result.archiveDir}`);
304
+ for (const fact of result.archived) {
305
+ out(` ${pyText(fact.name)} (${pyText(fact.type)}, ${fact.indexBytes} bytes)`);
306
+ }
307
+ out(`restore one with: ${PROG} restore <name> --store ${store.root}`);
308
+ return 0;
309
+ }
310
+ function cmdArchived(store) {
311
+ const names = store.archived();
312
+ out(`archived facts: ${names.length} (${pyJoin(store.root, 'archive')})`);
313
+ for (const name of names)
314
+ out(` ${name}`);
315
+ return 0;
316
+ }
317
+ function cmdArchive(store, args) {
318
+ try {
319
+ store.archive(args.name);
320
+ }
321
+ catch (error) {
322
+ if (error instanceof MemoryValidationError) {
323
+ err(`archive failed: ${error.message}`);
324
+ return 1;
325
+ }
326
+ throw error;
327
+ }
328
+ out(`archived '${args.name}' — index now ${size(store)}/${store.indexBudget} bytes`);
329
+ return 0;
330
+ }
331
+ function cmdRestore(store, args) {
332
+ try {
333
+ store.restore(args.name);
334
+ }
335
+ catch (error) {
336
+ if (error instanceof MemoryBudgetExceeded) {
337
+ err(`restore failed: '${args.name}' would put the index over the budget of ` +
338
+ `${store.indexBudget} bytes. Nothing changed — raise --budget or compact first.`);
339
+ return 1;
340
+ }
341
+ if (error instanceof MemoryValidationError) {
342
+ err(`restore failed: ${error.message}`);
343
+ return 1;
344
+ }
345
+ throw error;
346
+ }
347
+ out(`restored '${args.name}' — index now ${size(store)}/${store.indexBudget} bytes`);
348
+ return 0;
349
+ }
350
+ const COMMANDS = {
351
+ status: cmdStatus,
352
+ lint: cmdLint,
353
+ compact: cmdCompact,
354
+ archived: cmdArchived,
355
+ archive: cmdArchive,
356
+ restore: cmdRestore,
357
+ };
358
+ export function main(argv) {
359
+ const args = parse(argv);
360
+ return COMMANDS[args.command](open(args), args);
361
+ }
362
+ try {
363
+ process.exitCode = main(process.argv.slice(2));
364
+ }
365
+ catch (error) {
366
+ if (error instanceof HelpRequested) {
367
+ // `-h` is an ACTION, not a flag read after parsing: argparse prints and exits 0 the
368
+ // moment it is taken. `error.spec` is WHOSE help — `restore -h` prints the sub-parser's.
369
+ process.stdout.write(formatHelp(error.spec ?? PARSER, helpWidth()));
370
+ process.exitCode = 0;
371
+ }
372
+ else if (error instanceof ArgvError) {
373
+ // And whose USAGE, which is not always this parser's: a bad `--budget` under `status` is
374
+ // `bantamkit-memory status: error: …`, while an unrecognized argument the sub-parser
375
+ // handed back is reported by the top parser. Both measured against the reference.
376
+ const spec = error.spec ?? PARSER;
377
+ process.stderr.write(`${formatUsageBlock(spec, helpWidth())}${spec.prog}: error: ${error.message}\n`);
378
+ process.exitCode = 2;
379
+ }
380
+ else if (error instanceof Refusal) {
381
+ process.stderr.write(`${error.message}\n`);
382
+ process.exitCode = 1;
383
+ }
384
+ else if (error instanceof BantamError) {
385
+ // The store errors the five commands do NOT catch — an unreadable `facts/` under
386
+ // `status`, `compact` or `archived`. The reference lets those escape `main` and CPython
387
+ // prints a TRACEBACK, exiting 1. The exit code matches; the stderr text cannot, because
388
+ // a Python traceback is not a portable artifact. `src/cli.ts` sets the precedent for the
389
+ // shape used here. Carried as a reported divergence, not a silent one.
390
+ process.stderr.write(`${PROG}: ${error.message}\n`);
391
+ process.exitCode = 1;
392
+ }
393
+ else {
394
+ throw error;
395
+ }
396
+ }
@@ -0,0 +1,256 @@
1
+ import { type StoreBinding } from './layers.js';
2
+ import { MemoryStore } from './store.js';
3
+ /**
4
+ * The model's spelling adapted to the store's contract: lowercase, `_`/space -> `-`.
5
+ *
6
+ * The store's pattern does not move; the component bends the argument to it. Measured cause:
7
+ * 5 of the 7b probe runs burned their whole turn budget retrying `memory_save` with the
8
+ * snake_case names the model invents. Non-strings are handed on untouched so that store
9
+ * validation still speaks.
10
+ */
11
+ export declare function normalizeName<T>(name: T): T;
12
+ /**
13
+ * The last layer `layered` appends, named once so other code can recognise it.
14
+ *
15
+ * It was spelled inline and nowhere else, which is how it came to be prescribed as a place
16
+ * to start a project store: nothing that wrote the advice could see that the directory it
17
+ * was recommending is also the machine-wide layer.
18
+ */
19
+ export declare function profileStore(): string;
20
+ /** The name a layer answers under: the PROJECT directory, not the store directory. */
21
+ export declare function layerLabel(root: string): string;
22
+ export interface MemoryOptions {
23
+ k?: number;
24
+ indexBudget?: number;
25
+ /**
26
+ * The clock, frozen. `Memory` in the reference takes no such argument — the store it wraps
27
+ * does, and the conformance harness freezes every layer's `_today` after construction.
28
+ * This is the same seam, reachable without touching a private field; the default is the
29
+ * store's own `date.today()`, so nothing about a real run changes.
30
+ */
31
+ today?: () => string;
32
+ }
33
+ /**
34
+ * What `save` DECIDED, beside the sentence it says about it.
35
+ *
36
+ * THE POINT OF THIS TYPE IS THAT `status` IS NOT DERIVED FROM `reply`. `Memory.save` has
37
+ * four outcomes — stored, deduped, refused by validation, refused by the budget — and all
38
+ * four leave the process as one string that the MCP host records as "completed
39
+ * successfully in Nms". Anything downstream that wanted to tell them apart had exactly one
40
+ * route: match the reply text. That route is a re-derivation which breaks silently the day
41
+ * the wording improves, and it is why this field exists.
42
+ *
43
+ * `status` is one of `saved`, `duplicate`, `refused-validation`, `refused-budget`, and
44
+ * every one of them is read off a decision the code had already made — `SaveResult`'s own
45
+ * `status`, or which `catch` arm matched. `reply` is the unchanged string `save()` has
46
+ * always returned; nothing reads it.
47
+ */
48
+ export interface SaveOutcome {
49
+ readonly reply: string;
50
+ readonly status: string;
51
+ }
52
+ /**
53
+ * What `compact` DID, beside the sentence it says about it.
54
+ *
55
+ * Same seam as `SaveOutcome` and `RecallOutcome`: `status` is read off a decision the store
56
+ * already made — whether `CompactResult.archived` is empty — and never off the reply.
57
+ * `archived` (**not** deleted, see `MemoryStore.compact`) is the ONLY place the count
58
+ * survives as a number, because the reply spells it out in prose and the host records only
59
+ * that a `memory_compact` "completed successfully".
60
+ *
61
+ * `status` is `archived` or `nothing-archived`. `reply` is the unchanged string `compact()`
62
+ * has always returned; nothing reads it.
63
+ */
64
+ export interface CompactOutcome {
65
+ readonly reply: string;
66
+ readonly status: string;
67
+ readonly archived: number;
68
+ readonly indexBefore: number;
69
+ readonly indexAfter: number;
70
+ readonly budget: number;
71
+ }
72
+ /**
73
+ * What `recall` DID, beside the facts it formatted.
74
+ *
75
+ * `recall` walks the layers in order, stops the moment the budget is spent, dedupes by
76
+ * name, and — when nothing matched — picks one of three different verdicts about WHY (see
77
+ * `nothingToReport`). None of that survived into the reply the host times: an empty recall
78
+ * and a three-fact recall are the same log line to it, and the three empties are
79
+ * indistinguishable from each other even to a reader of the reply, because telling them
80
+ * apart means matching prose.
81
+ *
82
+ * `status` is `answered` or one of `empty-no-match` / `empty-unreadable-layer` /
83
+ * `empty-nothing-saved`, keyed on the same branch that chooses the verdict sentence rather
84
+ * than on the sentence. `source` is the KIND of the layer that answered first — `project`,
85
+ * `extra` or `profile` — deliberately not the layer's full label, because `extra:<name>`
86
+ * carries a directory name off the operator's disk and the log this feeds is
87
+ * metadata-only.
88
+ */
89
+ export interface RecallOutcome {
90
+ readonly reply: string;
91
+ readonly status: string;
92
+ readonly budget: number;
93
+ readonly layers: number;
94
+ readonly reached: number;
95
+ readonly returned: number;
96
+ readonly candidates: number;
97
+ readonly source: string | null;
98
+ readonly unreadable: number;
99
+ }
100
+ export declare class Memory {
101
+ readonly store: MemoryStore;
102
+ readonly k: number;
103
+ private readonly layers;
104
+ private showLayers;
105
+ /**
106
+ * How this store came to be the store, captured once. It cannot be re-derived later: the
107
+ * constructor above has already made the directory, so a store that was only DESIGNATED a
108
+ * moment ago is indistinguishable on disk from one that was found empty. `null` means the
109
+ * caller named the path outright and no resolution happened to report.
110
+ */
111
+ private readonly binding;
112
+ constructor(store: string, options?: MemoryOptions, binding?: StoreBinding | null);
113
+ /**
114
+ * Project store (resolved) + configured read-only grants + profile store.
115
+ *
116
+ * `resolveProjectStore` rather than `discoverProjectStore`: the path both return is the
117
+ * same path, but only the binding carries WHY it is that path and whether it holds
118
+ * anything — and `recall` cannot recover either fact afterwards, because constructing the
119
+ * store creates the directory.
120
+ */
121
+ static layered(start?: string | null, options?: MemoryOptions): Memory;
122
+ /** The labels, in order. `_layers` is private in the reference too; this is for the tests. */
123
+ layerLabels(): string[];
124
+ /** The reply the model reads. Unchanged, and still the tool's advertised return. */
125
+ save(type: string, name: string, description: string, body: string, links?: readonly string[] | null): string;
126
+ /**
127
+ * `save`, with the branch it took carried out alongside the sentence it wrote.
128
+ *
129
+ * Same body, same order, same four exits: the only change is that each exit now NAMES
130
+ * itself. That is the whole of the narrow seam the MCP server needs to log an outcome
131
+ * without matching text, and it moves no wording and no return type — `save()` above
132
+ * still answers `string`, and `assets/tools/memory_save.json` still advertises `str`.
133
+ */
134
+ saveOutcome(type: string, name: string, description: string, body: string, links?: readonly string[] | null): SaveOutcome;
135
+ /**
136
+ * `k` is the model asking for MORE, never for less than the store's default.
137
+ *
138
+ * Measured cause (RB-P1, qwen2.5:14b-instruct): 57 of 60 `memory_recall` calls across 12
139
+ * seeded runs sent `k: 1`, and honouring it truncated recall to the single best-scoring
140
+ * fact. The floor is the operator's configured default, not a constant, so a consumer who
141
+ * really wants top-1 says so once at construction.
142
+ */
143
+ recall(query: string, k?: number | null): string;
144
+ /**
145
+ * `recall`, carrying the walk it performed as numbers rather than as prose.
146
+ *
147
+ * The reply is byte-for-byte what `recall` has always returned; every field beside it is
148
+ * counted here because it CANNOT be recovered afterwards. How far down the layer list the
149
+ * budget got, how many of the layers reached were readable, how many facts those layers
150
+ * held against how many came back, and which of the three empty verdicts fired — all of
151
+ * it is gone by the time the string exists, and the host's log records only that a
152
+ * `memory_recall` "completed successfully".
153
+ *
154
+ * `candidates` counts `facts/*.md` in the layers actually READ, via `countFacts` — the
155
+ * same filter `MemoryStore.recall` scores over, so the ratio to `returned` is between two
156
+ * counts of one population and not between two different ones. A layer whose directory
157
+ * refuses to list contributes nothing and raises the `unreadable` count instead of being
158
+ * scored as empty; that distinction is the whole subject of `nothingToReport` below and
159
+ * must not be undone here.
160
+ */
161
+ recallOutcome(query: string, k?: number | null): RecallOutcome;
162
+ /**
163
+ * Archive the stalest facts until a fact as large as the biggest one kept will fit.
164
+ *
165
+ * The reply is the ONLY place the model learns what left the index: `archive/` is a
166
+ * directory the model calling this will never look in — if the return value does not carry
167
+ * what was lost, nothing does. Ported from `Memory.compact` in `component.py`, which is
168
+ * written for a model; the tool's advertised return is this string, unchanged.
169
+ */
170
+ compact(reserve?: number | null): string;
171
+ /**
172
+ * `compact`, with the decision it took carried beside the sentence it wrote.
173
+ *
174
+ * `this.store` is the writable project layer and nothing else: `layered` appends grants
175
+ * and the profile store to `_layers` only, never to `this.store`, so this cannot reach
176
+ * either (`test/layers.test.mjs` pins a profile fact through a compaction). The reply is
177
+ * byte-for-byte what `compact` has always returned on the reference: Python `f"{int}"`
178
+ * is a plain decimal, and `result.archive_dir` is `str(Path)`, which `archiveDir` already
179
+ * is — `MemoryStore.compact` builds it with `pyJoin`.
180
+ */
181
+ compactOutcome(reserve?: number | null): CompactOutcome;
182
+ /**
183
+ * `(index bytes, budget)` for the writable project store; bytes may be `null`.
184
+ *
185
+ * Measured exactly the way `MemoryStore.checkIndexBudget` measures — the UTF-8 length of
186
+ * `indexText()` — so the number a log records and the number a refusal was decided
187
+ * against are one number and not two that can drift apart.
188
+ *
189
+ * `null` means the store could not be read, and it is never 0: an unreadable store
190
+ * reporting an empty index is the same slander this module spends `nothingToReport`
191
+ * refusing to commit, and it would read as infinite headroom at exactly the moment there
192
+ * is none. The cost is a full parse of `facts/`, which is why nothing calls this on the
193
+ * tool path unless a log is actually enabled.
194
+ */
195
+ indexAccounting(): [indexBytes: number | null, budget: number];
196
+ /**
197
+ * An empty recall is at least three different situations; say which one.
198
+ *
199
+ * Answers `[status, text]`. The status is the SAME BRANCH that picks the sentence, handed
200
+ * out rather than re-derived: a caller that needs to tell the three empties apart — the
201
+ * MCP event log does — would otherwise have to match the prose this comment exists to say
202
+ * is allowed to improve.
203
+ *
204
+ * Until job37 all of them returned "no memories matched. Try different words", which tells
205
+ * a person to rephrase a question against a filing cabinet that may not exist.
206
+ *
207
+ * The verdict is keyed on what could have been read RIGHT NOW, counted the way
208
+ * `MemoryStore._facts` reads, not on the construction-time binding — a store that was empty
209
+ * at construction and has been saved to since really can answer. The DIAGNOSIS is keyed on
210
+ * the binding, because "designated" stops being visible on disk the moment `MemoryStore`
211
+ * creates the directory.
212
+ */
213
+ private nothingToReport;
214
+ /**
215
+ * How many facts a store holds, or `null` when that cannot be established.
216
+ *
217
+ * A store that cannot be listed answers `null` and never 0: "I could not read it" and "it
218
+ * holds nothing" are the two answers this whole unit exists to keep apart.
219
+ */
220
+ private static factCount;
221
+ /** Roots of the layers that could not be listed at all — never counted as 0. */
222
+ private unreadableLayers;
223
+ /**
224
+ * Facts the layers actually consulted could have matched, counted live.
225
+ *
226
+ * An unreadable layer contributes nothing rather than counting as empty: `recall` already
227
+ * skips it, and it must not become evidence that there was nothing to read.
228
+ */
229
+ private searchableFacts;
230
+ /**
231
+ * Is this store also the machine-wide profile layer `layered` appends?
232
+ *
233
+ * Resolved on both sides because the walk returns its store path unresolved while
234
+ * `Path.home()` may itself be a symlink; a comparison that missed on a symlink would put
235
+ * the harmful advice back exactly where it does damage.
236
+ */
237
+ private static isProfileStore;
238
+ /**
239
+ * Did the walk actually leave the directory it started in?
240
+ *
241
+ * `origin === "walk"` says a walk ran, not that it climbed. A walk that terminates at step
242
+ * zero has bound `searchedFrom`'s OWN store, and the clause that reads well for an ancestor
243
+ * binding — "which has no store of its own" — is then simply false about the commonest case
244
+ * there is: a project whose store exists and is empty.
245
+ */
246
+ private walkAscended;
247
+ /**
248
+ * Why THIS store, when the project layer is the one that cannot answer.
249
+ *
250
+ * Silent while the project store holds facts: that is the case that already works, and it
251
+ * keeps its exact wording. The remedy names `MEMORY_DIR_ENV` from `layers`, never a
252
+ * literal, so the message and the resolver cannot drift onto two different variables.
253
+ */
254
+ private bindingDiagnosis;
255
+ private format;
256
+ }