bantamkit-mcp 0.30.0 → 0.31.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.
- package/README.md +83 -2
- package/assets/pricing/default.json +7 -0
- package/assets/schemas/shiftwork-checkpoint.json +12 -1
- package/assets/tools/shiftwork_clock_out.json +1 -1
- package/assets/tools/token_ledger.json +40 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +135 -4
- package/dist/eventlog.js +6 -5
- package/dist/mcp/identity.d.ts +80 -0
- package/dist/mcp/identity.js +466 -4
- package/dist/mcp/pyargs.js +10 -0
- package/dist/mcp/server.d.ts +3 -2
- package/dist/mcp/server.js +58 -1
- package/dist/mcp/status.d.ts +54 -10
- package/dist/mcp/status.js +86 -10
- package/dist/memory/cli.js +1 -1
- package/dist/memory/store.d.ts +56 -0
- package/dist/memory/store.js +71 -2
- package/dist/pricing.d.ts +100 -0
- package/dist/pricing.js +357 -0
- package/dist/selfupdate.d.ts +320 -0
- package/dist/selfupdate.js +594 -0
- package/dist/shiftwork.d.ts +5 -3
- package/dist/shiftwork.js +82 -4
- package/dist/tokenledger.d.ts +91 -0
- package/dist/tokenledger.js +372 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bantamkit-mcp
|
|
2
2
|
|
|
3
3
|
The bantamkit MCP server as a pure-Node package: `npx bantamkit-mcp`, no Python, no
|
|
4
|
-
`uv`, no `pipx`, no interpreter bootstrap. It serves the same
|
|
4
|
+
`uv`, no `pipx`, no interpreter bootstrap. It serves the same fourteen tools, the same
|
|
5
5
|
`bantamkit_status` prompt and the same two resource templates as `runtime-py`'s server,
|
|
6
6
|
reads and writes the same memory store, and is checked against the Python server frame by
|
|
7
7
|
frame — 4500+ conformance cases, with every intentional difference written down as a
|
|
@@ -278,7 +278,74 @@ no repository, and reading a checkout's HEAD would describe the tree rather than
|
|
|
278
278
|
that were imported (RB-P84).
|
|
279
279
|
|
|
280
280
|
**Pin the version in the config** if you want this to be a non-issue:
|
|
281
|
-
`"args": ["-y", "bantamkit-mcp@0.
|
|
281
|
+
`"args": ["-y", "bantamkit-mcp@0.30.0"]`.
|
|
282
|
+
|
|
283
|
+
## Updating
|
|
284
|
+
|
|
285
|
+
**There is no `bantamkit-mcp --update`, deliberately.** The package manager that installed
|
|
286
|
+
this is the thing that updates it, and there are five install shapes with five different
|
|
287
|
+
answers. Pick the row you are actually on — and note that **the running server keeps
|
|
288
|
+
serving the code it loaded at startup**, so every row ends with restarting it in the host.
|
|
289
|
+
|
|
290
|
+
> **AMENDED 2026-09-11 — there IS a `bantamkit-mcp --update` now, and the paragraph above is
|
|
291
|
+
> kept rather than rewritten because everything in it is still true of what the flag does.**
|
|
292
|
+
> `bantamkit-mcp --update` asks the npm registry for `latest` and compares it to the version
|
|
293
|
+
> that is installed. If they match it prints both numbers and `up to date.` and stops. If the
|
|
294
|
+
> index is ahead **and this is a registry install**, it runs the row below that applies to you
|
|
295
|
+
> and prints what npm said. If the index is ahead and this is any OTHER shape, **it refuses,
|
|
296
|
+
> exits 1, and names the row you are on** — it will not write a registry install into a tree
|
|
297
|
+
> you manage with `git`, and a command that exits 0 having changed nothing is worse than one
|
|
298
|
+
> that says no. Either way it ends by telling you to restart the server, for the reason the
|
|
299
|
+
> paragraph above gives: a successful update does not change the process that is answering
|
|
300
|
+
> you. The flag reaches the network and nothing else here does — not at startup, not on
|
|
301
|
+
> `bantamkit_status`, not on any tool — with a 10-second timeout, and being offline is a named
|
|
302
|
+
> refusal on stderr, never a traceback. The table below is still the reference for what to do
|
|
303
|
+
> by hand, and it is what `--update` prints back at you when it will not act.
|
|
304
|
+
|
|
305
|
+
| how it was installed | how to update |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `npx -y bantamkit-mcp` in the host config | nothing to update — but npx **caches the resolved version**, so add `@latest` (or a pinned `@0.30.0`) or it will keep serving what it resolved weeks ago. `rm -rf ~/.npm/_npx` forces a clean resolve. |
|
|
308
|
+
| `npm i -g bantamkit-mcp` | `npm i -g bantamkit-mcp@latest` |
|
|
309
|
+
| `npm i --prefix <dir> bantamkit-mcp` | `npm i --prefix <dir> bantamkit-mcp@latest` |
|
|
310
|
+
| PyPI (`pip install "bantamkit[mcp]"`) | `pip install -U "bantamkit[mcp]"` · pipx: `pipx upgrade bantamkit` · uv: `uv tool upgrade bantamkit` |
|
|
311
|
+
| a checkout, via `tools/bantamkit-mcp-node` | `git pull && npm ci --prefix runtime-ts && npm run build --prefix runtime-ts` — this one never touches a registry, and `runtime-ts/dist/` is build output, so a pull alone changes nothing |
|
|
312
|
+
|
|
313
|
+
**Then restart the server in your host**, or you will keep talking to the old build. In
|
|
314
|
+
Claude Code that is `/mcp` → reconnect; in Claude Desktop it is a full restart of the app.
|
|
315
|
+
Measured 2026-09-07 (served-tools: dated — the surface was eleven then; 0.30.0's
|
|
316
|
+
`memory_dream` and `repo_map` made it thirteen): a checkout whose `dist/` had just been
|
|
317
|
+
rebuilt at 0.30.0 kept answering `version 0.29.1, serving 11 tools` until the host
|
|
318
|
+
reconnected — the disk was current and the process was not.
|
|
319
|
+
|
|
320
|
+
**Verify with `bantamkit_status`, not with the install log.** It reports the version and
|
|
321
|
+
the `build_id` of *the code that is answering you*:
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
bantamkit Active 🟢
|
|
325
|
+
version 0.30.0, build sha256:4441619d…
|
|
326
|
+
serving 14 tools, 1 prompt, 2 resource templates
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
A version string that moved and a `build_id` that did not means you are reading a config,
|
|
330
|
+
not a process.
|
|
331
|
+
|
|
332
|
+
### The failure this table exists for
|
|
333
|
+
|
|
334
|
+
Measured on a real machine, 2026-09-07: a Claude Desktop entry pointed at
|
|
335
|
+
`~/.local/share/bantamkit-mcp/node_modules/.bin/bantamkit-mcp`, which was **0.25.0** — five
|
|
336
|
+
releases stale — and its `package.json` declared
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
"bantamkit-mcp": "file:/private/tmp/.../scratchpad/bantamkit-mcp-0.25.0.tgz"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
a **local tarball in a temp directory that no longer existed**. `npm update` in that
|
|
343
|
+
directory cannot help: the dependency does not name a registry. The fix is to install over
|
|
344
|
+
it from the registry (`npm i --prefix ~/.local/share/bantamkit-mcp bantamkit-mcp@latest`),
|
|
345
|
+
which restores a normal semver dependency and leaves the host config's path valid.
|
|
346
|
+
|
|
347
|
+
If you install from a local `.tgz` to test a build, **install over it from the registry
|
|
348
|
+
afterwards**, or that machine is pinned to a file that will be deleted.
|
|
282
349
|
|
|
283
350
|
## The second bin: `bantamkit-memory`
|
|
284
351
|
|
|
@@ -300,6 +367,20 @@ second bin instead of a subcommand: lifecycle output on the server's stdout woul
|
|
|
300
367
|
the transport, and `bantamkit-mcp`'s help is a byte-compared artifact against
|
|
301
368
|
`python -m bantamkit.mcpserver -h`, which a subparsers action would move.
|
|
302
369
|
|
|
370
|
+
**Amendment, 2026-09-11 (J46-26/J46-27).** "Not a thing you run by hand" is now half
|
|
371
|
+
true and the half that changed is worth knowing. Typing `bantamkit-mcp` at a prompt
|
|
372
|
+
with nothing after it no longer opens a mute server and blocks — it prints the help,
|
|
373
|
+
on stdout, exit 0, the same bytes `-h` prints. The discrimination is whether **stdin is
|
|
374
|
+
a terminal** and nothing else, so every host launch is unchanged: `"args": []` down a
|
|
375
|
+
pipe still starts the server and still answers `initialize`. The paragraph above stays
|
|
376
|
+
true of what it was describing — stdout is the wire, and nothing but frames goes down
|
|
377
|
+
it when a host is on the other end. What is new is that typing the command to see what
|
|
378
|
+
it does is now a reasonable thing to do. An argument after the command is an operator
|
|
379
|
+
asking for a configured server and still gets one: `bantamkit-mcp --store /tmp/x` at a
|
|
380
|
+
terminal serves. Compared between the two runtimes by
|
|
381
|
+
`tools/conformance/suites/cli.mjs` (`bare-at-a-tty`, `flagged-at-a-tty`,
|
|
382
|
+
`bare-over-a-pipe`).
|
|
383
|
+
|
|
303
384
|
Five subcommands, scoped to the writable **project** layer only — read-only grants and the
|
|
304
385
|
profile store are out of its reach by the code path, not by convention:
|
|
305
386
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"currency": "USD",
|
|
4
|
+
"unit": "micro_usd_per_million_tokens",
|
|
5
|
+
"note": "Ships with NO rates, deliberately. bantamkit is offline and nothing in this repository or on the machine it was built on carries a published price, so every rate here would have had to be recalled rather than sourced - which is the unfalsifiable figure this project exists to refuse. The mechanism is complete and the refusal path is therefore the DEFAULT: price_tokens answers {\"unavailable\": ...} for every model until an operator records a rate. To record one, copy this file, add an entry under 'rates', and point BANTAMKIT_PRICES at the copy. Each entry needs 'recorded' (YYYY-MM-DD, the day YOU read the rate), 'source' (where you read it, so the next reader can re-derive it), and one or more of the four token classes priced in micro-USD per 1,000,000 tokens - so $3.00 per million input tokens is 3000000. The loader REFUSES a rate that has no date or no source: a number in this file without both is not a fact.",
|
|
6
|
+
"rates": {}
|
|
7
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://bantamkit.dev/schemas/shiftwork-checkpoint.json",
|
|
4
4
|
"title": "Shift-work checkpoint contract v1",
|
|
5
|
-
"$comment": "Encodes checkpoint contract v1 (docs/superpowers/specs/2026-08-10-shift-work-checkpoint-contract-draft.md) plus the two driver-forced deltas: plan.units[].role (model dispatch without reading briefs) and state.external[].until_cmd (machine-checkable wait, replacing prose `until`). Re-planning needs no schema change: a planner unit is just a unit. STRICTNESS POSTURE: `version` is const-pinned to 1 so a reader can refuse unknown majors; every object the driver or a session reads by field name sets additionalProperties:false, and every field the draft's schema block shows on every instance is required (empty arrays are legal, so a fresh checkpoint is cheap to write). The two exceptions are `history[]` and `retro[]` items: those are the free-text annotation space the draft treats as notes, so they allow extra keys (e.g. a session adding `seq` or `tokens`) while still requiring their load-bearing fields. `units[].commits` is optional because the draft only carries it on completed units. `state.external[].status` is a non-empty string rather than an enum: the draft closes the enum for unit status (the driver's SUCCESS test) but never enumerates external status, and inventing values here would reject honest checkpoints. `plan.cursor` must name an existing unit id — JSON Schema cannot express that cross-reference, so the driver checks it structurally and escalates on a dangling cursor. Serialization: checkpoints are YAML for humans per the draft, but the v1 driver reads JSON only (stdlib-only rule), so sessions driven by driver.py write .json; this schema validates the parsed document either way.",
|
|
5
|
+
"$comment": "Encodes checkpoint contract v1 (docs/superpowers/specs/2026-08-10-shift-work-checkpoint-contract-draft.md) plus the two driver-forced deltas: plan.units[].role (model dispatch without reading briefs) and state.external[].until_cmd (machine-checkable wait, replacing prose `until`). Re-planning needs no schema change: a planner unit is just a unit. STRICTNESS POSTURE: `version` is const-pinned to 1 so a reader can refuse unknown majors; every object the driver or a session reads by field name sets additionalProperties:false, and every field the draft's schema block shows on every instance is required (empty arrays are legal, so a fresh checkpoint is cheap to write). The two exceptions are `history[]` and `retro[]` items: those are the free-text annotation space the draft treats as notes, so they allow extra keys (e.g. a session adding `seq` or `tokens`) while still requiring their load-bearing fields. `units[].commits` is optional because the draft only carries it on completed units. `state.external[].status` is a non-empty string rather than an enum: the draft closes the enum for unit status (the driver's SUCCESS test) but never enumerates external status, and inventing values here would reject honest checkpoints. `plan.cursor` must name an existing unit id — JSON Schema cannot express that cross-reference, so the driver checks it structurally and escalates on a dangling cursor. Serialization: checkpoints are YAML for humans per the draft, but the v1 driver reads JSON only (stdlib-only rule), so sessions driven by driver.py write .json; this schema validates the parsed document either way. AMENDMENT (job46/AS-2, docs/roadmap-agent-stack.md): `job.roles` is a THIRD v1 addition, beyond the two driver-forced deltas named above, and it is OPTIONAL — a checkpoint that omits it is exactly the document the rest of this comment describes, and every checkpoint written before it existed still validates unchanged. Both CLAUDE.md files say the model a unit runs on is `per role, never random, always logged`; logged it was, in `history[]`/`retro[]`, which are the two objects here that allow extra keys, so nothing could ever compare the model a role was ALLOWED to use against the model that answered. `job.roles` is that declaration, and it sits on `job` because that is where a reader already looks for the rules binding every unit. It is a MAP (unit role -> the model identifiers that role may report), not a record the driver reads by fixed field name, so the closure the rest of this file spells `additionalProperties: false` is spelled here as `propertyNames` over the SAME three names `plan.units[].role` enumerates, with `additionalProperties` carrying the value schema once instead of three times: a key outside the role enum is refused, and so is a value that is not a non-empty array of non-empty strings (an empty list would be a rule no session could satisfy, which is a typo and not a policy). A role the map does not name is unconstrained — declaring one role does not silently forbid the others. The ENFORCEMENT is not here: this file states the contract and the runtimes state the refusal, so no example error text appears in these descriptions. `version` stays 1 because nothing already in the document changes meaning: a reader that ignored `roles` would still read every other field correctly. What it would not do is enforce the rule — and since `job` is closed, an older reader does not ignore the key, it refuses the whole file. That refusal is the intended behaviour and not a gap: opening `job` so an old reader could skip the key is what would make this check bypassable by running an older bantamkit.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["version", "job", "plan", "state", "history", "retro", "handoff"],
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -27,6 +27,17 @@
|
|
|
27
27
|
"type": "array",
|
|
28
28
|
"items": {"type": "string", "minLength": 1},
|
|
29
29
|
"description": "Invariants binding every unit, copied verbatim into each session."
|
|
30
|
+
},
|
|
31
|
+
"roles": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "OPTIONAL. Per unit role, the model identifiers a session in that role may report. Omit it and the job behaves as it always has: the model is recorded and nothing compares it to anything. A role this map does not name is unconstrained; only a role named here is held to its list. Keys are the `plan.units[].role` enum, so a role that is not a role is refused rather than quietly ignored.",
|
|
34
|
+
"propertyNames": {"enum": ["planner", "implementer", "reviewer"]},
|
|
35
|
+
"additionalProperties": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"minItems": 1,
|
|
38
|
+
"items": {"type": "string", "minLength": 1},
|
|
39
|
+
"description": "The model identifiers this role may report, spelled exactly as a session logs them. Non-empty: a role allowed no model is a typo, not a policy."
|
|
40
|
+
}
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shiftwork_clock_out",
|
|
3
|
-
"description": "Shift-work clock-out: record a finished unit — set its status, advance plan.cursor, merge handoff_patch, push history_entry onto the 5-entry ring — validating the whole mutated document against the checkpoint schema BEFORE an atomic write (a failure writes nothing and returns result=error). Every success appends one accounting line (unit, role, status, ts, plus your accounting fields, e.g. tokens/duration/model) to <checkpoint>.log.jsonl.",
|
|
3
|
+
"description": "Shift-work clock-out: record a finished unit — set its status, advance plan.cursor, merge handoff_patch, push history_entry onto the 5-entry ring — validating the whole mutated document against the checkpoint schema BEFORE an atomic write (a failure writes nothing and returns result=error). Every success appends one accounting line (unit, role, status, ts, plus your accounting fields, e.g. tokens/duration/model) to <checkpoint>.log.jsonl. If the checkpoint's job.roles names this unit's role, accounting.model must be one of that role's model identifiers, spelled exactly \u2014 a model that is not on the list, or none reported at all, is refused before anything is written; a role job.roles omits, and a checkpoint carrying no job.roles, are unconstrained.",
|
|
4
4
|
"surfaces": [
|
|
5
5
|
"mcp"
|
|
6
6
|
],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "token_ledger",
|
|
3
|
+
"description": "What a session actually cost, read off the host's own transcripts. Walks `root` for `*.jsonl` transcripts -- the host writes `<cwd-slug>/<session>.jsonl` and `<cwd-slug>/<session>/subagents/*.jsonl` beneath a projects directory, which on this machine is `~/.claude/projects` -- and answers ONE JSON document: `root` (echoed as given), `transcripts` (files read), `lines` (records seen), `requests` (distinct API requests counted), `totals` (the four token classes), `sessions` (one row each), `omissions` (every line that was read and not counted), and `cost` only when `model` is given. This is the ONLY reader of the token counts: `docs/eventlog.md`'s four event streams record decisions, injections, tool calls and the host's arrival log, and NONE of the four carries a token count. Every figure here is the API's own `usage` block and nothing is estimated -- the byte-derived `est` figures in `tools/ledger/token-ledger.mjs` (tool_result bytes, repeated Reads) are deliberately NOT served, because an estimate handed to a model through a tool is an estimate that will be quoted back as a fact. COUNTING IS PER `requestId`, NEVER PER LINE: the host writes one API response as several assistant records -- a text block, a tool_use block -- which all carry the SAME `usage`, so summing records overcounts by the number of content blocks. The dedupe spans the WHOLE walk and not one file, because a resumed session rewrites earlier records verbatim into a new file and a per-file rule counts that request twice; the first occurrence in walk order wins and every later copy is reported as a `duplicate-request` omission rather than dropped in silence. EVERY LINE IS COUNTED OR OMITTED AND THE TWO ADD UP: `lines` equals `requests` plus the sum of every omission's `count`, always, so 'your transcripts hold no usage' is distinguishable from 'I skipped most of your transcripts'. An omission is `{subject, count, what}` with subject one of `undecodable-file`, `unparsed-line`, `not-an-object`, `no-session-id`, `not-an-assistant-record`, `no-usage`, `malformed-usage`, `no-request-id`, `duplicate-request`, and `what` naming the FIRST site as `<relpath>:<line>` plus `and N more` -- bounded on purpose, because a real corpus omits tens of thousands of lines as `not-an-assistant-record` (the host writes attachments, mode changes and titles into the same file) and listing them all would hand back a megabyte of paths in place of an answer. A blank line is not a record and is counted nowhere. A record is counted only when it carries a non-empty `sessionId`, `type` of `assistant`, a `message.usage` object holding ALL FOUR of `input_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens` and `output_tokens` as non-negative integers below 2**53, and a non-empty `requestId`; the host's other `usage` keys (`service_tier`, `iterations`, `server_tool_use`) are ignored, but a MISSING class is `malformed-usage` and never a zero, because a class defaulted to zero is a token invented. A session row carries `cwd` (the first one seen), `first` and `last` (the minimum and maximum timestamp, as strings), `requests`, `sidechain_requests` and the four classes. `sidechain_requests` is stated rather than hidden because it is an ASYMMETRY a caller would otherwise discover late: a subagent's transcript carries its PARENT's `sessionId` with `isSidechain: true`, so its cost lands in the parent session and is not a session of its own. Sessions are ordered by `first` as a STRING then by session id, both in code-point order, never by parsed time -- the host writes ISO-8601 with a `Z`, for which byte order already is time order. Directory listings are sorted by name in code-point order at every level, so the walk order -- which decides which copy of a duplicated `requestId` counts -- is a property of the tree and not of the filesystem. THERE IS NO TIME WINDOW, deliberately: the operator script has `--days N` and this has nothing, because a tool whose answer depends on the wall clock cannot be pinned by a gate that runs twice, and 'the last 7 days' is a question about a live machine rather than a fact about a corpus. `root` is the CALLER's, exactly as `skill_audit`'s is: this tool reads nothing but `root`, so it can be pointed at a frozen corpus and asked the same question twice. Pass `model` to convert the totals into money through the price table; `assets/pricing/default.json` ships with NO rates on purpose, so `{\"unavailable\": \"no rate recorded for model ...\"}` is the NORMAL answer and not an error path -- a rate enters only as an operator's own fact with its date and its source, through `$BANTAMKIT_PRICES` or through `prices`. `cost` carries `micros` (integer micro-USD), `amount` (a fixed-6-decimal string), a per-class `breakdown` and the `rate` that produced it; the four classes are priced separately because 98.1 % of a real prompt measured here was `cache_read`, and one averaged rate would be wrong in exactly that direction. Four argument failures are refused with their own sentences and never as a partial answer: an EMPTY `root` -- which passes JSON-schema `string` and would otherwise resolve to the server's own working directory -- a `root` that is missing, a `root` that is a file, and an EMPTY `model`, which would otherwise be reported as \"no rate recorded for model ''\" and read as a missing price rather than a missing argument. Nothing about the CONTENT of the tree refuses: a transcript that will not decode, a line that will not parse and a record with no usage are omissions and are counted, because a ledger that refused over one of eight hundred transcripts would have said nothing about the other seven hundred and ninety-nine. The one content failure that DOES refuse is a token total above 2**53-1, which is exact in Python and rounded in JavaScript: the place the two runtimes would part company is a refusal, not a wrong number. Deterministic: the tool reads `root`, and `prices` or `$BANTAMKIT_PRICES` when a `model` is named.",
|
|
4
|
+
"surfaces": [
|
|
5
|
+
"mcp"
|
|
6
|
+
],
|
|
7
|
+
"parameters": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"required": [
|
|
10
|
+
"root"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"root": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Directory of host transcripts to walk, absolute or relative to the server's working directory; `~/.claude/projects` is where the host keeps them on this machine. It must name a directory: the empty string is refused rather than resolved, because resolving it would read whatever directory the server happens to be standing in"
|
|
16
|
+
},
|
|
17
|
+
"model": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Model to price the totals as, e.g. the id an assistant record's `message.model` carries. Omit it and no `cost` key is reported at all; name one with no recorded rate and `cost` is `{\"unavailable\": ...}`, which is what the shipped table answers for every model"
|
|
20
|
+
},
|
|
21
|
+
"prices": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Price table to read instead of `$BANTAMKIT_PRICES` or the shipped one; only consulted when `model` is given. A malformed table is a configuration fault and stops with its own sentence, rather than being reported as an unpriced model"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"output_schema": {
|
|
28
|
+
"properties": {
|
|
29
|
+
"result": {
|
|
30
|
+
"title": "Result",
|
|
31
|
+
"type": "string"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": [
|
|
35
|
+
"result"
|
|
36
|
+
],
|
|
37
|
+
"type": "object",
|
|
38
|
+
"title": "token_ledgerOutput"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The `bantamkit-mcp` entry point: the stdio MCP server, and six flags.
|
|
4
4
|
*
|
|
5
|
+
* AMENDED 2026-09-11, J46-31 — "six flags" WAS ALREADY WRONG BEFORE THIS JOB AND IS WRONGER
|
|
6
|
+
* NOW. It was written at `0d067fa`; the parser had nine by the time `--update` was proposed
|
|
7
|
+
* and has ten today, plus `-h/--help`: `--assets-root`, `--k`, `--index-budget`,
|
|
8
|
+
* `--mcp-report`, `--statusline`, `--update`, `--install`, `--force`, `--store`, `--start`.
|
|
9
|
+
* The sentence is kept rather than renumbered because WHAT IT WAS COUNTING AT `0d067fa`
|
|
10
|
+
* cannot be recovered — six is not the count of anything in `PARSER` at that commit either —
|
|
11
|
+
* and a number quietly changed to a different wrong number is worse than one a reader can
|
|
12
|
+
* date. DO NOT RENUMBER IT AGAIN. `PARSER` below is the count; the paragraph after this one
|
|
13
|
+
* is the reason a second copy of a generated fact does not belong in a comment, and this
|
|
14
|
+
* amendment is that reason happening to the comment itself.
|
|
15
|
+
*
|
|
5
16
|
* THE COMMAND LINE IS DATA, NOT PRINTED TEXT. `PARSER` below is the whole description of
|
|
6
17
|
* this CLI, and `pyargparse.ts` renders it — the help table, the usage line at whatever
|
|
7
18
|
* width the terminal is, and every `bantamkit-mcp: error: …` block — with CPython
|
|
@@ -19,6 +30,12 @@
|
|
|
19
30
|
* that every line a real session put on stdout parses as a JSON-RPC frame, that stdout never
|
|
20
31
|
* ends mid-line, and that a clean session writes nothing to stderr either.
|
|
21
32
|
*
|
|
33
|
+
* THE BARE FORM STILL SERVES — WITH ONE QUALIFICATION ADDED BY J46-27, and it does not touch
|
|
34
|
+
* the sentence above. A bare invocation whose STDIN IS A TERMINAL is a person, not a host, and
|
|
35
|
+
* gets the help instead of a mute server; a bare invocation over a pipe is unchanged, byte for
|
|
36
|
+
* byte, and that is the arm `test/server.test.mjs` drives with a real `initialize`. See
|
|
37
|
+
* `typedBareAtATerminal` below for why the discrimination is stdin's tty-ness and nothing else.
|
|
38
|
+
*
|
|
22
39
|
* PRODUCTION RUNS LAYERED. No `--store` means `Memory.layered`, which prepends `[project] `
|
|
23
40
|
* to a recall line; a build validated only against `--store` would ship a recall string the
|
|
24
41
|
* real deployment never produces.
|
|
@@ -52,6 +69,8 @@
|
|
|
52
69
|
* and `docs/install.md` carry the same correction in their own words.
|
|
53
70
|
*/
|
|
54
71
|
import { readFileSync } from 'node:fs';
|
|
72
|
+
import { dirname } from 'node:path';
|
|
73
|
+
import { fileURLToPath } from 'node:url';
|
|
55
74
|
import { assetsRoot } from './assets.js';
|
|
56
75
|
import { BantamError } from './errors.js';
|
|
57
76
|
import { buildReport, resolveEventLogPath } from './mcpreport.js';
|
|
@@ -63,6 +82,7 @@ import { RawStdioTransport } from './mcp/transport.js';
|
|
|
63
82
|
import { ArgumentTypeError, ArgvError, formatHelp, formatUsageBlock, HelpRequested, helpWidth, parseArgs as parseWithSpec, pyIntStrict, } from './pyargparse.js';
|
|
64
83
|
import { pyRepr } from './memory/pyfs.js';
|
|
65
84
|
import { HOSTS, install as installHost, InstallError, thisCommand } from './hostinstall.js';
|
|
85
|
+
import { runUpdate } from './selfupdate.js';
|
|
66
86
|
import { statusLine } from './statusline.js';
|
|
67
87
|
/** `SystemExit("...")`: the message on stderr, exit 1. */
|
|
68
88
|
class Refusal extends Error {
|
|
@@ -153,6 +173,25 @@ const PARSER = {
|
|
|
153
173
|
help: 'print one status line for a host status bar, then exit',
|
|
154
174
|
defaultValue: false,
|
|
155
175
|
},
|
|
176
|
+
// THE ONE FLAG ON THIS PARSER THAT TOUCHES THE NETWORK, and the placement is what keeps
|
|
177
|
+
// that from spreading. `--assets-root` sits first because it needs NOTHING; this one needs
|
|
178
|
+
// the most of any flag here, so it does NOT go there — and the second reason is the same
|
|
179
|
+
// measured one the comments above give: the FIRST line of the 80-column usage is pinned by
|
|
180
|
+
// `test/cli-surface.test.mjs` and by the `cli` conformance suite, and a flag registered
|
|
181
|
+
// before `--mcp-report` would move it and turn a differential suite into a re-baselining
|
|
182
|
+
// one. Registered HERE — after `--statusline`, before `--install`, the reference's own
|
|
183
|
+
// position at `a590df8` — it grows the SECOND usage line only.
|
|
184
|
+
//
|
|
185
|
+
// DEFAULTS OFF, like every other flag on this parser, which is the whole of AS-7(3): the
|
|
186
|
+
// network is reached when a person asks for it by name and on no other path. There is no
|
|
187
|
+
// startup check, nothing on `bantamkit_status`, and no background poller.
|
|
188
|
+
{
|
|
189
|
+
optionStrings: ['--update'],
|
|
190
|
+
dest: 'update',
|
|
191
|
+
kind: 'storeTrue',
|
|
192
|
+
help: 'check the package index and update this install if it differs, then exit',
|
|
193
|
+
defaultValue: false,
|
|
194
|
+
},
|
|
156
195
|
// THE SAME PLACE AND THE SAME REASON AS THE TWO ABOVE, and the position is copied from
|
|
157
196
|
// the reference rather than chosen: argparse prints optionals in registration order, so
|
|
158
197
|
// this line decides where `[--install {...}]` sits in the generated usage. After
|
|
@@ -203,9 +242,10 @@ const PARSER = {
|
|
|
203
242
|
},
|
|
204
243
|
],
|
|
205
244
|
// `--store`/`--start` moved from 6/7 to 8/9 when `--install` and `--force` were added
|
|
206
|
-
// ahead of them. These are POSITIONS, not names,
|
|
207
|
-
// leaving this line alone would silently make two
|
|
208
|
-
|
|
245
|
+
// ahead of them, and from 8/9 to 9/10 when `--update` was. These are POSITIONS, not names,
|
|
246
|
+
// so adding an action above the group and leaving this line alone would silently make two
|
|
247
|
+
// unrelated flags mutually exclusive.
|
|
248
|
+
groups: [[9, 10]],
|
|
209
249
|
};
|
|
210
250
|
/** `_parse_args`, arm for arm, including the mutually exclusive group and `-h`. */
|
|
211
251
|
export function parseArgs(argv) {
|
|
@@ -218,10 +258,68 @@ export function parseArgs(argv) {
|
|
|
218
258
|
assetsRoot: values['assets_root'],
|
|
219
259
|
mcpReport: values['mcp_report'],
|
|
220
260
|
statusline: values['statusline'],
|
|
261
|
+
update: values['update'],
|
|
221
262
|
install: values['install'],
|
|
222
263
|
force: values['force'],
|
|
223
264
|
};
|
|
224
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* `_typed_bare_at_a_terminal`: True when a PERSON typed `bantamkit-mcp` with nothing after
|
|
268
|
+
* it. Never for a host.
|
|
269
|
+
*
|
|
270
|
+
* WHY THERE IS A DISCRIMINATION HERE AT ALL. Typing the command at a prompt used to open a
|
|
271
|
+
* stdio JSON-RPC server and block: no output, no prompt back, Ctrl-C the only way out. To a
|
|
272
|
+
* person that is indistinguishable from a hang, and it is what the user asked to be fixed on
|
|
273
|
+
* 2026-09-11 — "เพิ่ม task set default when call bantamkit-mcp only ให้แสดงเหมือน --help".
|
|
274
|
+
*
|
|
275
|
+
* WHY IT IS NOT SIMPLY "NO ARGUMENTS -> PRINT HELP". The bare invocation IS the production
|
|
276
|
+
* launch path, and this file's own header says so above: "the production invocation passes NO
|
|
277
|
+
* arguments at all (`.mcp.json` and the user-scope config both call the launcher bare), so the
|
|
278
|
+
* bare form is the one that must serve." On THIS side that is sharper than on the Python side:
|
|
279
|
+
* the user-scope registration on this machine points at `tools/bantamkit-mcp-node`, so the Node
|
|
280
|
+
* server is the one a broken bare path takes down.
|
|
281
|
+
*
|
|
282
|
+
* SO THE SIGNAL IS stdin's tty-ness, AND IT IS THE ONLY SIGNAL. A host wires stdin to a pipe or
|
|
283
|
+
* a socket; a person at a keyboard has a terminal on it. Deliberately NOT `process.stdout.isTTY`:
|
|
284
|
+
* stdout is the JSON-RPC channel and a host may redirect the two streams differently, so a tty
|
|
285
|
+
* on stdout says nothing about who is asking. Deliberately not a flag either — a flag to opt out
|
|
286
|
+
* means the bare form is no longer bare, and the bare form is the one under discussion.
|
|
287
|
+
*
|
|
288
|
+
* AND THE EMPTY ARGV IS A SCOPE, NOT A SECOND SIGNAL. What the user asked for is the command
|
|
289
|
+
* "only". `bantamkit-mcp --store /tmp/x` typed at a terminal is an operator explicitly asking
|
|
290
|
+
* for a configured server and keeps getting one; hand-driving the line-delimited protocol at a
|
|
291
|
+
* prompt stays possible. `argv` says WHICH invocation is in scope, the tty says WHO is on the
|
|
292
|
+
* other end of it. `runtime-py/src/bantamkit/mcpserver.py` holds the same pair.
|
|
293
|
+
*
|
|
294
|
+
* THE ONE THING THAT IS NOT A TRANSLATION OF THE REFERENCE. `sys.stdin.isatty()` returns a
|
|
295
|
+
* bool; `process.stdin.isTTY` is `true` on a terminal and **`undefined`** — not `false` — on a
|
|
296
|
+
* pipe, a file or `/dev/null` (measured on Node v25.2.1: `isTTY= undefined undefined`). So this
|
|
297
|
+
* is a TRUTHINESS test. A `=== false` test would be the same bug inverted and far worse than a
|
|
298
|
+
* missing feature: `undefined === false` is `false`, so every host launch would take the person
|
|
299
|
+
* branch and be handed the help table on the JSON-RPC channel.
|
|
300
|
+
*
|
|
301
|
+
* The reference's two "not a person, and must not be allowed to raise" arms — `sys.stdin is
|
|
302
|
+
* None` and `isatty()` on a closed stream raising `ValueError` — have Node counterparts on the
|
|
303
|
+
* same getter: `process.stdin` is documented to be able to be `null` where no stdin exists, and
|
|
304
|
+
* the getter itself can throw for an fd type the platform cannot wrap. Probed here on macOS with
|
|
305
|
+
* fd 0 closed (`0<&-`) it neither threw nor answered null, so the guards are not decoration for
|
|
306
|
+
* a measured case — they are on the path EVERY host takes, and a serving invocation must not be
|
|
307
|
+
* turnable into a stack trace by the check that decides it is serving.
|
|
308
|
+
*/
|
|
309
|
+
function typedBareAtATerminal(argv) {
|
|
310
|
+
if (argv.length > 0)
|
|
311
|
+
return false;
|
|
312
|
+
let stream;
|
|
313
|
+
try {
|
|
314
|
+
stream = process.stdin;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if (stream === null || stream === undefined)
|
|
320
|
+
return false;
|
|
321
|
+
return Boolean(stream.isTTY);
|
|
322
|
+
}
|
|
225
323
|
/** `_build_memory`, including the two refusals argparse cannot express. */
|
|
226
324
|
function buildMemory(options) {
|
|
227
325
|
if (options.k < 1)
|
|
@@ -277,8 +375,22 @@ async function main(argv) {
|
|
|
277
375
|
process.stdout.write(`${statusLine(process.env, options.store, options.start)}\n`);
|
|
278
376
|
return 0;
|
|
279
377
|
}
|
|
378
|
+
if (options.update) {
|
|
379
|
+
// Same discipline and the same place as the three above, and the same place the reference
|
|
380
|
+
// dispatches it: after `--statusline`, before `--install`. It is the one flag here that
|
|
381
|
+
// reaches the network, and it still returns before `buildMemory` — somebody who typed
|
|
382
|
+
// `--update` has not asked for a `.bantamkit/memory` directory in whatever cwd they were
|
|
383
|
+
// standing in, and has certainly not asked for a stdio server on a process that is about
|
|
384
|
+
// to be replaced on disk.
|
|
385
|
+
//
|
|
386
|
+
// `import.meta.url`'s DIRECTORY is the running package directory — `dist/` in an install,
|
|
387
|
+
// the built tree in a checkout — which is the counterpart of the reference's
|
|
388
|
+
// `_running_package_file().resolve().parent`. It stands in for `source` on the two shapes
|
|
389
|
+
// that record no origin, and it is not a guess: it is where the code being executed lives.
|
|
390
|
+
return await runUpdate((text) => process.stdout.write(text), (text) => process.stderr.write(text), version(), dirname(fileURLToPath(import.meta.url)));
|
|
391
|
+
}
|
|
280
392
|
// ORDER IS OBSERVABLE, and it is the reference's order. `main` in `mcpserver.py` checks
|
|
281
|
-
// assets_root, then mcp_report, then statusline, then install, then force — so
|
|
393
|
+
// assets_root, then mcp_report, then statusline, then update, then install, then force — so
|
|
282
394
|
// `--mcp-report --install cursor` prints a report and writes NOTHING. Dispatching install
|
|
283
395
|
// earlier here made the same argv write a file on one runtime and not the other: one
|
|
284
396
|
// command line, two different states on the user's disk. Reviewed and moved.
|
|
@@ -300,6 +412,25 @@ async function main(argv) {
|
|
|
300
412
|
// dropped the flag that says where. Refusing names the missing half.
|
|
301
413
|
if (options.force)
|
|
302
414
|
throw new Refusal('--force is only meaningful with --install');
|
|
415
|
+
// A PERSON TYPED IT. `typedBareAtATerminal` carries the whole argument; what belongs here is
|
|
416
|
+
// only that this sits BEFORE `buildMemory`, which is what creates a store. Somebody who typed
|
|
417
|
+
// a command to see what it does has not asked for a `.bantamkit/memory` directory in whatever
|
|
418
|
+
// cwd they were standing in, and every flag above returns before a transport for the same
|
|
419
|
+
// class of reason. It is also after `--force`, which is the reference's order.
|
|
420
|
+
//
|
|
421
|
+
// STDOUT AND EXIT 0, i.e. byte-for-byte what `-h` does, because the request was
|
|
422
|
+
// "ให้แสดงเหมือน --help" — show it the way `--help` shows it. `formatHelp(PARSER, helpWidth())`
|
|
423
|
+
// is the SAME CALL the `HelpRequested` arm at the bottom of this file makes, over the same
|
|
424
|
+
// `PARSER` object, so the two cannot diverge: there is one parser and two callers, never two
|
|
425
|
+
// strings. That is the defect this file's header records having already shipped once.
|
|
426
|
+
//
|
|
427
|
+
// The alternative reading — stderr and exit 2, "a bare invocation is a usage error" — was
|
|
428
|
+
// considered and refused by J46-26: it is not an error, it is the documented answer to the
|
|
429
|
+
// documented request, and nothing non-interactive can reach this line at all.
|
|
430
|
+
if (typedBareAtATerminal(argv)) {
|
|
431
|
+
process.stdout.write(formatHelp(PARSER, helpWidth()));
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
303
434
|
const memory = buildMemory(options);
|
|
304
435
|
const wire = new RawStdioTransport();
|
|
305
436
|
const server = buildServer(memory, wire, version());
|
package/dist/eventlog.js
CHANGED
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
* one stray write breaks the wire suite. Nothing in this module names `process.stdout`,
|
|
33
33
|
* `process.stderr` or `console`.
|
|
34
34
|
* * **Metadata only.** Never a tool argument's value, never a memory body, never a
|
|
35
|
-
* validated output, never a query, never a document row.
|
|
36
|
-
* unbounded free text and
|
|
37
|
-
* `skill_audit`'s `root` is another
|
|
38
|
-
*
|
|
39
|
-
* name, never a skill id, never a
|
|
35
|
+
* validated output, never a query, never a document row. Eight of the fourteen tools take
|
|
36
|
+
* unbounded free text and seven take absolute paths (`bantamkit_read`'s `path` is one,
|
|
37
|
+
* `skill_audit`'s `root` is another, `repo_map`'s `root` and `focus` are the third and
|
|
38
|
+
* `token_ledger`'s `root` and `prices` are the fourth; each record carries counts and
|
|
39
|
+
* tokens from a closed set — never the path, never a part name, never a skill id, never a
|
|
40
|
+
* mapped file, never a session id, never a model name).
|
|
40
41
|
* Every value written here is an ASCII token from a closed set, a number, or a boolean.
|
|
41
42
|
* * **Never `String(err)`.** Only the constructor name. This is not hypothetical: the host
|
|
42
43
|
* itself has already persisted `input_value={'schema_path': '/Users/k...
|
package/dist/mcp/identity.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const SERVER_NAME = "bantamkit";
|
|
2
|
+
/**
|
|
3
|
+
* A build fact that could not be derived, carrying WHY. Never becomes a value.
|
|
4
|
+
*
|
|
5
|
+
* Exported since job46: `bantamkit_status` has to distinguish "this install could not be
|
|
6
|
+
* classified" — which is a REPORTED gap and not a degraded server — from a real fault, and
|
|
7
|
+
* `instanceof` is the only way to do that without matching on a message.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Undetermined extends Error {
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
12
|
* sha256 over (relative path, size, bytes) of every file, in sorted path order.
|
|
4
13
|
*
|
|
@@ -47,6 +56,77 @@ export declare const notBytecodeCache: (parts: string[]) => boolean;
|
|
|
47
56
|
* reference's printer uses and what the conformance case compares the count against.
|
|
48
57
|
*/
|
|
49
58
|
export declare function packFileCount(root: string): number;
|
|
59
|
+
/**
|
|
60
|
+
* The closed vocabulary, spelled the same way in both runtimes, and it answers ONE question:
|
|
61
|
+
* what would updating this install even mean?
|
|
62
|
+
*
|
|
63
|
+
* registry came from a package index (npm here, PyPI there). Reinstall by name.
|
|
64
|
+
* local-file came from a path on THIS machine — an archive or a directory whose contents
|
|
65
|
+
* were copied in. The path is recorded and may be gone.
|
|
66
|
+
* linked a directory on this machine is still the source being read: a `file:` dir or
|
|
67
|
+
* an `npm link` symlink here, an editable install there. Update that tree.
|
|
68
|
+
* checkout no installer recorded this tree at all. Git.
|
|
69
|
+
* ephemeral a temporary environment discarded after the run. Nothing to update.
|
|
70
|
+
*
|
|
71
|
+
* `ephemeral` IS ANSWERED HERE AND NEVER BY `runtime-py`, which is the one divergence in this
|
|
72
|
+
* surface: npm writes an `_npx` marker into the cache project's own `package.json`, so this is
|
|
73
|
+
* a RECORD, while a `pipx run` or `uvx` environment is not distinguishable from an ordinary
|
|
74
|
+
* venv without pattern-matching cache directory names — a guess, and that surface does not
|
|
75
|
+
* guess. The word is declared on BOTH sides anyway so a consumer of either runtime handles ONE
|
|
76
|
+
* set of five rather than two sets it has to reconcile.
|
|
77
|
+
*/
|
|
78
|
+
export declare const INSTALL_SHAPES: readonly ["registry", "local-file", "linked", "checkout", "ephemeral"];
|
|
79
|
+
/**
|
|
80
|
+
* Where the running code came from, and the origin path it can still be checked against.
|
|
81
|
+
*
|
|
82
|
+
* `source` is `null` for the shapes that HAVE no origin path rather than for the ones whose
|
|
83
|
+
* path could not be read — `sourceReason` carries which, in the operator's words, and
|
|
84
|
+
* `buildIdentity` turns it into the `{"unavailable": ...}` shape RB-P51 requires. A shape that
|
|
85
|
+
* could not be derived at all is an `Undetermined`, never a sixth word.
|
|
86
|
+
*/
|
|
87
|
+
export interface Install {
|
|
88
|
+
readonly shape: string;
|
|
89
|
+
readonly source: string | null;
|
|
90
|
+
readonly sourceReason: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The whole diagnosis, over a running file and the path the process was launched through.
|
|
94
|
+
*
|
|
95
|
+
* Taken as ARGUMENTS rather than read from the process so that every shape can be built as a
|
|
96
|
+
* real `node_modules` tree with a real hidden lockfile, a real symlink and a really-absent
|
|
97
|
+
* tarball, and read back through the same code the server runs. A stubbed filesystem here
|
|
98
|
+
* would assert this function's own reasoning back at it, and the entire question is what npm
|
|
99
|
+
* actually writes down.
|
|
100
|
+
*/
|
|
101
|
+
export declare function deriveInstall(runningFile: string, entryPath: string | null): Install;
|
|
102
|
+
/**
|
|
103
|
+
* Is the recorded origin still on disk? `present: null` when the check itself could not be made.
|
|
104
|
+
*
|
|
105
|
+
* `existsSync` is deliberately not used: it swallows EVERY error and answers `false`, so an
|
|
106
|
+
* origin under a directory the server may not read would be reported as DELETED — a wrong
|
|
107
|
+
* answer where the reference gives none. This is the half that is re-read on every tool call,
|
|
108
|
+
* so it is one `stat` and nothing else.
|
|
109
|
+
*/
|
|
110
|
+
export declare function originStat(path: string): {
|
|
111
|
+
present: boolean | null;
|
|
112
|
+
reason: string;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Which install shape is running. Importable, and NOT owned by the status path.
|
|
116
|
+
*
|
|
117
|
+
* DERIVED ONCE, because the bytes that were imported cannot change under a process. The footer
|
|
118
|
+
* runs `degradedConditions` on every tool call and `docs/status.md` promises what that costs;
|
|
119
|
+
* walking `node_modules` for a hidden lockfile is not on that list. So the SHAPE is memoised
|
|
120
|
+
* and only the EXISTENCE of the recorded path is re-read — which is the half that can actually
|
|
121
|
+
* change while a server is running, and the half the measured defect is about. The failure is
|
|
122
|
+
* memoised too: a derivation that already failed would fail identically a second time.
|
|
123
|
+
*
|
|
124
|
+
* Deliberately a module-level function taking no arguments and touching no server state: the
|
|
125
|
+
* `--update` flag planned as J46-29 has to be shape-aware — two of the five shapes have no
|
|
126
|
+
* registry route at all — and it must be able to ask this question from the CLI, before a
|
|
127
|
+
* memory store or a transport exists, without building an MCP server to do it.
|
|
128
|
+
*/
|
|
129
|
+
export declare function currentInstall(): Install;
|
|
50
130
|
/** `sha256(json.dumps(inputs, sort_keys=True, separators=(",", ":")))` — the identity. */
|
|
51
131
|
export declare function buildIdFor(inputs: Record<string, string>): string;
|
|
52
132
|
/**
|