bantamkit-mcp 0.33.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,324 +1,239 @@
1
- # bantamkit-mcp
2
-
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 twelve tools, the same
5
- `bantamkit_status` prompt and the same two resource templates as `runtime-py`'s server,
6
- reads and writes the same memory store, and is checked against the Python server frame by
7
- frame — 4500+ conformance cases, with every intentional difference written down as a
8
- ruling. It installs **two** commands, not one — the server and `bantamkit-memory`, the
9
- operator CLI for memory-store lifecycle; see *The second bin* below.
10
-
11
- Every number on this page was measured on the machine that wrote it, with a command you
12
- can rerun. Where something was not measured, it says so.
13
-
14
- ## Install and run
15
-
16
- ```jsonc
17
- // .mcp.json — project scope
18
- {
19
- "mcpServers": {
20
- "bantamkit": {
21
- "command": "npx",
22
- "args": ["-y", "bantamkit-mcp"]
23
- }
24
- }
25
- }
26
- ```
27
-
28
- That is the whole install. No `pip`, no venv, no `PYTHONPATH`. `runtime-ts/mcp.json.example`
29
- in the repository is the annotated version, with the four forms and the two measured
30
- questions that decide between them.
1
+ # bantamkit-mcp — memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop
31
2
 
32
- **Pass `-y`.** `npx` historically prompted before installing a package it had not seen,
33
- and stdin here is the JSON-RPC channel: a prompt that consumed one frame looks like a
34
- server that lost a request. npm 11.6.2 was measured sending that prompt to stderr rather
35
- than reading stdin, but that is version-dependent and `-y` costs nothing.
3
+ **bantamkit-mcp** is a pure-Node **MCP server** that gives coding agents a per-person **memory**
4
+ store, JSON Schema validation and shift-work tools. Install it from **npm** with one `npx`
5
+ command, connect it to **Claude Code**, **Claude Desktop**, **Cursor** or **GitHub Copilot in VS
6
+ Code**, and every later launch starts **offline**. No Python, `pip`, `uv`, `pipx` or venv.
36
7
 
37
- **Pin the version.** `npx -y bantamkit-mcp` resolves `latest` and caches it, so two
38
- machines can run different builds from one identical config line. See *Silent version
39
- float* below — it is the failure this package makes easiest to hit and hardest to see.
8
+ It serves the same twelve tools, `bantamkit_status` prompt and two resource templates as the
9
+ Python server on **PyPI** (`pip install "bantamkit[mcp]"`), and reads and writes the same memory
10
+ store. The two are compared frame by frame: 8,130 conformance cases on 2026-09-15, with every
11
+ intentional difference written down as a ruling.
40
12
 
41
- ### Connect it to a host
13
+ ## Contents
42
14
 
