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,210 @@
1
+ import type { EventLog } from '../eventlog.js';
2
+ import { Memory } from '../memory/component.js';
3
+ /**
4
+ * The tool AND the prompt answer to this one name. Deliberately the same word: the operator
5
+ * who read `bantamkit_status` in a footer must find it in their prompt menu without
6
+ * translating, and a model that read it in a prompt must find the tool.
7
+ */
8
+ export declare const STATUS_NAME = "bantamkit_status";
9
+ /** The prompt's own title, as `prompts/list` advertises it. */
10
+ export declare const STATUS_PROMPT_TITLE = "bantamkit status";
11
+ /**
12
+ * The prompt's description — the sentence a person reads in the host's own command menu,
13
+ * which is why it is addressed to them and not to a model.
14
+ */
15
+ export declare const STATUS_PROMPT_DESCRIPTION: string;
16
+ /**
17
+ * What the prompt says after the report. The report is the payload; this is the one line
18
+ * that tells the model what the person wanted with it, and it asks for a RELAY rather than
19
+ * an interpretation — the operator invoked this to read the server's own words.
20
+ */
21
+ export declare const STATUS_PROMPT_TAIL: string;
22
+ /**
23
+ * The prompt and resource-template counts `statusReport` prints.
24
+ *
25
+ * Named here for the same reason the reference names them: neither SDK offers a public count
26
+ * of its own registrations, and reaching into a manager's private map is the habit
27
+ * `fromManifest` exists to have ended. They are PINNED AGAINST THE WIRE — the `advertisement`
28
+ * wire session drives a real `prompts/list` and `resources/templates/list` against both
29
+ * runtimes, so a registration added or removed without moving these fails the suite.
30
+ */
31
+ export declare const SERVED_PROMPTS = 1;
32
+ export declare const SERVED_RESOURCE_TEMPLATES = 2;
33
+ /**
34
+ * Percent of the index budget that has to be SPENT before the store is called degraded.
35
+ *
36
+ * 90 and not 100 because the useful moment is before the refusal, not after it: at 100% the
37
+ * next `memory_save` has already failed and the operator has already seen the error. An
38
+ * INTEGER percent, compared by cross-multiplication below, so the two runtimes cannot land
39
+ * on opposite sides of the line through a float they rounded differently.
40
+ */
41
+ export declare const INDEX_PRESSURE_PERCENT = 90;
42
+ /**
43
+ * One thing that is wrong, carried in the two forms the two surfaces need.
44
+ *
45
+ * `key` is an ASCII token from a closed set, which is the ONLY form that may reach the event
46
+ * log (`docs/eventlog.md`'s metadata-only rule); nothing here writes one today, and the field
47
+ * exists so that a future record cannot be tempted to log the prose. `sentence` is what a
48
+ * person reads. It never carries a tool argument, a memory body, a validated output, or a
49
+ * grant NAME — a layer's KIND is reportable, its name is not.
50
+ */
51
+ export interface Condition {
52
+ readonly key: string;
53
+ readonly sentence: string;
54
+ }
55
+ /** `1 prompt` / `2 prompts`. Spelled once so the two runtimes cannot disagree twice. */
56
+ export declare function plural(count: number, word: string): string;
57
+ /**
58
+ * The size of the writable store's `index.md` on disk, or `null` if it cannot be read.
59
+ *
60
+ * THE STAT, NOT THE PARSE, and the difference is the reason this can run on every tool call.
61
+ * `Memory.indexAccounting()` re-derives the index by reading every fact file — its own
62
+ * docstring says so, and the event log only calls it when a log is actually enabled. A footer
63
+ * that has to decide on EVERY call cannot pay that, so this reads the rendered artefact the
64
+ * store itself keeps up to date (`MemoryStore.rebuildIndex` writes it on every save) with one
65
+ * `stat`.
66
+ *
67
+ * What that trade costs, said plainly: a store whose facts were edited on disk behind the
68
+ * server's back has a stale `index.md`, and this reports the stale size. The condition is a
69
+ * WARNING that the budget is nearly spent, not the budget check itself —
70
+ * `MemoryStore.checkIndexBudget` is still the thing that refuses a save, and it still
71
+ * measures the parse. The two cannot disagree about a store only bantamkit has written.
72
+ *
73
+ * ABSENT IS 0 AND UNREADABLE IS `null`, split on the errno rather than collapsed. A store
74
+ * that has never been saved to has no `index.md` and really does spend nothing of its budget;
75
+ * a store whose directory refuses a `stat` has an unknown index, and reporting that as 0 would
76
+ * claim infinite headroom at exactly the moment there may be none.
77
+ */
78
+ export declare function indexBytes(memory: Memory): number | null;
79
+ /**
80
+ * Is the pack this build loads still there?
81
+ *
82
+ * `buildServer` reads every manifest eagerly at startup, so a server that is RUNNING resolved
83
+ * its pack once. It can still lose it afterwards — an upgrade that replaces the directory, a
84
+ * `BANTAMKIT_ASSETS` pointed at a scratch tree that gets cleaned up — and the failure is
85
+ * quiet: the tool descriptions were read at startup and keep being served, while
86
+ * `resources/read` and every later `loadSkill` have nothing behind them.
87
+ *
88
+ * Deliberately no path in the sentence. `assetsRoot()` resolves to DIFFERENT paths in the two
89
+ * runtimes by construction, so a path here would be the one line of this surface that cannot
90
+ * be compared across them, to buy what `--assets-root` prints on demand.
91
+ */
92
+ export declare function assetPackCondition(): Condition | null;
93
+ /**
94
+ * A memory layer that could not be LISTED — which is not a layer that held nothing.
95
+ *
96
+ * This is the sharpest of the four, because it is the one whose damage is a wrong ANSWER
97
+ * rather than a missing one: `Memory.nothingToReport` already refuses to say "nothing is
98
+ * saved" when a layer is unreadable, but that sentence only reaches a person who happened to
99
+ * run an empty recall. The footer says it on every call.
100
+ *
101
+ * KIND, NEVER NAME. A layer's label is `project`, `extra:<grant name>` or `profile`, and the
102
+ * grant name is the operator's own words for somebody's directory. The kind is the part that
103
+ * is reportable; the split is here rather than at the call site so there is exactly one place
104
+ * it can be got wrong.
105
+ *
106
+ * It reaches `Memory`'s private `layers` and `factCount` through element access on purpose,
107
+ * exactly as the reference reads `_layers` and `_fact_count`. A public accessor would be the
108
+ * right shape and would be a change to the MEMORY layer, which this unit is not authorised to
109
+ * make. Nothing is re-implemented: `factCount` is the same counter `unreadableLayers` uses, so
110
+ * "unreadable" means here exactly what it means to recall.
111
+ */
112
+ export declare function unreadableLayerCondition(memory: Memory): Condition | null;
113
+ /**
114
+ * The index is nearly as big as the budget that has to hold it.
115
+ *
116
+ * THE REMEDY NAMES THE COMMAND *THIS* INSTALL PROVIDES, and that is the one place this
117
+ * sentence is allowed to differ from `mcpserver.py`'s. The reference spells
118
+ * `python -m bantamkit.memory compact`; a pure-npm install has no `bantamkit.memory` module
119
+ * and no interpreter to run it with, so printing that here would hand the operator a command
120
+ * that cannot run. `runtime-ts` ships the same lifecycle as the `bantamkit-memory` bin
121
+ * (`src/memory/cli.ts`), so that is what is named. Ruled in `docs/porting.md`'s divergence
122
+ * table and pinned on both sides by `tools/conformance/suites/wire.mjs`.
123
+ *
124
+ * IT IS A LITERAL AND NOT AN IMPORT, exactly as the reference's is. `memory/cli.ts`'s `PROG`
125
+ * is the CLI's own name for itself and the MCP server does not import that module — the
126
+ * coupling is held from the OUTSIDE instead, by `runtime-ts/test/server.test.mjs` here and by
127
+ * `tests/test_status_surface.py`'s
128
+ * `test_the_index_remedy_names_the_command_this_install_actually_provides` there.
129
+ *
130
+ * WHAT THE REMEDY DOES NOT PROMISE. `index-budget-low` fires at >= 90% of budget, while
131
+ * `compact` archives only while the index is above `budget - reserve` and `reserve` defaults
132
+ * to the largest surviving index line. So across most of the band that prints this sentence
133
+ * the command exits 0 having archived nothing. That is the SAME arithmetic in both runtimes
134
+ * (`INDEX_PRESSURE_PERCENT` here, `MemoryStore.compact`'s default `reserve` there), so it is
135
+ * not a divergence and is not fixed here; it is registered in `docs/porting.md`. The sentence
136
+ * says "archive or shorten facts", which is what the operator has to do either way.
137
+ *
138
+ * THIS SENTENCE IS FOR THE OPERATOR; THE MODEL HAS ITS OWN REMEDY. Since job42 the model
139
+ * can call `memory_compact` (the ninth served tool) when a save is actually refused, and
140
+ * the refused-budget reply in `memory/component.ts` names it. That does not change this
141
+ * line: it is read in a status report, by a person, and the command it names is the one
142
+ * this install provides. The two remedies run the same `MemoryStore.compact`.
143
+ */
144
+ export declare function indexPressureCondition(memory: Memory): Condition | null;
145
+ /**
146
+ * The log was asked for and a record has already been lost.
147
+ *
148
+ * THE `enabled` GUARD IS LOAD-BEARING, not decorative. `writeFailed` is set inside `record`'s
149
+ * `catch` and is never cleared, so a log that was switched on, failed, and was switched off
150
+ * again would still be carrying the flag — and a server whose operator never asked for a log
151
+ * would then report itself degraded for a diagnostic that is not running. Only a log that is
152
+ * BOTH on and known to have lost a record is a problem anyone can act on.
153
+ */
154
+ export declare function eventLogCondition(log: EventLog): Condition | null;
155
+ /**
156
+ * Everything wrong right now, worst first. An empty array means healthy.
157
+ *
158
+ * ORDER IS SEVERITY AND IT IS LOAD-BEARING, because the footer shows the first one: a pack
159
+ * that vanished breaks every asset-backed surface; an unreadable layer makes recall ANSWER
160
+ * WRONGLY rather than fail; a full index refuses the next save; a broken event log costs
161
+ * diagnostics only.
162
+ *
163
+ * EVERY CONDITION IS OBSERVED, NOT INFERRED — no heartbeat, no timer, no last-seen timestamp.
164
+ * Each one is a state a test can construct and then watch this report: delete the pack, make a
165
+ * layer's `facts/` a file, build a store whose index already exceeds nine tenths of its
166
+ * budget, point the log at an unwritable path. A condition that cannot be constructed is not
167
+ * claimed.
168
+ *
169
+ * THE COST, because this runs on every tool call: one `stat` for the index, one `stat` for the
170
+ * pack, one directory listing per memory layer (two to four), and a field read for the log. No
171
+ * fact file is opened and no index is parsed.
172
+ */
173
+ export declare function degradedConditions(memory: Memory, log: EventLog): Condition[];
174
+ /**
175
+ * The one line other tools' results carry when something is wrong. `''` when nothing is.
176
+ *
177
+ * THE EMPTY STRING FOR A HEALTHY SERVER IS THE WHOLE POINT, and it is checked in a pair: the
178
+ * notice must be present on a degraded call AND absent on a healthy one, because only the pair
179
+ * proves it is conditional. A footer on every result is noise, and noise trains a reader to
180
+ * skip it — at which point the one time it matters it is invisible.
181
+ *
182
+ * ONE SHAPE, ALWAYS, including for a single condition — a count of 1 reads fine, and a second
183
+ * shape is a second thing for the port to get right. The worst condition is spelt out because
184
+ * a bare count is not actionable; the rest are a number and a pointer, because this is a
185
+ * FOOTER on somebody else's answer and has no licence to become the answer.
186
+ */
187
+ export declare function degradedNotice(conditions: readonly Condition[]): string;
188
+ /**
189
+ * The whole answer `bantamkit_status` returns — prose, for a person, in a transcript.
190
+ *
191
+ * THE FIRST LINE IS THE ANSWER and it is the line the operator asked for by name:
192
+ * `bantamkit Active 🟢`, or `bantamkit Degraded 🟠` when `conditions` is non-empty. A reader
193
+ * who stops after eight characters has still learned the thing they came for.
194
+ *
195
+ * THE ONE FIELD THAT IS NOT COMPARABLE ACROSS RUNTIMES is `build`. It is `build_id`, a
196
+ * fingerprint of the running source, and the two runtimes fingerprint two different trees by
197
+ * construction — `docs/porting.md`'s divergence table already rules exactly this. It is here
198
+ * anyway because "which bantamkit" is half the question this tool exists to answer: two
199
+ * endpoints registered under one name is the situation RB-P84 filed, and a version string
200
+ * cannot tell them apart.
201
+ *
202
+ * `identity` ARRIVES AS AN ARGUMENT rather than being built here, which is the one shape
203
+ * difference from the reference. `buildIdentity` needs the package version and the SDK version
204
+ * — both of which the server resolves and neither of which this module can see — so the
205
+ * alternative was a cycle between this file and `server.ts`. Nothing about the report changes:
206
+ * it reads the same two fields off the same object the `build_identity` tool answers with.
207
+ *
208
+ * NO ARGUMENT VALUE CAN REACH THIS. The tool takes none, and every input above is server state.
209
+ */
210
+ export declare function statusReport(memory: Memory, log: EventLog, conditions: readonly Condition[], identity: ReadonlyMap<string, unknown>, tools: number, prompts: number, templates: number): string;
@@ -0,0 +1,357 @@
1
+ /**
2
+ * `bantamkit_status`, the degraded footer, and the conditions both are built from.
3
+ *
4
+ * A port of the block of the same name in `runtime-py/src/bantamkit/mcpserver.py`. The
5
+ * contract both runtimes emit is `docs/status.md`, and `tools/conformance/suites/wire.mjs`
6
+ * compares the two reports with line 2's build digest masked — the one value
7
+ * `docs/porting.md` already rules divergent, because it fingerprints the executing tree and
8
+ * these are two trees. If you find yourself needing a SECOND mask, something is diverging
9
+ * that the contract says should not.
10
+ *
11
+ * WHY A TOOL RESULT AND NOT A NOTIFICATION, MEASURED BEFORE ANY OF THIS WAS WRITTEN.
12
+ * The request this answers is "let me SEE that the server is alive", and the obvious
13
+ * implementation — push a line at the host — does not exist on any of the five hosts
14
+ * bantamkit targets. Claude Code's own binary carries the sentence `modern protocol
15
+ * revision with no unsolicited notification path`, and its proprietary `claude/channel`
16
+ * capability is gated six ways, one of which is `provider !== "firstParty"` — that rules
17
+ * out Copilot on its own. bantamkit's host log says the same from the running side:
18
+ * `"protocolEra":"modern"` in every `Connection established` record, and ten records of
19
+ * `Channel notifications skipped: server did not declare claude/channel capability`. So the
20
+ * only surface every host is guaranteed to render is a TOOL RESULT, and that is what all
21
+ * three pieces are. Nothing here pushes and nothing here declares `claude/channel`.
22
+ *
23
+ * THE THREE PIECES AND WHO EACH ONE IS FOR:
24
+ * * `bantamkit_status` — a tool. The MODEL can call it, so an agent mid-conversation can
25
+ * answer "is this thing working" without the operator leaving the transcript.
26
+ * * a same-named PROMPT — `prompts/list` was empty on both runtimes while both advertised
27
+ * `prompts: {listChanged: false}`. A prompt is what a PERSON invokes.
28
+ * * the FOOTER — one line appended to the OTHER tools' results, and only when something is
29
+ * wrong. Never on a healthy call: a footer on every result is noise, noise trains the
30
+ * reader to stop reading, and the one time it matters it is then invisible.
31
+ */
32
+ import { statSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { AssetNotFound, assetsRoot } from '../assets.js';
35
+ import { Memory } from '../memory/component.js';
36
+ /**
37
+ * The tool AND the prompt answer to this one name. Deliberately the same word: the operator
38
+ * who read `bantamkit_status` in a footer must find it in their prompt menu without
39
+ * translating, and a model that read it in a prompt must find the tool.
40
+ */
41
+ export const STATUS_NAME = 'bantamkit_status';
42
+ /** The prompt's own title, as `prompts/list` advertises it. */
43
+ export const STATUS_PROMPT_TITLE = 'bantamkit status';
44
+ /**
45
+ * The prompt's description — the sentence a person reads in the host's own command menu,
46
+ * which is why it is addressed to them and not to a model.
47
+ */
48
+ export const STATUS_PROMPT_DESCRIPTION = "Is bantamkit actually working? Inserts the running server's own status report — active " +
49
+ 'or degraded, its version and build fingerprint, the memory store it is bound to, whether ' +
50
+ 'the event log is on, and every degraded condition in full. Invoke it when you want the ' +
51
+ 'server to answer for itself rather than ask a model to go and check.';
52
+ /**
53
+ * What the prompt says after the report. The report is the payload; this is the one line
54
+ * that tells the model what the person wanted with it, and it asks for a RELAY rather than
55
+ * an interpretation — the operator invoked this to read the server's own words.
56
+ */
57
+ export const STATUS_PROMPT_TAIL = 'Show me that report as it stands. If it says Degraded, tell me which of the problems ' +
58
+ 'above you would deal with first and why; if it says Active, say so in one line and stop.';
59
+ /**
60
+ * The prompt and resource-template counts `statusReport` prints.
61
+ *
62
+ * Named here for the same reason the reference names them: neither SDK offers a public count
63
+ * of its own registrations, and reaching into a manager's private map is the habit
64
+ * `fromManifest` exists to have ended. They are PINNED AGAINST THE WIRE — the `advertisement`
65
+ * wire session drives a real `prompts/list` and `resources/templates/list` against both
66
+ * runtimes, so a registration added or removed without moving these fails the suite.
67
+ */
68
+ export const SERVED_PROMPTS = 1;
69
+ export const SERVED_RESOURCE_TEMPLATES = 2;
70
+ /**
71
+ * Percent of the index budget that has to be SPENT before the store is called degraded.
72
+ *
73
+ * 90 and not 100 because the useful moment is before the refusal, not after it: at 100% the
74
+ * next `memory_save` has already failed and the operator has already seen the error. An
75
+ * INTEGER percent, compared by cross-multiplication below, so the two runtimes cannot land
76
+ * on opposite sides of the line through a float they rounded differently.
77
+ */
78
+ export const INDEX_PRESSURE_PERCENT = 90;
79
+ /** `1 prompt` / `2 prompts`. Spelled once so the two runtimes cannot disagree twice. */
80
+ export function plural(count, word) {
81
+ return count === 1 ? `${count} ${word}` : `${count} ${word}s`;
82
+ }
83
+ /**
84
+ * The size of the writable store's `index.md` on disk, or `null` if it cannot be read.
85
+ *
86
+ * THE STAT, NOT THE PARSE, and the difference is the reason this can run on every tool call.
87
+ * `Memory.indexAccounting()` re-derives the index by reading every fact file — its own
88
+ * docstring says so, and the event log only calls it when a log is actually enabled. A footer
89
+ * that has to decide on EVERY call cannot pay that, so this reads the rendered artefact the
90
+ * store itself keeps up to date (`MemoryStore.rebuildIndex` writes it on every save) with one
91
+ * `stat`.
92
+ *
93
+ * What that trade costs, said plainly: a store whose facts were edited on disk behind the
94
+ * server's back has a stale `index.md`, and this reports the stale size. The condition is a
95
+ * WARNING that the budget is nearly spent, not the budget check itself —
96
+ * `MemoryStore.checkIndexBudget` is still the thing that refuses a save, and it still
97
+ * measures the parse. The two cannot disagree about a store only bantamkit has written.
98
+ *
99
+ * ABSENT IS 0 AND UNREADABLE IS `null`, split on the errno rather than collapsed. A store
100
+ * that has never been saved to has no `index.md` and really does spend nothing of its budget;
101
+ * a store whose directory refuses a `stat` has an unknown index, and reporting that as 0 would
102
+ * claim infinite headroom at exactly the moment there may be none.
103
+ */
104
+ export function indexBytes(memory) {
105
+ try {
106
+ return statSync(join(memory.store.root, 'index.md')).size;
107
+ }
108
+ catch (e) {
109
+ // `ENOENT` is the reference's `FileNotFoundError`; every other system error is its bare
110
+ // `OSError`. A non-system throw is a bug here and is not swallowed.
111
+ const code = e.code;
112
+ if (code === undefined)
113
+ throw e;
114
+ return code === 'ENOENT' ? 0 : null;
115
+ }
116
+ }
117
+ /**
118
+ * Is the pack this build loads still there?
119
+ *
120
+ * `buildServer` reads every manifest eagerly at startup, so a server that is RUNNING resolved
121
+ * its pack once. It can still lose it afterwards — an upgrade that replaces the directory, a
122
+ * `BANTAMKIT_ASSETS` pointed at a scratch tree that gets cleaned up — and the failure is
123
+ * quiet: the tool descriptions were read at startup and keep being served, while
124
+ * `resources/read` and every later `loadSkill` have nothing behind them.
125
+ *
126
+ * Deliberately no path in the sentence. `assetsRoot()` resolves to DIFFERENT paths in the two
127
+ * runtimes by construction, so a path here would be the one line of this surface that cannot
128
+ * be compared across them, to buy what `--assets-root` prints on demand.
129
+ */
130
+ export function assetPackCondition() {
131
+ let root;
132
+ try {
133
+ root = assetsRoot();
134
+ }
135
+ catch (e) {
136
+ if (!(e instanceof AssetNotFound))
137
+ throw e;
138
+ return {
139
+ key: 'asset-pack-missing',
140
+ sentence: 'the asset pack cannot be resolved at all, so skills, rubrics and tool ' +
141
+ 'descriptions have nothing behind them — reinstall the package, or point ' +
142
+ 'BANTAMKIT_ASSETS at a real pack and restart the server.',
143
+ };
144
+ }
145
+ if (!isDir(root)) {
146
+ return {
147
+ key: 'asset-pack-missing',
148
+ sentence: 'the asset pack is gone from where this server resolved it, so skills, ' +
149
+ 'rubrics and tool descriptions can no longer be re-read — run ' +
150
+ '`bantamkit-mcp --assets-root` to see where it is looking, then restart.',
151
+ };
152
+ }
153
+ return null;
154
+ }
155
+ /** `Path.is_dir()`: false for a missing path and for anything that is not a directory. */
156
+ function isDir(path) {
157
+ try {
158
+ return statSync(path).isDirectory();
159
+ }
160
+ catch (e) {
161
+ if (e.code === undefined)
162
+ throw e;
163
+ return false;
164
+ }
165
+ }
166
+ /**
167
+ * A memory layer that could not be LISTED — which is not a layer that held nothing.
168
+ *
169
+ * This is the sharpest of the four, because it is the one whose damage is a wrong ANSWER
170
+ * rather than a missing one: `Memory.nothingToReport` already refuses to say "nothing is
171
+ * saved" when a layer is unreadable, but that sentence only reaches a person who happened to
172
+ * run an empty recall. The footer says it on every call.
173
+ *
174
+ * KIND, NEVER NAME. A layer's label is `project`, `extra:<grant name>` or `profile`, and the
175
+ * grant name is the operator's own words for somebody's directory. The kind is the part that
176
+ * is reportable; the split is here rather than at the call site so there is exactly one place
177
+ * it can be got wrong.
178
+ *
179
+ * It reaches `Memory`'s private `layers` and `factCount` through element access on purpose,
180
+ * exactly as the reference reads `_layers` and `_fact_count`. A public accessor would be the
181
+ * right shape and would be a change to the MEMORY layer, which this unit is not authorised to
182
+ * make. Nothing is re-implemented: `factCount` is the same counter `unreadableLayers` uses, so
183
+ * "unreadable" means here exactly what it means to recall.
184
+ */
185
+ export function unreadableLayerCondition(memory) {
186
+ const layers = memory['layers'];
187
+ const unreadable = layers.filter(([, store]) => Memory['factCount'](store.root) === null);
188
+ if (unreadable.length === 0)
189
+ return null;
190
+ const kinds = [...new Set(unreadable.map(([label]) => label.split(':')[0]))].sort();
191
+ return {
192
+ key: 'memory-layer-unreadable',
193
+ sentence: `${plural(unreadable.length, 'memory layer')} could not be read ` +
194
+ `(${plural(kinds.length, 'kind')}: ${kinds.join(', ')}), so an empty recall is not ` +
195
+ 'evidence that nothing is saved — check that those store directories exist and ' +
196
+ 'are readable.',
197
+ };
198
+ }
199
+ /**
200
+ * The index is nearly as big as the budget that has to hold it.
201
+ *
202
+ * THE REMEDY NAMES THE COMMAND *THIS* INSTALL PROVIDES, and that is the one place this
203
+ * sentence is allowed to differ from `mcpserver.py`'s. The reference spells
204
+ * `python -m bantamkit.memory compact`; a pure-npm install has no `bantamkit.memory` module
205
+ * and no interpreter to run it with, so printing that here would hand the operator a command
206
+ * that cannot run. `runtime-ts` ships the same lifecycle as the `bantamkit-memory` bin
207
+ * (`src/memory/cli.ts`), so that is what is named. Ruled in `docs/porting.md`'s divergence
208
+ * table and pinned on both sides by `tools/conformance/suites/wire.mjs`.
209
+ *
210
+ * IT IS A LITERAL AND NOT AN IMPORT, exactly as the reference's is. `memory/cli.ts`'s `PROG`
211
+ * is the CLI's own name for itself and the MCP server does not import that module — the
212
+ * coupling is held from the OUTSIDE instead, by `runtime-ts/test/server.test.mjs` here and by
213
+ * `tests/test_status_surface.py`'s
214
+ * `test_the_index_remedy_names_the_command_this_install_actually_provides` there.
215
+ *
216
+ * WHAT THE REMEDY DOES NOT PROMISE. `index-budget-low` fires at >= 90% of budget, while
217
+ * `compact` archives only while the index is above `budget - reserve` and `reserve` defaults
218
+ * to the largest surviving index line. So across most of the band that prints this sentence
219
+ * the command exits 0 having archived nothing. That is the SAME arithmetic in both runtimes
220
+ * (`INDEX_PRESSURE_PERCENT` here, `MemoryStore.compact`'s default `reserve` there), so it is
221
+ * not a divergence and is not fixed here; it is registered in `docs/porting.md`. The sentence
222
+ * says "archive or shorten facts", which is what the operator has to do either way.
223
+ *
224
+ * THIS SENTENCE IS FOR THE OPERATOR; THE MODEL HAS ITS OWN REMEDY. Since job42 the model
225
+ * can call `memory_compact` (the ninth served tool) when a save is actually refused, and
226
+ * the refused-budget reply in `memory/component.ts` names it. That does not change this
227
+ * line: it is read in a status report, by a person, and the command it names is the one
228
+ * this install provides. The two remedies run the same `MemoryStore.compact`.
229
+ */
230
+ export function indexPressureCondition(memory) {
231
+ const size = indexBytes(memory);
232
+ const budget = memory.store.indexBudget;
233
+ // INTEGER CROSS-MULTIPLICATION, never `size / budget >= 0.9`. Two runtimes that each
234
+ // rounded their own float could land on opposite sides of the line for the same store,
235
+ // and this comparison is the whole reason they cannot.
236
+ if (size === null || size * 100 < INDEX_PRESSURE_PERCENT * budget)
237
+ return null;
238
+ return {
239
+ key: 'index-budget-low',
240
+ sentence: `the memory index is ${size} bytes of a ${budget}-byte budget, so the next save ` +
241
+ 'is close to being refused — archive or shorten facts with ' +
242
+ '`bantamkit-memory compact`.',
243
+ };
244
+ }
245
+ /**
246
+ * The log was asked for and a record has already been lost.
247
+ *
248
+ * THE `enabled` GUARD IS LOAD-BEARING, not decorative. `writeFailed` is set inside `record`'s
249
+ * `catch` and is never cleared, so a log that was switched on, failed, and was switched off
250
+ * again would still be carrying the flag — and a server whose operator never asked for a log
251
+ * would then report itself degraded for a diagnostic that is not running. Only a log that is
252
+ * BOTH on and known to have lost a record is a problem anyone can act on.
253
+ */
254
+ export function eventLogCondition(log) {
255
+ if (!(log.enabled && log.writeFailed))
256
+ return null;
257
+ return {
258
+ key: 'event-log-unwritable',
259
+ sentence: 'the event log is switched on but a write to it has already failed, so tool ' +
260
+ 'outcomes are going unrecorded — check the path in BANTAMKIT_EVENT_LOG and ' +
261
+ 'whether its directory is writable.',
262
+ };
263
+ }
264
+ /**
265
+ * Everything wrong right now, worst first. An empty array means healthy.
266
+ *
267
+ * ORDER IS SEVERITY AND IT IS LOAD-BEARING, because the footer shows the first one: a pack
268
+ * that vanished breaks every asset-backed surface; an unreadable layer makes recall ANSWER
269
+ * WRONGLY rather than fail; a full index refuses the next save; a broken event log costs
270
+ * diagnostics only.
271
+ *
272
+ * EVERY CONDITION IS OBSERVED, NOT INFERRED — no heartbeat, no timer, no last-seen timestamp.
273
+ * Each one is a state a test can construct and then watch this report: delete the pack, make a
274
+ * layer's `facts/` a file, build a store whose index already exceeds nine tenths of its
275
+ * budget, point the log at an unwritable path. A condition that cannot be constructed is not
276
+ * claimed.
277
+ *
278
+ * THE COST, because this runs on every tool call: one `stat` for the index, one `stat` for the
279
+ * pack, one directory listing per memory layer (two to four), and a field read for the log. No
280
+ * fact file is opened and no index is parsed.
281
+ */
282
+ export function degradedConditions(memory, log) {
283
+ return [
284
+ assetPackCondition(),
285
+ unreadableLayerCondition(memory),
286
+ indexPressureCondition(memory),
287
+ eventLogCondition(log),
288
+ ].filter((condition) => condition !== null);
289
+ }
290
+ /**
291
+ * The one line other tools' results carry when something is wrong. `''` when nothing is.
292
+ *
293
+ * THE EMPTY STRING FOR A HEALTHY SERVER IS THE WHOLE POINT, and it is checked in a pair: the
294
+ * notice must be present on a degraded call AND absent on a healthy one, because only the pair
295
+ * proves it is conditional. A footer on every result is noise, and noise trains a reader to
296
+ * skip it — at which point the one time it matters it is invisible.
297
+ *
298
+ * ONE SHAPE, ALWAYS, including for a single condition — a count of 1 reads fine, and a second
299
+ * shape is a second thing for the port to get right. The worst condition is spelt out because
300
+ * a bare count is not actionable; the rest are a number and a pointer, because this is a
301
+ * FOOTER on somebody else's answer and has no licence to become the answer.
302
+ */
303
+ export function degradedNotice(conditions) {
304
+ if (conditions.length === 0)
305
+ return '';
306
+ return (`⚠️ bantamkit degraded (${conditions.length}): ${conditions[0].sentence} ` +
307
+ 'Call `bantamkit_status` for the full report.');
308
+ }
309
+ /**
310
+ * The whole answer `bantamkit_status` returns — prose, for a person, in a transcript.
311
+ *
312
+ * THE FIRST LINE IS THE ANSWER and it is the line the operator asked for by name:
313
+ * `bantamkit Active 🟢`, or `bantamkit Degraded 🟠` when `conditions` is non-empty. A reader
314
+ * who stops after eight characters has still learned the thing they came for.
315
+ *
316
+ * THE ONE FIELD THAT IS NOT COMPARABLE ACROSS RUNTIMES is `build`. It is `build_id`, a
317
+ * fingerprint of the running source, and the two runtimes fingerprint two different trees by
318
+ * construction — `docs/porting.md`'s divergence table already rules exactly this. It is here
319
+ * anyway because "which bantamkit" is half the question this tool exists to answer: two
320
+ * endpoints registered under one name is the situation RB-P84 filed, and a version string
321
+ * cannot tell them apart.
322
+ *
323
+ * `identity` ARRIVES AS AN ARGUMENT rather than being built here, which is the one shape
324
+ * difference from the reference. `buildIdentity` needs the package version and the SDK version
325
+ * — both of which the server resolves and neither of which this module can see — so the
326
+ * alternative was a cycle between this file and `server.ts`. Nothing about the report changes:
327
+ * it reads the same two fields off the same object the `build_identity` tool answers with.
328
+ *
329
+ * NO ARGUMENT VALUE CAN REACH THIS. The tool takes none, and every input above is server state.
330
+ */
331
+ export function statusReport(memory, log, conditions, identity, tools, prompts, templates) {
332
+ const build = identity.get('build_id');
333
+ const facts = Memory['factCount'](memory.store.root);
334
+ const size = indexBytes(memory);
335
+ const budget = memory.store.indexBudget;
336
+ const lines = [
337
+ `bantamkit ${conditions.length > 0 ? 'Degraded 🟠' : 'Active 🟢'}`,
338
+ `version ${identity.get('version')}, build ` +
339
+ (typeof build === 'string' ? build : 'unavailable'),
340
+ `serving ${plural(tools, 'tool')}, ${plural(prompts, 'prompt')}, ` +
341
+ `${plural(templates, 'resource template')}`,
342
+ 'memory: ' +
343
+ (facts === null
344
+ ? 'the project store could not be read'
345
+ : `${plural(facts, 'fact')} in the project store`) +
346
+ ', index ' +
347
+ (size === null ? 'unreadable' : String(size)) +
348
+ ` of ${budget} bytes`,
349
+ `event log: ${log.enabled ? 'on' : 'off'}`,
350
+ ];
351
+ if (conditions.length > 0) {
352
+ lines.push(`${plural(conditions.length, 'problem')}:`);
353
+ for (const condition of conditions)
354
+ lines.push(`- ${condition.sentence}`);
355
+ }
356
+ return lines.join('\n');
357
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The stdio transport, with the two things the SDK's own one cannot give this port.
3
+ *
4
+ * 1. THE RAW ARGUMENT BYTES. `StdioServerTransport` hands the server `JSON.parse(line)`, and
5
+ * N6 measured what that costs: a `5.0` that has been through `JSON.parse` is
6
+ * indistinguishable from `5`, so an accounting field arrives as an integer and the
7
+ * `.log.jsonl` line Python writes as `5.0` gets written as `5`. Nothing errors. The file
8
+ * just stops being diffable, silently, from the first successful clock-out.
9
+ *
10
+ * The SDK exposes no hook for this — `ReadBuffer.readMessage` parses inside the buffer and
11
+ * the buffer is private — so the buffering is done here instead. It is thirty lines
12
+ * (`indexOf('\n')`, strip a trailing `\r`) and it keeps the LINE next to the parsed
13
+ * message, keyed by JSON-RPC id. A handler then re-reads its own arguments with
14
+ * `pyjson.parseJson`, which tags `5.0` as a float and keeps an integer past 2^53 exact.
15
+ *
16
+ * 2. THE OUTGOING NUMBER LITERALS AND THE ENVELOPE ORDER. `serializeMessage` is
17
+ * `JSON.stringify(message) + '\n'`, which un-does the work above on the way out: the same
18
+ * `5.0` in a `structuredContent` leaves as `5`. So a tool handler registers the EXACT text
19
+ * of its result here, keyed by request id, and `send` splices it in. Everything else still
20
+ * goes through `JSON.stringify`, which is byte-for-byte what pydantic-core writes for
21
+ * strings, bools, nulls and safe integers.
22
+ *
23
+ * While the envelope is being built by hand anyway, its keys are written in the reference's
24
+ * order (`jsonrpc`, `id`, `result`) rather than the SDK's (`result`, `jsonrpc`, `id`). Key
25
+ * order is not observable to a conforming client, but it costs nothing here and it removes
26
+ * a difference from the list rather than adding a ruling to it.
27
+ *
28
+ * NOTHING IN THIS FILE WRITES TO STDOUT EXCEPT `send`. That is the invariant the whole
29
+ * package is under, and it is checked by `test/server.test.mjs`, which asserts that every
30
+ * line a real session produced parses as a frame and that stdout never ends mid-line.
31
+ */
32
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
33
+ import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
34
+ type Readable = NodeJS.ReadableStream;
35
+ type Writable = NodeJS.WritableStream;
36
+ export declare class RawStdioTransport implements Transport {
37
+ private readonly input;
38
+ private readonly output;
39
+ onclose?: () => void;
40
+ onerror?: (error: Error) => void;
41
+ onmessage?: (message: JSONRPCMessage) => void;
42
+ sessionId?: string;
43
+ private buffer;
44
+ private started;
45
+ private readonly rawLines;
46
+ private readonly exactResults;
47
+ private readonly ondata;
48
+ private readonly onstreamerror;
49
+ constructor(input?: Readable, output?: Writable);
50
+ /** The verbatim request line that carried this id, or `undefined` if it is gone. */
51
+ rawLineFor(id: unknown): string | undefined;
52
+ /** Register the exact JSON text to send as this request's `result`. */
53
+ setExactResult(id: unknown, json: string): void;
54
+ start(): Promise<void>;
55
+ close(): Promise<void>;
56
+ send(message: JSONRPCMessage): Promise<void>;
57
+ /**
58
+ * One frame, in the reference's key order, with an exact `result` when one was registered.
59
+ *
60
+ * The raw line and the exact text are both dropped here rather than in the handler: the
61
+ * handler cannot know whether the protocol will actually send its answer (a cancelled
62
+ * request never reaches `send`), and a map that only ever grows is a leak in a process
63
+ * that is meant to run for a session.
64
+ */
65
+ private frame;
66
+ private consume;
67
+ }
68
+ export {};