43
- **One command, and it writes the entry for you:**
15
+ | Topic | What you'll find |
16
+ |---|---|
17
+ | [Install the MCP server](#install-the-mcp-server) | Two ways to launch it — pick one |
18
+ | [Install once, run offline (recommended)](#install-once-run-offline) | One command; later launches need no network |
19
+ | [Run with npx at every launch](#run-with-npx-at-every-launch) | The online form, and why it hangs offline |
20
+ | [Connect to a host](#connect-to-a-host) | The same five steps for every host |
21
+ | [Connect to Claude Code](#connect-to-claude-code) | `--install claude`, `claude mcp list`, `claude mcp remove` |
22
+ | [Connect to Claude Desktop](#connect-to-claude-desktop) | `claude_desktop_config.json` per OS |
23
+ | [Connect to Cursor](#connect-to-cursor) | `~/.cursor/mcp.json` |
24
+ | [Connect to VS Code (GitHub Copilot)](#connect-to-vs-code-github-copilot) | `mcp.json` with the `servers` key |
25
+ | [Connect other MCP clients](#connect-other-mcp-clients) | Any stdio host, and a GUI host's PATH |
26
+ | [Updating](#updating) | `--update`, the per-install table, then restart |
27
+ | [Troubleshooting](#troubleshooting) | Connection closed, timeouts, `ENOENT`, refusals, the wrong store |
28
+ | [Configuration](#configuration) | Every flag and environment variable, with its default |
29
+ | [Silent version float](#silent-version-float) | Why two machines run different builds, and `build_identity` |
30
+ | [The operator CLI: `bantamkit-memory`](#the-operator-cli-bantamkit-memory) | The second bin: status, lint, compact, archive, restore |
31
+ | [Memory stores are layered by default](#memory-stores-are-layered-by-default) | Why not to add `--store` by reflex |
32
+ | [The asset pack](#the-asset-pack) | What ships in `assets/` and how it is found |
33
+ | [Sharing a store with the Python server](#sharing-a-store-with-the-python-server) | Concurrent writes, and one hand-edit to avoid |
34
+ | [Development](#development) | Build, test, conformance |
35
+ | [Measurements and history](#measurements-and-history) | Where the measured numbers behind this page live |
36
+
37
+ ## Install the MCP server
38
+
39
+ A host launches the server every session, so what matters is whether each launch needs the
40
+ network. Pick **one** of the two ways below. To just try it: `npx -y bantamkit-mcp --assets-root`.
41
+
42
+ ### Install once, run offline
43
+
44
+ **Recommended.**
44
45
 
45
46
  ```bash
46
- npx -y bantamkit-mcp --install claude # Claude Code
47
- npx -y bantamkit-mcp --install claude-desktop # Claude Desktop
48
- npx -y bantamkit-mcp --install copilot # GitHub Copilot in VS Code
49
- npx -y bantamkit-mcp --install cursor # Cursor
47
+ npx -y bantamkit-mcp@latest --install claude # or claude-desktop, copilot, cursor
50
48
  ```
51
49
 
52
- `--install claude` runs `claude mcp add` rather than editing `~/.claude.json` directly:
53
- that file is the host's, and it carries state that is not MCP configuration. What happens on
54
- a second run there is Claude Code's decision, not this command's, and `--force` does not
55
- reach it.
56
-
57
- **For the other three**, which are edited directly: it never prompts. A second run that
58
- finds its own entry says so and changes nothing; an entry that differs is refused, printed
59
- beside the one it would write, and replaced only with `--force`. Every write backs the file
60
- up first as `<name>.backup-<date>`, preserves the file's permissions, and a file that does
61
- not parse is refused rather than replaced.
62
-
63
- The rest of this section is what those commands write, for anyone who would rather do it by
64
- hand. Every host runs the same command; only the file and the key around it change.
65
-
66
- **Claude Code** — one command, no file to edit. `-s user` makes it available in every
67
- project; drop it for this project only.
50
+ From 0.34.0 this runs `npm install --prefix ~/.bantamkit/mcp` once (Windows:
51
+ `%USERPROFILE%\.bantamkit\mcp`, not measured) and records
52
+ `<the node that ran it> ~/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js`, both absolute.
53
+ No later launch needs npx, the registry or your PATH. Measured from the published 0.34.0:
54
+ install exit 0 in 8.14 s; the recorded command then served 12 tools in 0.09 s with the network
55
+ cut.
56
+
57
+ - **`@latest` matters:** without it npx may reuse an older cached resolve.
58
+ - **It skips npm when the kept install already reports this version or a newer one.** It never
59
+ downgrades. If npm fails, no host file is read or written and no backup is taken.
60
+ - **Run from any other install** (`npm i -g`, `npm i --prefix`, a checkout), it records that
61
+ install's own `dist/cli.js` instead.
62
+ - **The recorded node is one version of node.** Switch away from it and re-run with `--force`.
63
+ - **Replacing an old `npx` entry needs `--force`** (Claude Code: `claude mcp remove` first).
64
+ - **Installing into another host while offline:** plain `npx -y` hangs before bantamkit starts.
65
+ Use `npx --offline -y bantamkit-mcp@0.34.0 --install <host>`, or the kept install's own CLI:
66
+ `<node> ~/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js --install <host>`.
67
+
68
+ To make the kept install by hand, install once, then register node and `cli.js` by absolute
69
+ path (`node -p process.execPath` prints the node path). `-s user` makes it available in every
70
+ project; drop it for this project only:
68
71
 
69
72
  ```bash
70
- claude mcp add bantamkit -s user -- npx -y bantamkit-mcp
73
+ npm install --prefix ~/.bantamkit/mcp bantamkit-mcp@latest
74
+ claude mcp add bantamkit -s user -- "$(node -p process.execPath)" ~/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js
71
75
  ```
72
76
 
73
- **Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json`
74
- on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Top-level key
75
- `mcpServers`; each entry takes `command`, `args` and an optional `env`.
77
+ ### Run with npx at every launch
76
78
 
77
79
  ```json
78
80
  {"mcpServers": {"bantamkit": {"command": "npx", "args": ["-y", "bantamkit-mcp"]}}}
79
81
  ```
80
82
 
81
- **GitHub Copilot in VS Code** — `.vscode/mcp.json` for one workspace, or the user
82
- profile via the **MCP: Open User Configuration** command. Note the top-level key is
83
- `servers`, not `mcpServers`.
83
+ One config line (in `.mcp.json` for project scope, or a host's user config) and no install — but it asks the registry on **every** launch. Offline it fails
84
+ silently: with a warm cache, `npx -y bantamkit-mcp`, `npx -y bantamkit-mcp@latest` and
85
+ `npx -y bantamkit-mcp@0.33.0` each hung to the 45 s bound with 0 bytes on stdout, and the host
86
+ reports only its own handshake timeout.
84
87
 
85
- ```json
86
- {"servers": {"bantamkit": {"type": "stdio", "command": "npx", "args": ["-y", "bantamkit-mcp"]}}}
87
- ```
88
-
89
- There is a CLI equivalent: `code --add-mcp '{"name":"bantamkit","command":"npx","args":["-y","bantamkit-mcp"]}'`.
90
-
91
- **Cursor** — `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` globally. Back
92
- to `mcpServers`.
88
+ - **Pass `-y`.** stdin is the JSON-RPC channel, and an install prompt that read one frame would
89
+ look like a server that lost a request.
90
+ - **Pin the version** (`bantamkit-mcp@0.34.0`) or add `@latest`: npx caches what `latest`
91
+ resolved to. See [Silent version float](#silent-version-float).
92
+ - **`npx --offline -y bantamkit-mcp@<version>`** serves a warm cache without the registry (0.40 s
93
+ measured), and fails at once with `npm error code ENOTCACHED` on a cache that never saw it.
93
94
 
94
- ```json
95
- {"mcpServers": {"bantamkit": {"command": "npx", "args": ["-y", "bantamkit-mcp"]}}}
96
- ```
95
+ `runtime-ts/mcp.json.example` in the repository is the annotated version, with five forms.
97
96
 
98
- **Anything else that speaks MCP over stdio** runs `npx -y bantamkit-mcp` and talks
99
- JSON-RPC on its stdin and stdout. Nothing about this package is host-specific.
97
+ ## Connect to a host
100
98
 
101
- The Claude Code and Claude Desktop forms were taken from this machine — `claude mcp add
102
- --help` and an existing config file. The VS Code and Cursor forms are from those
103
- projects' own documentation, not from a host installed here.
99
+ `--install <host>` (`claude`, `claude-desktop`, `copilot`, `cursor`) writes the entry. It never
100
+ prompts, so it behaves the same in a terminal, in CI and inside another agent. For the three
101
+ JSON hosts it backs the file up first as `<name>.backup-<date>`, keeps the file's permissions,
102
+ refuses a file that does not parse, and refuses an entry that differs unless you pass `--force`.
104
103
 
105
- ### Measured: what a cold start costs
104
+ Each host below has the same steps: **1** command, **2** file, **3** entry, **4** confirm,
105
+ **5** undo or re-run. The entry's `command` and `args` depend on the install route:
106
106
 
107
- `node tools/conformance/npx-cold-start.mjs` in the repo packs the tarball, installs it
108
- from disk into a cache that has never seen it, and drives a real MCP handshake. On
109
- node v25.2.1 / npm 11.6.2, macOS (darwin 25.5.0), Apple silicon:
110
-
111
- | | cold cache | warm cache |
112
- |---|---|---|
113
- | wall to the first JSON-RPC frame | **3.90 s** and **5.09 s**, two runs | **1.14 s** and **1.11 s** |
114
- | npm/npx bytes on stderr | 211 (an `npm notice` about npm itself) | 0 |
115
-
116
- Both cold figures are reported rather than averaged, and the two in that column are two
117
- runs from **one sitting on 2026-08-25** — the column is labelled "two runs" and one
118
- execution cannot honestly fill it.
119
-
120
- **The spread is bigger than a range would suggest, and it is the network.** Five cold
121
- runs on that same day, same machine, same commit, measured **3.68 / 3.90 / 4.66 / 5.09 /
122
- 9.66 s**. An earlier revision of this table read "4.13 s and 7.36 s" and called a cold
123
- start "worth about 4-7 s here"; three of those five runs fall outside that range, so the
124
- range is dropped rather than re-fitted. What a reader should take from the cold column is
125
- an ORDER OF MAGNITUDE — seconds, dominated by the registry round trip, worse on a slower
126
- link — and not a number to compare a future run against. The warm figure is the one that
127
- is a property of this package, and it is stable across every run above.
128
-
129
- - **92 packages** installed (top-level under `node_modules`, scope-aware); **111**
130
- `package.json` **under `node_modules`**, which is the count of packages actually
131
- installed:
132
-
133
- ```
134
- find "$BED/cold/cache/_npx/<hash>/node_modules" -type f -name package.json | wc -l # 111
135
- ```
136
-
137
- Counting from the `_npx/<hash>` directory instead gives **112**, and the extra file is
138
- not a package: npx synthesises a `package.json` at that root holding the `file:` spec
139
- and an `_npx.packages` array. The method is written down here because the 112 was read
140
- once as a drift of one and it is not one — it is a different denominator. Reproduce
141
- either with `node tools/conformance/npx-cold-start.mjs --keep`, which prints the scratch
142
- path it leaves behind.
143
- - **15.7 MB** of files under `$npm_config_cache/_npx/<hash>` (26 MB of allocated blocks by
144
- `du`), **37.8 MB** for the whole cache including npm's content-addressable store (this
145
- read 37.7 until 2026-08-25; all four cold runs that day printed 37.8).
146
- - `bantamkit-mcp` itself is **0.9 MB** of that (1.2 MB allocated). The rest is
147
- `@modelcontextprotocol/sdk@1.30.0`'s dependency tree, which pulls in `express`, `cors`,
148
- `body-parser`, `ajv`, `eventsource`, `hono` and `express-rate-limit` — the SDK's HTTP
149
- transport, none of which this stdio server uses. That is the SDK's shape, not a choice
150
- this package makes; it declares exactly one runtime dependency.
151
- - The tarball is **~266 KB**, 137 files: 50 in `dist/`, 84 in `assets/`, plus `LICENSE`,
152
- `package.json` and this README. (The exact byte count moves whenever this file does;
153
- the gate prints it, and `test/packaging.test.mjs` pins the file list.)
154
-
155
- ### Measured: what happens when the registry is not reachable
156
-
157
- A cold `npx` start needs the network. Measured with the registry pointed at a closed port
158
- and at a blackholed address, driving the same handshake:
159
-
160
- | registry | seconds before `npx` gave up | bytes the client saw on stdout |
107
+ | Route | `command` | `args` |
161
108
  |---|---|---|
162
- | connection refused (`http://127.0.0.1:1/`) | **140.3 s** | **0** |
163
- | packets dropped (`http://192.0.2.1:443/`) | **590.4 s** | **0** |
164
-
165
- **Read that as: the failure is silence.** Nothing appears on the JSON-RPC channel for the
166
- whole interval, and then the process exits 1. An MCP host does not see "no network"; it
167
- sees a server that accepted the launch and never answered `initialize`, and it reports its
168
- own handshake timeout. The npm error text goes to stderr, which most hosts do not surface.
169
-
170
- A warm cache does not have this problem — `npx` runs the cached install without contacting
171
- the registry — so this bites a new machine, a cleared cache, or a CI runner, which is
172
- exactly when nobody is watching.
173
-
174
- If offline operation matters, do not use `npx`. Run `npm i -g bantamkit-mcp` once and
175
- point the config at the installed binary, or keep using `tools/bantamkit-mcp`.
176
-
177
- ### Measured: `npx` is not on a login-less PATH
178
-
179
- ```sh
180
- $ env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin sh -c 'command -v npx'
181
- $ echo $?
182
- 1
183
- ```
184
-
185
- On the machine this was written on, `node`, `npm` and `npx` exist only on the
186
- mise-injected PATH, and mise is activated from `~/.zshrc` — an **interactive** shell rc.
187
- `launchctl getenv PATH` is unset, so a GUI-launched application inherits launchd's default
188
- `/usr/bin:/bin:/usr/sbin:/sbin`, where none of the three is found.
189
-
190
- **What a GUI-launched MCP host actually sees is `ENOENT` on `npx`.** Not a bantamkit
191
- error, not a bad config — the host reports that it could not spawn the command, which is
192
- the least informative message in the whole chain. This is the RB-P96 failure class
193
- relocated: the sh launcher was guaranteed present because it was a file in the repository;
194
- `npx` is guaranteed present only if the host's *process* environment has it.
195
-
196
- Two ways out, both in `runtime-ts/mcp.json.example`:
109
+ | npm, install once | `/absolute/path/to/node` | `["/Users/you/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js"]` |
110
+ | Python venv | `/absolute/path/to/env/bin/bantamkit-mcp` | `[]` |
111
+ | npx every launch | `npx` | `["-y", "bantamkit-mcp"]` |
112
+
113
+ A JSON file cannot expand `~`, so spell both paths out. To check a recorded command, run it in
114
+ a terminal: with nothing on stdin it prints `usage: bantamkit-mcp …` and exits 0.
115
+
116
+ ### Connect to Claude Code
117
+
118
+ 1. **Command:** `npx -y bantamkit-mcp@latest --install claude`
119
+ 2. **File:** none edited directly — it runs `claude mcp add bantamkit -s user -- <command> <args>`
120
+ (user scope, every project). `~/.claude.json` is the host's file, and it holds state that is
121
+ not MCP configuration.
122
+ 3. **Entry:** printed as `ran : claude mcp add bantamkit -s user -- <node> <cli.js>`.
123
+ 4. **Confirm:** `claude mcp list` prints `bantamkit: <node> <cli.js> - ✔ Connected` (Claude Code
124
+ 2.1.272; run it outside a project whose `.mcp.json` also names bantamkit, or it prints
125
+ `[Conflicting scopes]`). In a session, ask the agent to call `bantamkit_status`.
126
+ 5. **Undo / re-run:** `--force` does not reach Claude Code, and a second add fails with
127
+ `MCP server bantamkit already exists in user config`. Remove first:
128
+
129
+ ```bash
130
+ claude mcp remove bantamkit -s user
131
+ npx -y bantamkit-mcp@latest --install claude
132
+ ```
133
+
134
+ ### Connect to Claude Desktop
135
+
136
+ 1. **Command:** `npx -y bantamkit-mcp@latest --install claude-desktop`
137
+ 2. **File:** macOS `~/Library/Application Support/Claude/claude_desktop_config.json` ·
138
+ Windows `%APPDATA%\Claude\claude_desktop_config.json` ·
139
+ Linux `~/.config/Claude/claude_desktop_config.json`
140
+ 3. **Entry** (key `mcpServers`; each entry takes `command`, `args` and an optional `env`):
141
+
142
+ ```json
143
+ {"mcpServers": {"bantamkit": {"command": "/absolute/path/to/node", "args": ["/Users/you/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js"]}}}
144
+ ```
145
+
146
+ 4. **Confirm:** it prints `installed bantamkit into claude-desktop` with file, key and command.
147
+ Fully quit and reopen Claude Desktop, then ask it to call `bantamkit_status`.
148
+ 5. **Undo / re-run:** there is no uninstall flag — delete the `bantamkit` entry or restore
149
+ `claude_desktop_config.json.backup-<date>`. A matching re-run prints
150
+ `bantamkit is already installed in claude-desktop and matches`; a differing entry needs `--force`.
151
+
152
+ ### Connect to Cursor
153
+
154
+ 1. **Command:** `npx -y bantamkit-mcp@latest --install cursor`
155
+ 2. **File:** `~/.cursor/mcp.json` on every OS (`%USERPROFILE%\.cursor\mcp.json` on Windows);
156
+ `.cursor/mcp.json` for one project, by hand.
157
+ 3. **Entry** (key `mcpServers`):
158
+
159
+ ```json
160
+ {"mcpServers": {"bantamkit": {"command": "/absolute/path/to/node", "args": ["/Users/you/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js"]}}}
161
+ ```
162
+
163
+ 4. **Confirm:** it prints `installed bantamkit into cursor` and `key : mcpServers`. Restart
164
+ Cursor and ask the agent to call `bantamkit_status`.
165
+ 5. **Undo / re-run:** delete the entry or restore `mcp.json.backup-<date>`. To replace a
166
+ differing entry, such as an old `npx` one:
167
+
168
+ ```bash
169
+ npx -y bantamkit-mcp@latest --install cursor --force
170
+ ```
171
+
172
+ ### Connect to VS Code (GitHub Copilot)
173
+
174
+ 1. **Command:** `npx -y bantamkit-mcp@latest --install copilot`
175
+ 2. **File:** macOS `~/Library/Application Support/Code/User/mcp.json` ·
176
+ Windows `%APPDATA%\Code\User\mcp.json` · Linux `~/.config/Code/User/mcp.json`;
177
+ `.vscode/mcp.json` for one workspace, or **MCP: Open User Configuration**, by hand.
178
+ 3. **Entry** — the key is **`servers`**, not `mcpServers`, plus `"type": "stdio"`. This is the
179
+ detail that catches people out:
180
+
181
+ ```json
182
+ {"servers": {"bantamkit": {"type": "stdio", "command": "/absolute/path/to/node", "args": ["/Users/you/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js"]}}}
183
+ ```
184
+
185
+ VS Code's own CLI can add an entry too (from VS Code's documentation, not measured here):
186
+ `code --add-mcp '{"name":"bantamkit","command":"npx","args":["-y","bantamkit-mcp"]}'`.
187
+ 4. **Confirm:** it prints `installed bantamkit into copilot` and `key : servers`. Restart
188
+ VS Code and ask Copilot to call `bantamkit_status`.
189
+ 5. **Undo / re-run:** delete the entry or restore `mcp.json.backup-<date>`; `--force` replaces
190
+ a differing entry.
191
+
192
+ ### Connect other MCP clients
193
+
194
+ Anything that speaks MCP over stdio runs the same `command`/`args` and talks JSON-RPC on stdin
195
+ and stdout; nothing in this package is host-specific. Codex and a generic JSON config:
196
+ [docs/mcp.md → Client setup](https://github.com/Ink01101011/bantamkit/blob/main/docs/mcp.md#client-setup).
197
+
198
+ **A GUI-launched host does not read your shell rc.** If `node` and `npx` come from mise, nvm or
199
+ similar, they are not on its PATH (`/usr/bin:/bin:/usr/sbin:/sbin` on macOS). An `npx` entry then
200
+ fails with `ENOENT`, and a `.bin/bantamkit-mcp` shim exits 127 with
201
+ `env: node: No such file or directory`, because its first line is `#!/usr/bin/env node`. Name
202
+ node by absolute path, as `--install` does.
197
203
 
198
- ```jsonc
199
- // absolute path to the npx you actually have
200
- { "command": "/Users/you/.local/share/mise/installs/node/latest/bin/npx",
201
- "args": ["-y", "bantamkit-mcp@0.25.0"] }
202
- ```
204
+ ## Updating
203
205
 
204
- ```jsonc
205
- // or install once and skip npx entirely: npm i -g bantamkit-mcp
206
- { "command": "/usr/local/bin/bantamkit-mcp", "args": [] }
206
+ ```bash
207
+ npx -y bantamkit-mcp@latest --update
207
208
  ```
208
209
 
209
- ## What `npx` gives up versus `tools/bantamkit-mcp`
210
-
211
- The sh launcher is not obsolete. It guarantees things `npx` cannot, and this table is the
212
- prep probe's, unsoftened.
213
-
214
- | guarantee | `tools/bantamkit-mcp` | `npx bantamkit-mcp` |
215
- |---|---|---|
216
- | present in every checkout **and** every worktree | yes — it is a file in the tree | **gone.** Depends on the host's PATH, not on the project directory |
217
- | runs *this* checkout's code | yes | **gone for developers too.** Testing a worktree needs an absolute `node <worktree>/dist/cli.js`, or the host silently runs the published build |
218
- | code from the worktree, deps from the main checkout | yes (`PYTHONPATH` + `PYTHONSAFEPATH`) | **no Node analogue.** `NODE_PATH` is ignored by ESM and there is no `-P`. `npm link` and workspaces are a different failure surface, not the same one solved |
219
- | stdin is the JSON-RPC channel | yes | yes, with `-y` |
220
- | names the cause when dependencies are missing | yes | the analogous failure is *no network*, and it has no message at all — see the table above |
221
- | starts without a network | yes | **no**, on a cold cache |
222
- | `--which`, for diagnosing which endpoint answered | the flag exists | **not in the npx CLI** — but `tools/bantamkit-mcp-node --which` has it, see below |
223
- | one config line, no clone, no venv | no | **yes.** This is the whole reason the package exists |
224
-
225
- ### `--which` is a launcher flag, not a package flag
226
-
227
- This section previously read "`--which` is deleted, not ported" and said "there is no
228
- Node `--which`, and none is planned". Both are now false, and the second was made false
229
- inside this repository: `tools/bantamkit-mcp-node` ships `--which`, and
230
- `runtime-ts/test/launcher.test.mjs` runs it — including on a checkout that has never been
231
- built, which is the case the Python flag's `find_spec` was chosen for. It prints
232
- `checkout=`, `deps_root=`, `runtime=node`, `node=`, `entry=` (suffixed `(missing)` when
233
- `dist/` is absent) and `sdk=`.
234
-
235
- What is genuinely not ported is `--which` **on the published package**, and the reason is
236
- the line above it in the table: `npx bantamkit-mcp` does not run a checkout, so
237
- `checkout=` and `source=` have nothing to report. The question a reader has about an npx
238
- endpoint is not *where did this resolve* but *which build answered*, and that is
239
- `build_identity` — a tool on the wire rather than a flag on a launcher, and what the next
240
- section is about.
241
-
242
- The old wording came from a real defect, which is now closed. `tools/bantamkit-mcp:47`
243
- used to name `tools/mcpreach/mcpreach.py` as `--which`'s consumer and `docs/mcp.md` used
244
- to document a five-value exit-code interface for that program (`0` REACHABLE, `1`
245
- UNREACHABLE, `2` FOREIGN, `3` UNDECLARED, `4` NO_ENV). **That program has never existed** —
246
- `git log --all --diff-filter=A -- '*mcpreach*'` is empty across every ref in this
247
- repository, and `docs/eval.md` records the decision not to ship it, the half-built checker
248
- having "never been seen to fire". Both citations were rewritten on 2026-08-24 to name what
249
- actually runs, and `runtime-py/tests/test_doc_commands_gate.py` is now red if any fenced
250
- shell block in the repository names a `tools/` program that is not in the tree.
251
-
252
- ## Silent version float, and the instrument for it
253
-
254
- This is the one that will bite a team hardest.
255
-
256
- `npx -y bantamkit-mcp` resolves the dist-tag `latest` and caches the result. Two people
257
- with byte-identical `.mcp.json` files can be running different builds — one resolved last
258
- week, one resolved this morning — and nothing in the config, the logs or the tool output
259
- says so. The sh launcher could not do this: it ran the checkout you were standing in.
260
-
261
- Call `build_identity`. Three fields answer it:
210
+ `--update` — `check the package index and update this install if it differs, then exit` — asks
211
+ the npm registry for `latest` and compares it with the installed version:
262
212
 
263
- - **`runtime`** — `"node"` here, absent on the Python server. Its *presence* is the
264
- discriminator, and it is folded into `build_id` so the two lineages cannot collide even
265
- if their code digests agreed.
266
- - **`assets_digest`** — sha256 over every byte of the asset pack, computed identically in
267
- both runtimes and **verified equal**: `sha256:b03141bf…` over 83 files from Python and
268
- from Node. This is the cross-runtime instrument; two machines disagreeing here are
269
- serving different data.
270
- - **`code_digest`** / **`build_id`** — over `dist/**/*.js` on this side and `*.py` on the
271
- other, so they are *required* to differ across runtimes and required to match across two
272
- installs of the same version. `build_id` differing between two teammates on the same
273
- version string is the float, caught.
274
-
275
- The output carries a `cross_runtime` sentence saying in plain words which fields are
276
- comparable. `git_commit` is refused rather than guessed: an installed npm tarball carries
277
- no repository, and reading a checkout's HEAD would describe the tree rather than the bytes
278
- that were imported (RB-P84).
279
-
280
- **Pin the version in the config** if you want this to be a non-issue:
281
- `"args": ["-y", "bantamkit-mcp@0.30.0"]`.
213
+ - **Same version:** prints both numbers and `up to date.`
214
+ - **Behind, on a registry install or a kept install from `--install` (0.34.0+):** runs the
215
+ matching row below and prints npm's output.
216
+ - **Behind, on any other shape** (a checkout, a linked tree, a local file, an npx cache with no
217
+ kept install): refuses with exit 1 and names your row. It will not write a registry install
218
+ into a tree you manage with `git`.
282
219
 
283
- ## Updating
220
+ It is the only network access here — not at startup, not on `bantamkit_status`, not on any tool
221
+ — with a 10-second timeout, and being offline is a named refusal on stderr, never a traceback.
284
222
 
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 |
223
+ | How it was installed | How to update |
306
224
  |---|---|
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. |
225
+ | `npx -y bantamkit-mcp` in the host config | Nothing to update, but npx **caches the resolved version**: add `@latest` (or pin `@0.34.0`). `rm -rf ~/.npm/_npx` forces a clean resolve |
226
+ | `npx -y bantamkit-mcp --install <host>` from 0.34.0: a kept install at `~/.bantamkit/mcp` | `npx -y bantamkit-mcp@latest --update` patches it in place, or `npm i --prefix ~/.bantamkit/mcp bantamkit-mcp@latest`. The recorded paths do not change. Before 0.34.0, `--update` from an npx cache refuses instead |
308
227
  | `npm i -g bantamkit-mcp` | `npm i -g bantamkit-mcp@latest` |
309
228
  | `npm i --prefix <dir> bantamkit-mcp` | `npm i --prefix <dir> bantamkit-mcp@latest` |
310
229
  | 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 |
230
+ | a checkout, via `tools/bantamkit-mcp-node` | `git pull && npm ci --prefix runtime-ts && npm run build --prefix runtime-ts` — no registry, and `runtime-ts/dist/` is build output, so a pull alone changes nothing |
312
231
 
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.
232
+ **Then restart the server in your host** — a running server keeps serving the code it loaded at
233
+ startup. In Claude Code: `/mcp` → reconnect. In Claude Desktop: a full restart of the app.
319
234
 
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*:
235
+ **Verify with `bantamkit_status`, not the install log.** It reports the version and the
236
+ `build_id` of *the code that is answering you*:
322
237
 
323
238
  ```
324
239
  bantamkit Active 🟢
@@ -326,171 +241,138 @@ version 0.30.0, build sha256:4441619d…
326
241
  serving 12 tools, 1 prompt, 2 resource templates
327
242
  ```
328
243
 
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
- ```
244
+ A version that moved and a `build_id` that did not means you are reading a config, not a process.
341
245
 
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.
246
+ **Tested a build from a local `.tgz`? Install over it from the registry afterwards**
247
+ (`npm i --prefix <dir> bantamkit-mcp@latest`). Otherwise that machine stays pinned to a `file:`
248
+ dependency in a temp directory that will be deleted, and `npm update` cannot help.
346
249
 
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.
250
+ ## Troubleshooting
349
251
 
350
- ## The second bin: `bantamkit-memory`
252
+ | Symptom | Fix |
253
+ |---|---|
254
+ | Connection closed at startup (`CONNECTION_CLOSED`) | Builds before 0.32.1 crashed when a GUI host started them with cwd `/`. Update, then restart the server |
255
+ | The host times out; the server never answers | The entry uses `npx` with a registry spec and the network is down. Use [Install once, run offline](#install-once-run-offline) |
256
+ | `ENOENT` from the host, or `env: node: No such file or directory` | A GUI host does not have your shell's PATH. `--install` records node by absolute path; see [Connect other MCP clients](#connect-other-mcp-clients) |
257
+ | `npm error code ENOTCACHED` | `npx --offline` on a cache that has never seen that spec. Run it once online, or use the kept install |
258
+ | `already has a bantamkit entry with different settings … re-run with --force to replace it` | Re-run with `--force`; the old file is kept as `.backup-<date>` |
259
+ | `MCP server bantamkit already exists in user config` | `claude mcp remove bantamkit -s user`, then install again |
260
+ | The server stopped launching after a node change | `npx -y bantamkit-mcp@latest --install <host> --force` with the node you have now |
261
+ | Two machines with one config run different builds | [Silent version float](#silent-version-float): compare `build_id` |
262
+ | `bantamkit_status` still shows the old version after an update | The host has not reconnected; restart the server |
263
+ | Recall finds nothing, or the wrong store | Set `BANTAMKIT_MEMORY_DIR`; the reply names the store it searched |
264
+ | `malformed fact file …` | A hand-edited fact has a bare non-string field; see [Sharing a store](#sharing-a-store-with-the-python-server) |
265
+ | `AssetNotFound: no assets directory found; set BANTAMKIT_ASSETS` | See [The asset pack](#the-asset-pack) |
266
+
267
+ ## Configuration
268
+
269
+ Flags go in the entry's `args`; environment variables in its `env` block (or
270
+ `claude mcp add -e NAME=value`). Both runtimes print the same `--help`.
271
+
272
+ | Setting | What it does | Default | Example |
273
+ |---|---|---|---|
274
+ | `BANTAMKIT_MEMORY_DIR` | Pins the store to one absolute path; a missing or relative path refuses at startup | unset: nearest existing `.bantamkit/memory` at or above the start directory | `"env": {"BANTAMKIT_MEMORY_DIR": "/abs/project/.bantamkit/memory"}` |
275
+ | `--store STORE` | One store, layering off; outranks `BANTAMKIT_MEMORY_DIR` | off (layered) | `"--store", "/abs/store"` |
276
+ | `--start START` | Where store discovery starts; not with `--store` | cwd | `"--start", "/abs/project"` |
277
+ | Layered memory | Recall reads the project store, stores granted in `.bantamkit/config.yaml`, and `~/.bantamkit/memory`; saves go to the project store | on | [Memory stores are layered by default](#memory-stores-are-layered-by-default) |
278
+ | `--k K` | Default recall budget | `3` | `"--k", "5"` |
279
+ | `--index-budget BYTES` | Memory index byte budget | `24000` | `"--index-budget", "32000"` |
280
+ | `BANTAMKIT_EVENT_LOG` | Logs tool outcomes as JSONL | off; `on` → `<store>/events/mcp.jsonl`; other values are a path | `"env": {"BANTAMKIT_EVENT_LOG": "on"}` |
281
+ | `BANTAMKIT_ASSETS` | Your own asset pack (skills, rubrics, schemas) | the pack inside the package | `"env": {"BANTAMKIT_ASSETS": "/abs/my-assets"}` |
282
+ | `BANTAMKIT_HOST_LOG_ROOT` | Where `--mcp-report` finds the host's MCP logs | macOS `~/Library/Caches/claude-cli-nodejs`; elsewhere unset | `BANTAMKIT_HOST_LOG_ROOT=/abs/logs bantamkit-mcp --mcp-report` |
283
+ | `BANTAMKIT_PRICES` | Price table for the token ledger | `pricing/default.json` in the asset pack | `"env": {"BANTAMKIT_PRICES": "/abs/prices.json"}` |
284
+
285
+ One-shot commands that print and exit: `--install {claude,claude-desktop,copilot,cursor}` (with
286
+ `--force`), `--update`, `--assets-root`, `--mcp-report`, `--statusline`, `-h`. Typing
287
+ `bantamkit-mcp` at a terminal with nothing after it prints the help and exits 0; a host launch
288
+ down a pipe still starts the server, and `bantamkit-mcp --store /tmp/x` at a terminal still
289
+ serves. `--which` belongs to the checkout launchers `tools/bantamkit-mcp` and
290
+ `tools/bantamkit-mcp-node`, not to this package: an npx install runs no checkout, so ask
291
+ `build_identity` which build answered instead.
292
+
293
+ ## Silent version float
294
+
295
+ `npx -y bantamkit-mcp` resolves the dist-tag `latest` and caches the result. Two people with
296
+ byte-identical `.mcp.json` files can be running different builds — one resolved last week, one
297
+ this morning — and nothing in the config, the logs or the tool output says so. This is the one
298
+ that will bite a team hardest.
299
+
300
+ **Fix:** pin the version in the config (`"args": ["-y", "bantamkit-mcp@0.34.0"]`), or use the
301
+ kept install from `--install`, which serves the version in `~/.bantamkit/mcp` until someone runs
302
+ `--update`.
303
+
304
+ **Detect it:** call the `build_identity` tool. Three fields answer it:
351
305
 
352
- `package.json` declares **two** bins, so an install puts two commands on the path. Packed
353
- and installed into an empty scratch directory, that is what arrives:
306
+ - **`runtime`** — `"node"` here, absent on the Python server. Its *presence* is the
307
+ discriminator, and it is folded into `build_id` so the two lineages cannot collide.
308
+ - **`assets_digest`** — sha256 over every byte of the asset pack, computed identically in both
309
+ runtimes and verified equal (`sha256:eb79f9e3…` over 91 files from Python and from Node,
310
+ `node tools/conformance/run.mjs --all`, 2026-09-15). Two
311
+ machines disagreeing here are serving different data.
312
+ - **`code_digest`** / **`build_id`** — over `dist/**/*.js` here and `*.py` on the Python side, so
313
+ they must differ across runtimes and must match across two installs of one version. `build_id`
314
+ differing between two teammates on the same version string is the float, caught.
315
+
316
+ The output carries a `cross_runtime` sentence naming which fields are comparable. `git_commit` is
317
+ refused rather than guessed: an npm tarball carries no repository, and a checkout's HEAD would
318
+ describe the tree, not the bytes that were imported (RB-P84).
319
+
320
+ ## The operator CLI: `bantamkit-memory`
321
+
322
+ The package installs **two** commands: `bantamkit-mcp`, the server, whose stdout is the JSON-RPC
323
+ wire, and `bantamkit-memory`, the operator CLI for memory-store lifecycle, which prints reports.
324
+ It is a second bin rather than a subcommand because report output on the server's stdout would
325
+ corrupt the transport, and `bantamkit-mcp`'s help is byte-compared with
326
+ `python -m bantamkit.mcpserver -h`.
354
327
 
355
- ```console
356
- $ npm install ./bantamkit-mcp-0.25.0.tgz
357
- added 95 packages in 5s
358
- $ ls -l node_modules/.bin/
359
- bantamkit-mcp -> ../bantamkit-mcp/dist/cli.js
360
- bantamkit-memory -> ../bantamkit-mcp/dist/memory/cli.js
328
+ ```bash
329
+ npx -y -p bantamkit-mcp bantamkit-memory status # without installing; -p is required
361
330
  ```
362
331
 
363
- `bantamkit-mcp` is the server the `.mcp.json` line at the top launches, and its stdout is
364
- the JSON-RPC wire — not a thing you run by hand. `bantamkit-memory` is the operator CLI
365
- for memory-store lifecycle, and it prints reports. That is the whole reason it is a
366
- second bin instead of a subcommand: lifecycle output on the server's stdout would corrupt
367
- the transport, and `bantamkit-mcp`'s help is a byte-compared artifact against
368
- `python -m bantamkit.mcpserver -h`, which a subparsers action would move.
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
-
384
- Five subcommands, scoped to the writable **project** layer only — read-only grants and the
385
- profile store are out of its reach by the code path, not by convention:
386
-
387
- | subcommand | what it does |
332
+ The package name and the bin name differ, so plain `npx bantamkit-memory` would look for a
333
+ package called `bantamkit-memory`.
334
+
335
+ | Subcommand | What it does |
388
336
  |---|---|
389
337
  | `status` | index size, budget, headroom, archive count |
390
338
  | `lint` | exit 1 if the store is malformed or over budget |
391
339
  | `compact` | archive the stalest facts until the index fits |
392
340
  | `archived` | list what compaction has moved out |
341
+ | `archive NAME` | move one named fact out |
393
342
  | `restore NAME` | move an archived fact back |
394
343
 
395
- Each takes `--store PATH` or `--start DIR`; with neither, it resolves the project store
396
- the way `Memory.layered()` does. Exit codes are `0` success, `1` a failure the operator
397
- must act on (over budget, a malformed fact, a refused restore), `2` a usage error — so
398
- `lint` drops into a pre-commit hook or a CI job unchanged.
399
-
400
- Driven off `node_modules/.bin/` from the install above, against a scratch five-fact store:
401
-
402
- ```console
403
- $ bantamkit-memory status --store store
404
- store: store
405
- facts: 5
406
- index: 576 bytes
407
- budget: 24000
408
- headroom: 23424
409
- archived: 0
410
- $ bantamkit-memory lint --store store --budget 400
411
- lint: FAIL — index is 576 bytes, budget is 400
412
- try: bantamkit-memory compact --store store --budget 400
413
- $ echo $?
414
- 1
415
- $ bantamkit-memory compact --store store --budget 400
416
- compacted 3 fact(s)
417
- index: 576 -> 236 bytes (budget 400, target 267, reserve 133, headroom 164)
418
- archived -> store/archive
419
- assets-pack-has-eighty-four-files (project, 133 bytes)
420
- ci-runner-is-macos-only (project, 97 bytes)
421
- conformance-runner-entrypoint (reference, 110 bytes)
422
- restore one with: bantamkit-memory restore <name> --store store
423
- $ bantamkit-memory archived --store store
424
- archived facts: 3 (store/archive)
425
- assets-pack-has-eighty-four-files
426
- ci-runner-is-macos-only
427
- conformance-runner-entrypoint
428
- $ bantamkit-memory restore ci-runner-is-macos-only --store store
429
- restored 'ci-runner-is-macos-only' — index now 333/24000 bytes
430
- ```
431
-
432
- `compact` printing every name that left is the point: `archive/` is a directory nothing
433
- reads back on its own, so a compaction whose output is not shown is a silent deletion as
434
- far as the operator is concerned.
435
-
436
- Without installing, `npx -p bantamkit-mcp bantamkit-memory status` runs it out of the
437
- registry. `-p` is not optional — the package name and this bin's name differ, and plain
438
- `npx bantamkit-memory` would go looking for a package called `bantamkit-memory`. Measured
439
- here against the local tarball rather than the registry, since this version is not
440
- published:
441
-
442
- ```console
443
- $ npx -y -p ./bantamkit-mcp-0.25.0.tgz bantamkit-memory status --store npxstore
444
- store: npxstore
445
- facts: 0
446
- index: 0 bytes
447
- budget: 24000
448
- headroom: 24000
449
- archived: 0
450
- ```
451
-
452
- **The Python install does not provide this command**, which is why the two spellings
453
- exist. Measured against the distribution in this repository's venv:
344
+ Each takes `--store STORE` or `--start START`; with neither, it resolves the project store the way
345
+ `Memory.layered()` does. It reaches the writable **project** layer only — read-only grants and the
346
+ profile store are out of reach by the code path, not by convention. Exit codes: `0` success, `1`
347
+ a failure to act on (over budget, a malformed fact, a refused restore), `2` a usage error, so
348
+ `lint` drops into a pre-commit hook or CI job unchanged. `compact` prints every name it moved,
349
+ because `archive/` is a directory nothing reads back on its own.
454
350
 
455
- ```console
456
- $ .venv/bin/python -c "from importlib.metadata import distribution; d=distribution('bantamkit'); print(sorted(e.name for e in d.entry_points if e.group=='console_scripts'))"
457
- ['bantamkit-mcp']
458
- $ env PATH="$PWD/.venv/bin:/usr/bin:/bin" sh -c 'command -v bantamkit-memory'
459
- $ echo $?
460
- 1
461
- ```
462
-
463
- One console script, and it is the server. The Python operator therefore types
464
- `python -m bantamkit.memory`, and the two CLIs are identical bytes after substituting one
465
- for the other — except the wrap, because argparse's hanging indent is
466
- `len(prefix) + len(prog) + 1`. The reason there is no third spelling both installs could
467
- use lives in one place, the `prog` row of `docs/porting.md`'s divergence table, and this
468
- page does not restate it. `docs/memory.md`'s *The operator CLI* is the full reference;
469
- `tools/conformance/suites/memorycli.mjs` is the gate that compares the two.
351
+ The Python install has no such console script: there the same CLI is
352
+ `python -m bantamkit.memory`, identical bytes apart from the program name and its wrap. Full
353
+ reference: [docs/memory.md → The operator CLI](https://github.com/Ink01101011/bantamkit/blob/main/docs/memory.md#the-operator-cli).
470
354
 
471
- ## The default is layered — do not add `--store` by reflex
355
+ ## Memory stores are layered by default
472
356
 
473
- With **no arguments**, which is what the config above passes and what production runs, the
474
- server binds `Memory.layered`: the project store discovered from the working directory,
475
- plus the profile layer, plus any `extra_stores` from `.bantamkit/config.yaml`. Recall lines
476
- are prefixed with their layer — `[project] `, `[extra:<name>]`, `[profile]`.
357
+ With **no arguments** — what every config above passes, and what production runs — the server
358
+ binds `Memory.layered`: the project store discovered from the working directory, plus the profile
359
+ layer, plus any `extra_stores` from `.bantamkit/config.yaml`. Recall lines are prefixed with their
360
+ layer: `[project] `, `[extra:<name>]`, `[profile]`.
477
361
 
478
- `--store <path>` binds one store and drops the tag. It is a debugging flag. A recall string
479
- produced under `--store` is not a string the deployment ever emits, and the cold-start gate
480
- asserts the layered form specifically for that reason.
362
+ `--store <path>` binds one store and drops the tag. It is a debugging flag: a recall string
363
+ produced under `--store` is not one the deployment ever emits, and the cold-start gate asserts the
364
+ layered form for that reason.
481
365
 
482
366
  ## The asset pack
483
367
 
484
- The pack is language-agnostic data (tool manifests, schemas, skills, rubrics, contract
485
- wording, eval fixtures) that lives at the **repository root**, one level above this
486
- package, and is shared verbatim with `runtime-py`. npm cannot reach outside a package
487
- directory, so `scripts/sync-assets.mjs` vendors it into `runtime-ts/assets/` on `prepack`;
488
- that directory is generated and git-ignored.
368
+ The pack is language-agnostic data — tool manifests, schemas, skills, rubrics, contract wording,
369
+ eval fixtures — that lives at the **repository root** and is shared verbatim with `runtime-py`.
370
+ npm cannot reach outside a package directory, so `scripts/sync-assets.mjs` vendors it into
371
+ `runtime-ts/assets/` on `prepack`; that directory is generated and git-ignored.
489
372
 
490
- `build_identity` hashes **every byte of the whole tree**, not just the 20 files the tools
491
- read, so the pack ships whole — 83 files / 212,480 bytes — or `assets_digest` and
492
- `build_id` change. `test/packaging.test.mjs` asserts that against what `npm pack` would
493
- actually put in the tarball, never against the source tree.
373
+ `build_identity` hashes **every byte of the whole tree**, not just the 20 files the tools read, so
374
+ the pack ships whole — 91 files / 238,751 bytes on 2026-09-15 — or `assets_digest` and `build_id` change.
375
+ `test/packaging.test.mjs` asserts that against what `npm pack` would put in the tarball.
494
376
 
495
377
  `assetsRoot()` mirrors `runtime-py/src/bantamkit/assets.py` arm for arm:
496
378
 
@@ -501,23 +383,19 @@ actually put in the tarball, never against the source tree.
501
383
 
502
384
  ## Sharing a store with the Python server
503
385
 
504
- Both servers read and write the same memory store, and byte-compatibility is the product,
505
- not a nice-to-have. Two things to know:
386
+ Both servers read and write the same memory store; byte-compatibility is the product.
506
387
 
507
388
  - **Concurrent writes are safe for the memory store and lossy for the checkpoint, in both
508
- runtimes.** `index.md` is derived from the `facts/` directory, so the last writer
509
- re-enumerates everything and the index self-heals — measured over 8 trials of two
510
- processes × 15 saves, zero disagreements between `facts/` and `index.md`. A checkpoint is
511
- not derived that way: `shiftwork_clock_out` is read-modify-write and the write depends on
512
- the document it read, so two concurrent clock-outs on the *same cursor unit* both answer
513
- `ok` and one history entry is lost. Measured 10/10 trials on Node **and** 10/10 on the
514
- Python reference — reference behaviour reproduced, not something the port introduced. The
515
- cursor check serialises the normal case; only the same-unit collision loses.
389
+ runtimes.** `index.md` is derived from `facts/`, so the last writer re-enumerates everything
390
+ and the index self-heals: 8 trials of two processes × 15 saves, zero disagreements. A
391
+ checkpoint is not: `shiftwork_clock_out` is read-modify-write, so two concurrent clock-outs on
392
+ the *same cursor unit* both answer `ok` and one history entry is lost — 10/10 trials on Node
393
+ **and** 10/10 on the Python reference. The cursor check serialises the normal case; only the
394
+ same-unit collision loses.
516
395
  - **Do not hand-edit a fact file's `name`, `description` or `type` to a bare non-string.**
517
- `description: 2026` is a YAML integer, not the text `2026`. Python interpolates it and
518
- carries on; this port refuses the whole store with `malformed fact file …`. Quote it:
519
- `description: '2026'`. Every fact the tools *write* is quoted correctly, so this only
520
- reaches you through an editor. See `src/memory/factfile.ts:257`.
396
+ `description: 2026` is a YAML integer. Python interpolates it and carries on; this port refuses
397
+ the whole store with `malformed fact file …`. Quote it: `description: '2026'`. Every fact the
398
+ tools *write* is quoted correctly (`src/memory/factfile.ts:257`).
521
399
 
522
400
  ## Development
523
401
 
@@ -531,13 +409,23 @@ npm pack --dry-run # vendors the pack via prepack, lists the tarball
531
409
  From the repository root:
532
410
 
533
411
  ```sh
534
- node tools/conformance/run.mjs --all # Node vs Python, 4300+ cases
412
+ node tools/conformance/run.mjs --all # Node vs Python, 8,130 cases on 2026-09-15
535
413
  node tools/conformance/npx-cold-start.mjs # pack, cold npx, warm npx, PATH
536
- node tools/conformance/npx-cold-start.mjs --offline # + the no-registry probe (~2.5 min)
414
+ node tools/conformance/npx-cold-start.mjs --offline # + the no-network arms (299.94 s on 2026-09-15)
537
415
  ```
538
416
 
539
- The conformance harness shells out to a CPython to produce the reference side. That is a
417
+ The conformance harness shells out to a CPython for the reference side. That is a
540
418
  **development-time** dependency of the test tooling; nothing under `tools/` ships, and
541
- `files: ["dist", "assets"]` is the whole published surface.
542
-
543
- Runtime dependencies: exactly one, `@modelcontextprotocol/sdk`, pinned to `1.30.0`.
419
+ `files: ["dist", "assets"]` is the whole published surface. Runtime dependencies: exactly one,
420
+ `@modelcontextprotocol/sdk`, pinned to `1.30.0`.
421
+
422
+ ## Measurements and history
423
+
424
+ Every number on this page was measured on the machine that wrote it, with a command you can
425
+ rerun. The measurements, dated amendments and transcripts that used to fill this page are in
426
+ [docs/install.md → npm package: measured detail](https://github.com/Ink01101011/bantamkit/blob/main/docs/install.md#npm-package-measured-detail):
427
+ cold-start cost and package size, the registry-unreachable and offline tables, what `--install`
428
+ recorded before and after 0.34.0, `--update` transcripts, the login-less PATH probe, what `npx`
429
+ gives up versus the `tools/bantamkit-mcp` launcher, and the `bantamkit-memory` session
430
+ transcripts. Where the two runtimes deliberately differ:
431
+ [docs/porting.md](https://github.com/Ink01101011/bantamkit/blob/main/docs/porting.md#where-the-two-runtimes-deliberately-differ).