bantamkit-mcp 0.33.0 → 0.34.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 +194 -21
- package/dist/cli.js +5 -4
- package/dist/eventlog.js +24 -1
- package/dist/hostinstall.d.ts +39 -7
- package/dist/hostinstall.js +105 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/memory/store.d.ts +47 -0
- package/dist/memory/store.js +67 -1
- package/dist/npminstall.d.ts +54 -0
- package/dist/npminstall.js +141 -0
- package/dist/selfupdate.d.ts +39 -38
- package/dist/selfupdate.js +62 -108
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,13 @@ That is the whole install. No `pip`, no venv, no `PYTHONPATH`. `runtime-ts/mcp.j
|
|
|
29
29
|
in the repository is the annotated version, with the four forms and the two measured
|
|
30
30
|
questions that decide between them.
|
|
31
31
|
|
|
32
|
+
> **AMENDED 2026-09-15 (job51) — this is no longer the recommended form.** The block above
|
|
33
|
+
> resolves the package against the registry at every launch. When the registry cannot be
|
|
34
|
+
> reached, it hangs silently, even with a warm cache (measured below). From 0.34.0,
|
|
35
|
+
> `npx -y bantamkit-mcp@latest --install <host>` writes an entry that starts with no network.
|
|
36
|
+
> See *Install once, run offline*. `mcp.json.example` now has five forms, and the kept install
|
|
37
|
+
> is the first.
|
|
38
|
+
|
|
32
39
|
**Pass `-y`.** `npx` historically prompted before installing a package it had not seen,
|
|
33
40
|
and stdin here is the JSON-RPC channel: a prompt that consumed one frame looks like a
|
|
34
41
|
server that lost a request. npm 11.6.2 was measured sending that prompt to stderr rather
|
|
@@ -49,6 +56,23 @@ npx -y bantamkit-mcp --install copilot # GitHub Copilot in VS Code
|
|
|
49
56
|
npx -y bantamkit-mcp --install cursor # Cursor
|
|
50
57
|
```
|
|
51
58
|
|
|
59
|
+
> **ADDED 2026-09-15 (job51) — what these commands record changes in 0.34.0.** 0.33.0 and
|
|
60
|
+
> earlier record `npx -y bantamkit-mcp`. From 0.34.0 they record
|
|
61
|
+
> `<absolute node> <absolute dist/cli.js>`:
|
|
62
|
+
>
|
|
63
|
+
> - **Run from an npx cache**, as the lines above are, the `cli.js` belongs to a *kept
|
|
64
|
+
> install* at `~/.bantamkit/mcp`. On Windows that is `%USERPROFILE%\.bantamkit\mcp`, which was
|
|
65
|
+
> not measured. The kept install is made once with `npm install --prefix`, and only when it
|
|
66
|
+
> does not already report this version.
|
|
67
|
+
> - **Run from any other install** (`npm i -g`, `npm i --prefix`, a checkout), it is that
|
|
68
|
+
> install's own `dist/cli.js`.
|
|
69
|
+
>
|
|
70
|
+
> If npm fails, no host file is read or written and no backup is taken. Add `@latest`
|
|
71
|
+
> (`npx -y bantamkit-mcp@latest --install <host>`) so npx does not reuse an older cached
|
|
72
|
+
> resolve. Two further points are explained in *Install once, run offline*. Moving from an
|
|
73
|
+
> existing `npx` entry needs `--force`, and on Claude Code it needs `claude mcp remove`
|
|
74
|
+
> first. The recorded node path names one node version.
|
|
75
|
+
|
|
52
76
|
`--install claude` runs `claude mcp add` rather than editing `~/.claude.json` directly:
|
|
53
77
|
that file is the host's, and it carries state that is not MCP configuration. What happens on
|
|
54
78
|
a second run there is Claude Code's decision, not this command's, and `--force` does not
|
|
@@ -63,6 +87,26 @@ not parse is refused rather than replaced.
|
|
|
63
87
|
The rest of this section is what those commands write, for anyone who would rather do it by
|
|
64
88
|
hand. Every host runs the same command; only the file and the key around it change.
|
|
65
89
|
|
|
90
|
+
**AMENDED 2026-09-15 (job51) — the forms below are the online ones.** They are what 0.33.0
|
|
91
|
+
and earlier write. From 0.34.0, `--install` writes the kept install instead, and that is also
|
|
92
|
+
the recommended form to write by hand. Make the install once, then register node and its
|
|
93
|
+
`cli.js`, both by absolute path:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm install --prefix ~/.bantamkit/mcp bantamkit-mcp@0.33.0
|
|
97
|
+
claude mcp add bantamkit -s user -- "$(node -p process.execPath)" ~/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Both lines were run against a scratch `HOME`. npm exited 0, and Claude Code recorded
|
|
101
|
+
`"command": "<…>/mise/installs/node/25.2.1/bin/node"` with the absolute `cli.js` as the one
|
|
102
|
+
argument. For the three JSON hosts below, keep the file and the key, keep Copilot's
|
|
103
|
+
`"type": "stdio"`, and use this `command`/`args` pair (`node -p process.execPath` prints the
|
|
104
|
+
first path):
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{"command": "/absolute/path/to/node", "args": ["/Users/you/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js"]}
|
|
108
|
+
```
|
|
109
|
+
|
|
66
110
|
**Claude Code** — one command, no file to edit. `-s user` makes it available in every
|
|
67
111
|
project; drop it for this project only.
|
|
68
112
|
|
|
@@ -174,6 +218,121 @@ exactly when nobody is watching.
|
|
|
174
218
|
If offline operation matters, do not use `npx`. Run `npm i -g bantamkit-mcp` once and
|
|
175
219
|
point the config at the installed binary, or keep using `tools/bantamkit-mcp`.
|
|
176
220
|
|
|
221
|
+
> **AMENDED 2026-09-15 (job51) — "A warm cache does not have this problem" is false on npm
|
|
222
|
+
> 11.6.2 for every spec a host config actually types.** Both paragraphs are kept rather than
|
|
223
|
+
> rewritten, because the table above still holds. With a warm cache and the network cut,
|
|
224
|
+
> `npx -y bantamkit-mcp@0.33.0`, `npx -y bantamkit-mcp` and `npx -y bantamkit-mcp@latest` all
|
|
225
|
+
> hung silently, as the next section measures. "Point the config at the installed binary"
|
|
226
|
+
> has a trap of its own under a GUI host: see the `.bin` row in that table. The next section
|
|
227
|
+
> replaces both paragraphs.
|
|
228
|
+
|
|
229
|
+
### Install once, run offline
|
|
230
|
+
|
|
231
|
+
`node tools/conformance/npx-cold-start.mjs --offline` printed every row below in a single run.
|
|
232
|
+
It ran on 2026-09-15 at commit `2f58af4`, with node v25.2.1 and npm 11.6.2, on macOS 26.6.2 on
|
|
233
|
+
Apple silicon, and took 299.94 s wall. It cuts the network by pointing `npm_config_proxy`
|
|
234
|
+
and `npm_config_https_proxy` at `http://127.0.0.1:1`. It does not use `npm_config_registry`,
|
|
235
|
+
which changes npm's cache key and would make a warm cache measure as cold. "GUI PATH" means
|
|
236
|
+
`/usr/bin:/bin:/usr/sbin:/sbin`, the PATH a GUI-launched host inherits on this machine.
|
|
237
|
+
|
|
238
|
+
| arm | outcome | wall | stdout |
|
|
239
|
+
|---|---|---|---|
|
|
240
|
+
| cold cache, registry refusing connections, `npx -y` | exit 1, 0 frames | 140.29 s | 0 bytes |
|
|
241
|
+
| registry spec, warm cache, network cut: `npx -y bantamkit-mcp@0.33.0` | **silent hang**, killed at the 45 s bound | 45.01 s | 0 bytes |
|
|
242
|
+
| the same, `npx -y bantamkit-mcp` | **silent hang** | 45.01 s | 0 bytes |
|
|
243
|
+
| the same, `npx -y bantamkit-mcp@latest` | **silent hang** | 45.02 s | 0 bytes |
|
|
244
|
+
| the same warm cache, network cut: `npx --offline -y bantamkit-mcp@0.33.0` | OK, 12 tools | 0.40 s | — |
|
|
245
|
+
| local tarball spec (`--package=<file>.tgz`), warm cache, network cut: `npx -y` | OK | 1.35 s | 27987 bytes |
|
|
246
|
+
| cold cache, `npx --offline -y` (tarball spec) | exit 1, `npm error code ENOTCACHED` | 2.69 s | — |
|
|
247
|
+
| kept install: `npm install --prefix <dir> <tarball>`, once, online | OK | 4.82 s | — |
|
|
248
|
+
| that install as `<abs node> <dir>/node_modules/bantamkit-mcp/dist/cli.js`, GUI PATH, network cut | OK, 12 tools | 0.09 s | — |
|
|
249
|
+
| that install's `node_modules/.bin/bantamkit-mcp`, same PATH | exit 127, `env: node: No such file or directory` | 0.16 s | — |
|
|
250
|
+
|
|
251
|
+
What the table says:
|
|
252
|
+
|
|
253
|
+
- **The hang comes from a registry spec, not from npx in general.** npx resolves a registry
|
|
254
|
+
spec against the registry at every start, so a warm cache does not save it. A local tarball
|
|
255
|
+
on its own warm cache answered in 1.35 s with the same cut. Do not read the three hung rows
|
|
256
|
+
as "every npx launch hangs offline".
|
|
257
|
+
- **`npx --offline` is the middle option.** It serves a warm cache without the registry, in
|
|
258
|
+
0.40 s here. On a cache that has never seen the package it fails at once with
|
|
259
|
+
`ENOTCACHED`. It suits a machine that has already run that exact spec online once, and it
|
|
260
|
+
cannot start a new one.
|
|
261
|
+
- **Only a kept install launched as node plus `cli.js` needs nothing.** No package is
|
|
262
|
+
resolved and no PATH lookup happens. The `.bin` shim fails because its first line is
|
|
263
|
+
`#!/usr/bin/env node`, and a GUI host's PATH has no `node`. An `npm i -g` bin runs the
|
|
264
|
+
same file, so the global advice above hits the same trap unless `node` is on the host's
|
|
265
|
+
PATH.
|
|
266
|
+
|
|
267
|
+
**From 0.34.0, `--install` builds the kept install for you.** Run as
|
|
268
|
+
`npx -y bantamkit-mcp@latest --install <host>`, it finds that it is running from an npx cache.
|
|
269
|
+
It then installs `bantamkit-mcp@<its own version>` into `~/.bantamkit/mcp` with
|
|
270
|
+
`npm install --prefix`, and records `<process.execPath> ~/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js`
|
|
271
|
+
with both paths absolute. It skips npm — and never downgrades — when the kept install already
|
|
272
|
+
reports this version or a newer one; an unparseable kept version sorts as newer and is left
|
|
273
|
+
alone too (J51-9a). Only an older, missing or blank kept version, or one whose `cli.js` is
|
|
274
|
+
gone, runs npm. Measured in this unit: a kept install's manifest was hand-edited to
|
|
275
|
+
`0.100.0` (newer than the `0.33.0` tarball below), then `--install`ed again with the network
|
|
276
|
+
cut (`npm_config_proxy`/`npm_config_https_proxy` pointed at a closed port) — exit 0 in 0.355 s,
|
|
277
|
+
npm never ran, and the kept manifest still read `0.100.0` afterward.
|
|
278
|
+
|
|
279
|
+
**This installs the registry's package for that version, not this tree's code.** The `npm
|
|
280
|
+
install --prefix` above asks the registry for `bantamkit-mcp@<version>`, so a checkout or an
|
|
281
|
+
`npm pack` tarball only supplies the version *number*; the bytes that end up in
|
|
282
|
+
`~/.bantamkit/mcp` are whatever the registry published under that number. Measured in this
|
|
283
|
+
unit: a `0.33.0` tarball packed from this tree carries the string `J51-9a` twice in
|
|
284
|
+
`dist/hostinstall.js` (`tar -xOzf bantamkit-mcp-0.33.0.tgz package/dist/hostinstall.js | grep
|
|
285
|
+
-c J51-9a` → `2`), but after `--install cursor` ran from that same tarball, the kept
|
|
286
|
+
`~/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/hostinstall.js` carries it zero times — the
|
|
287
|
+
kept install is the published `0.33.0`, which predates J51-9a, not the tree that was packed.
|
|
288
|
+
Measured end to end from `npm pack` of this tree (version string still 0.33.0), in a scratch
|
|
289
|
+
`HOME` with an empty npm cache — read the table below as *when* the install happens, not
|
|
290
|
+
*whose code* it installs:
|
|
291
|
+
|
|
292
|
+
| command | result |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `npx -y -p <tgz> bantamkit-mcp --install cursor` | exit 0 in 11.60 s; wrote `"command": "/Users/…/mise/installs/node/25.2.1/bin/node"` and `"args": ["<HOME>/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js"]` |
|
|
295
|
+
| the same with `--install copilot`, network cut | exit 0 in 0.35 s; npm did not run |
|
|
296
|
+
| `--install cursor` over an existing `npx -y bantamkit-mcp` entry | exit 1: `cursor already has a bantamkit entry with different settings` … `re-run with --force to replace it` |
|
|
297
|
+
| the same with `--force` | exit 0, `backup : …/mcp.json.backup-2026-09-15` |
|
|
298
|
+
|
|
299
|
+
0.34.0 was not yet on npm when this was written. Measured the same day,
|
|
300
|
+
`npx -y bantamkit-mcp@0.33.0 --install cursor` still writes
|
|
301
|
+
`"command": "npx", "args": ["-y", "bantamkit-mcp"]`. Until 0.34.0 is out, make the kept install
|
|
302
|
+
by hand, as shown in *Connect it to a host*.
|
|
303
|
+
|
|
304
|
+
**Two consequences to know.**
|
|
305
|
+
|
|
306
|
+
1. **The recorded node is one version of node.** Under mise, `process.execPath` is
|
|
307
|
+
`~/.local/share/mise/installs/node/25.2.1/bin/node`. Uninstall or switch away from that
|
|
308
|
+
version and the host can no longer spawn the server. Re-run
|
|
309
|
+
`npx -y bantamkit-mcp@latest --install <host> --force` with the node you have now. nvm
|
|
310
|
+
also keeps one directory per version; that was not measured here.
|
|
311
|
+
2. **Moving from an `npx` entry needs `--force`**, as the table shows. On Claude Code
|
|
312
|
+
`--force` does not help: `claude mcp add` refuses a name it already has
|
|
313
|
+
(`MCP server bantamkit already exists in user config`, exit 1, measured with Claude Code
|
|
314
|
+
2.1.270 against a scratch `HOME`). Run `claude mcp remove bantamkit -s user` first.
|
|
315
|
+
|
|
316
|
+
**Updating the kept install.** From 0.34.0, `npx -y bantamkit-mcp@latest --update` run from
|
|
317
|
+
an npx cache finds `~/.bantamkit/mcp`. It compares that install's version with the registry,
|
|
318
|
+
and when they differ it runs `npm install --prefix ~/.bantamkit/mcp bantamkit-mcp@latest`.
|
|
319
|
+
With no kept install, it refuses exactly as it did before. Measured with a kept 0.32.1 in a
|
|
320
|
+
scratch `HOME`:
|
|
321
|
+
|
|
322
|
+
```console
|
|
323
|
+
$ npx -y -p <tgz> bantamkit-mcp --update
|
|
324
|
+
bantamkit-mcp 0.32.1 is installed; the package index has 0.33.0.
|
|
325
|
+
updating from the package index: npm install --prefix <HOME>/.bantamkit/mcp bantamkit-mcp@latest
|
|
326
|
+
the command printed:
|
|
327
|
+
<npm's own output>
|
|
328
|
+
updated bantamkit-mcp from 0.32.1 to 0.33.0.
|
|
329
|
+
restart the server: a running bantamkit-mcp keeps serving the code it loaded at startup, so bantamkit_status will report 0.32.1 until the host reconnects.
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
That run exited 0 in 1.58 s. A second run printed `up to date.` and exited 0. Then reconnect
|
|
333
|
+
the server in the host. Nothing checks for updates at server startup, and startup never
|
|
334
|
+
touches the network.
|
|
335
|
+
|
|
177
336
|
### Measured: `npx` is not on a login-less PATH
|
|
178
337
|
|
|
179
338
|
```sh
|
|
@@ -206,6 +365,12 @@ Two ways out, both in `runtime-ts/mcp.json.example`:
|
|
|
206
365
|
{ "command": "/usr/local/bin/bantamkit-mcp", "args": [] }
|
|
207
366
|
```
|
|
208
367
|
|
|
368
|
+
> **AMENDED 2026-09-15 (job51) — the second form has the same problem one level down.**
|
|
369
|
+
> `bantamkit-mcp` runs `dist/cli.js`, whose first line is `#!/usr/bin/env node`. Under this
|
|
370
|
+
> PATH the kept install's `.bin` shim exited 127 (`env: node: No such file or directory`). A
|
|
371
|
+
> global link runs the same file, though it was not launched separately here. Name node by
|
|
372
|
+
> absolute path instead, as *Install once, run offline* shows.
|
|
373
|
+
|
|
209
374
|
## What `npx` gives up versus `tools/bantamkit-mcp`
|
|
210
375
|
|
|
211
376
|
The sh launcher is not obsolete. It guarantees things `npx` cannot, and this table is the
|
|
@@ -218,7 +383,7 @@ prep probe's, unsoftened.
|
|
|
218
383
|
| 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
384
|
| stdin is the JSON-RPC channel | yes | yes, with `-y` |
|
|
220
385
|
| 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 |
|
|
386
|
+
| starts without a network | yes | **no**, on a cold cache. *Amended 2026-09-15:* nor on a warm one, for a registry spec (silent hang at a 45 s bound; *Install once, run offline*). From 0.34.0 `--install` records a kept install that does start offline |
|
|
222
387
|
| `--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
388
|
| one config line, no clone, no venv | no | **yes.** This is the whole reason the package exists |
|
|
224
389
|
|
|
@@ -280,31 +445,39 @@ that were imported (RB-P84).
|
|
|
280
445
|
**Pin the version in the config** if you want this to be a non-issue:
|
|
281
446
|
`"args": ["-y", "bantamkit-mcp@0.30.0"]`.
|
|
282
447
|
|
|
448
|
+
A kept install (what `--install` records from 0.34.0) does not float either. It serves the
|
|
449
|
+
version in `~/.bantamkit/mcp` until someone runs `--update`.
|
|
450
|
+
|
|
283
451
|
## Updating
|
|
284
452
|
|
|
285
|
-
**
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
>
|
|
303
|
-
>
|
|
453
|
+
**Updating.** `bantamkit-mcp --update` — quoted verbatim from `--help`, run in this job
|
|
454
|
+
(`node runtime-ts/dist/cli.js --help`, the same line as `.venv/bin/bantamkit-mcp --help`):
|
|
455
|
+
`--update check the package index and update this install if it differs, then exit`. It asks
|
|
456
|
+
the npm registry for `latest` and compares it to the version installed. If they match, it
|
|
457
|
+
prints both numbers and `up to date.` and stops. If the index is ahead **and this is a
|
|
458
|
+
registry install, or, from 0.34.0, a kept install made by `--install`**, it runs the row below
|
|
459
|
+
that applies to you and prints what npm said. If the index is ahead and this is any OTHER
|
|
460
|
+
shape — a checkout, a linked tree, a local file, or an npx cache with no kept install — **it
|
|
461
|
+
refuses, exits 1, and names the row you are on**: it will not write a registry install into a
|
|
462
|
+
tree you manage with `git`, and a command that exits 0 having changed nothing is worse than
|
|
463
|
+
one that says no. Either way it ends by telling you to restart the server — **the running
|
|
464
|
+
server keeps serving the code it loaded at startup**, so every row ends with restarting it in
|
|
465
|
+
the host. The flag reaches the network and nothing else here does — not at startup, not on
|
|
466
|
+
`bantamkit_status`, not on any tool — with a 10-second timeout, and being offline is a named
|
|
467
|
+
refusal on stderr, never a traceback. The table below is the reference for what to do by hand,
|
|
468
|
+
and it is what `--update` prints back at you when it will not act.
|
|
469
|
+
|
|
470
|
+
> **J51-9b (2026-09-15).** This section used to open "There is no `bantamkit-mcp --update`,
|
|
471
|
+
> deliberately," amended below rather than rewritten (2026-09-11, when the flag first
|
|
472
|
+
> shipped). A review (F4) found that opening sentence sitting directly above this job's own
|
|
473
|
+
> `--update` table row and flagged it as actively wrong, not merely stale, so this time the
|
|
474
|
+
> paragraph is rewritten rather than amended again; it says nothing the 2026-09-11 amendment
|
|
475
|
+
> did not already say, plus the 0.34.0 kept-install route this job added to the table below.
|
|
304
476
|
|
|
305
477
|
| how it was installed | how to update |
|
|
306
478
|
|---|---|
|
|
307
479
|
| `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. |
|
|
480
|
+
| `npx -y bantamkit-mcp --install <host>` from 0.34.0: a kept install at `~/.bantamkit/mcp` (added 2026-09-15) | `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
481
|
| `npm i -g bantamkit-mcp` | `npm i -g bantamkit-mcp@latest` |
|
|
309
482
|
| `npm i --prefix <dir> bantamkit-mcp` | `npm i --prefix <dir> bantamkit-mcp@latest` |
|
|
310
483
|
| PyPI (`pip install "bantamkit[mcp]"`) | `pip install -U "bantamkit[mcp]"` · pipx: `pipx upgrade bantamkit` · uv: `uv tool upgrade bantamkit` |
|
|
@@ -533,7 +706,7 @@ From the repository root:
|
|
|
533
706
|
```sh
|
|
534
707
|
node tools/conformance/run.mjs --all # Node vs Python, 4300+ cases
|
|
535
708
|
node tools/conformance/npx-cold-start.mjs # pack, cold npx, warm npx, PATH
|
|
536
|
-
node tools/conformance/npx-cold-start.mjs --offline # + the no-
|
|
709
|
+
node tools/conformance/npx-cold-start.mjs --offline # + the no-network arms (299.94 s on 2026-09-15)
|
|
537
710
|
```
|
|
538
711
|
|
|
539
712
|
The conformance harness shells out to a CPython to produce the reference side. That is a
|
package/dist/cli.js
CHANGED
|
@@ -81,7 +81,7 @@ import { buildServer } from './mcp/server.js';
|
|
|
81
81
|
import { RawStdioTransport } from './mcp/transport.js';
|
|
82
82
|
import { ArgumentTypeError, ArgvError, formatHelp, formatUsageBlock, HelpRequested, helpWidth, parseArgs as parseWithSpec, pyIntStrict, } from './pyargparse.js';
|
|
83
83
|
import { pyJoin, PyOSError, pyRepr, pyStatIsDir } from './memory/pyfs.js';
|
|
84
|
-
import { HOSTS,
|
|
84
|
+
import { HOSTS, InstallError, installSelf } from './hostinstall.js';
|
|
85
85
|
import { runUpdate } from './selfupdate.js';
|
|
86
86
|
import { statusLine } from './statusline.js';
|
|
87
87
|
/** `SystemExit("...")`: the message on stderr, exit 1. */
|
|
@@ -471,10 +471,11 @@ async function main(argv) {
|
|
|
471
471
|
// earlier here made the same argv write a file on one runtime and not the other: one
|
|
472
472
|
// command line, two different states on the user's disk. Reviewed and moved.
|
|
473
473
|
if (options.install !== null && options.install !== undefined) {
|
|
474
|
-
// Before any store or transport exists, the shape `--assets-root` established.
|
|
475
|
-
|
|
474
|
+
// Before any store or transport exists, the shape `--assets-root` established. The command
|
|
475
|
+
// is resolved INSIDE the try: on an `npx` cache it makes the kept install first, and npm
|
|
476
|
+
// failing there is an `InstallError` like any other refusal — `error:` and exit 1.
|
|
476
477
|
try {
|
|
477
|
-
process.stdout.write(`${
|
|
478
|
+
process.stdout.write(`${installSelf(options.install, options.force, { version: version() })}\n`);
|
|
478
479
|
}
|
|
479
480
|
catch (e) {
|
|
480
481
|
if (!(e instanceof InstallError))
|
package/dist/eventlog.js
CHANGED
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
* notice — the failure mode a diagnostic can least afford.
|
|
56
56
|
*/
|
|
57
57
|
import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
58
|
-
import { cmpCodepoint, osErrorClassName, PyOSError, pyJoin, pyParent } from './memory/pyfs.js';
|
|
58
|
+
import { cmpCodepoint, osErrorClassName, pyExists, PyOSError, pyJoin, pyName, pyParent, pyParents } from './memory/pyfs.js';
|
|
59
|
+
import { ensureBantamkitGitignore } from './memory/store.js';
|
|
59
60
|
/**
|
|
60
61
|
* Environment switch. Unset or `off`/`0`/`false`/`no`/empty -> disabled. `on`/`1`/`true`/
|
|
61
62
|
* `yes` -> the default file inside the memory store. Anything else is taken as the literal
|
|
@@ -242,11 +243,33 @@ export class EventLog {
|
|
|
242
243
|
* by construction, so that branch is a statement of intent, not a live path.
|
|
243
244
|
*/
|
|
244
245
|
append(path, payload) {
|
|
246
|
+
// AUDIT FINDING (J51-1, mirrored from `runtime-py/src/bantamkit/eventlog.py`): the
|
|
247
|
+
// `mkdirSync` below can bring a whole `.bantamkit` directory into existence on its own
|
|
248
|
+
// -- `BANTAMKIT_EVENT_LOG=on` with a project store that was never saved to reaches here
|
|
249
|
+
// first -- entirely bypassing `MemoryStore.ensureDirs`, which is the only other place a
|
|
250
|
+
// `.bantamkit` directory gets created. Without this call a store built that way would
|
|
251
|
+
// never get its self-ignoring `.gitignore`. Cheap and idempotent: a no-op unless one of
|
|
252
|
+
// `path`'s ancestors is literally named `.bantamkit`.
|
|
253
|
+
//
|
|
254
|
+
// USER RULING #2 (J51-8b): the ignore file is written only when THIS call is what
|
|
255
|
+
// creates `.bantamkit`, so existence has to be checked BEFORE the mkdir below -- after
|
|
256
|
+
// it, the directory unconditionally exists and the question is unanswerable.
|
|
257
|
+
let bantamkitDir = null;
|
|
258
|
+
for (const parent of pyParents(path)) {
|
|
259
|
+
if (pyName(parent) === '.bantamkit') {
|
|
260
|
+
bantamkitDir = parent;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const bantamkitDirExistedBefore = bantamkitDir !== null && pyExists(bantamkitDir);
|
|
245
265
|
// `pyParent` and not `node:path`'s `dirname`: this module is reached from a Windows
|
|
246
266
|
// host through the same code, and the reference's `Path(...).parent` is the spelling
|
|
247
267
|
// that understands a drive letter. `.`'s parent is `.` in both, so a bare filename
|
|
248
268
|
// makes the directory that already exists rather than reaching for the root.
|
|
249
269
|
mkdirSync(pyParent(path), { recursive: true });
|
|
270
|
+
if (bantamkitDir !== null) {
|
|
271
|
+
ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
|
|
272
|
+
}
|
|
250
273
|
let size = 0;
|
|
251
274
|
try {
|
|
252
275
|
size = statSync(path).size;
|
package/dist/hostinstall.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { keptCli, keptPrefix } from './npminstall.js';
|
|
2
|
+
export { keptCli, keptPrefix };
|
|
1
3
|
/** The four hosts, in the order they print in `--help`. */
|
|
2
4
|
export declare const HOSTS: readonly ["claude", "claude-desktop", "copilot", "cursor"];
|
|
3
5
|
export type Host = (typeof HOSTS)[number];
|
|
@@ -21,16 +23,46 @@ export declare function configKey(host: Host): string;
|
|
|
21
23
|
export declare function entryFor(host: Host, command: string, args: string[]): Record<string, unknown>;
|
|
22
24
|
/** Register this server with `host`. Returns the report to print; throws `InstallError`. */
|
|
23
25
|
export declare function install(host: Host, command: string, args: string[], force?: boolean): string;
|
|
26
|
+
/** The seams `thisCommand` takes, so no test ever reaches npm or the network. */
|
|
27
|
+
export interface CommandOptions {
|
|
28
|
+
/** The install shape. Default: `currentInstall().shape` — never a second detector. */
|
|
29
|
+
readonly shape?: () => string;
|
|
30
|
+
/** Runs an argv and returns `[exit code, output]`. Default: `npminstall.runInstaller`. */
|
|
31
|
+
readonly installer?: (command: string[]) => [number, string];
|
|
32
|
+
/** This process's version. Default: the `package.json` this build ships with. */
|
|
33
|
+
readonly version?: string;
|
|
34
|
+
}
|
|
24
35
|
/**
|
|
25
|
-
* What a host should run to get THIS server.
|
|
36
|
+
* What a host should run to get THIS server: `<absolute node> <absolute dist/cli.js>`.
|
|
37
|
+
*
|
|
38
|
+
* NOT `npx -y bantamkit-mcp` ANY MORE — see the module header for the measurement: with a warm
|
|
39
|
+
* cache and no network that command hangs silently, so a host that launches it offline sees a
|
|
40
|
+
* handshake timeout. Not a bare `node` either, because a GUI host's PATH need not have one, and
|
|
41
|
+
* not a `.bin` shim, whose `#!/usr/bin/env node` fails the same way (measured: exit 127).
|
|
42
|
+
* `process.execPath` is the node binary running THIS process, which is known to work.
|
|
43
|
+
*
|
|
44
|
+
* WHICH `cli.js` DEPENDS ON THE INSTALL SHAPE, and the shape is `currentInstall()`'s answer:
|
|
26
45
|
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
46
|
+
* - `ephemeral` (an `npx` cache): this process's own `cli.js` lives in a cache npm may discard,
|
|
47
|
+
* so the recorded one is the KEPT install at `keptPrefix()`. If its manifest already reports
|
|
48
|
+
* this version OR A NEWER ONE and its `cli.js` is there, nothing runs — a second `--install`
|
|
49
|
+
* for another host works offline, and a stale `npx` cache never moves a kept install that
|
|
50
|
+
* `--update` took further back to its own version (J51-9a). Otherwise `npm install --prefix <kept> bantamkit-mcp@<this version>`
|
|
51
|
+
* runs once, through the same spawner `--update` uses, and the `cli.js` is confirmed after.
|
|
52
|
+
* npm creates a missing prefix directory itself (measured, npm 11.6.2), so nothing is made
|
|
53
|
+
* here first and a failed install leaves no empty directory of this module's making.
|
|
54
|
+
* - every other shape (`registry`, `local-file`, `linked`, `checkout`) is already kept, so it
|
|
55
|
+
* is this process's own `dist/cli.js`.
|
|
56
|
+
*
|
|
57
|
+
* Every failure is an `InstallError` thrown BEFORE any host configuration is read or written.
|
|
32
58
|
*/
|
|
33
|
-
export declare function thisCommand(): {
|
|
59
|
+
export declare function thisCommand(options?: CommandOptions): {
|
|
34
60
|
command: string;
|
|
35
61
|
args: string[];
|
|
36
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* `--install <host>` whole: resolve the command (making the kept install if this is an `npx`
|
|
65
|
+
* cache), THEN touch the host. The order is the property: a refusal from npm leaves the host's
|
|
66
|
+
* file byte-unchanged with no backup, because `install` has not been reached.
|
|
67
|
+
*/
|
|
68
|
+
export declare function installSelf(host: Host, force?: boolean, options?: CommandOptions): string;
|
package/dist/hostinstall.js
CHANGED
|
@@ -17,15 +17,29 @@
|
|
|
17
17
|
* runtimes write different bytes for the same install.
|
|
18
18
|
*
|
|
19
19
|
* WHAT IS RULED DIFFERENT, and it is one field: the COMMAND. This side records
|
|
20
|
-
*
|
|
21
|
-
* should be the thing that answers, and neither side should send a host
|
|
22
|
-
* other's runtime. `docs/porting.md` carries the row.
|
|
20
|
+
* `<absolute node> <absolute .../dist/cli.js>`; the reference records its own console script.
|
|
21
|
+
* The thing installed should be the thing that answers, and neither side should send a host
|
|
22
|
+
* looking for the other's runtime. `docs/porting.md` carries the row.
|
|
23
|
+
*
|
|
24
|
+
* JOB51 CHANGED THIS SIDE'S COMMAND, AND THE REASON IS A MEASUREMENT. It used to be
|
|
25
|
+
* `npx -y bantamkit-mcp`. On npm 11.6.2, with a WARM cache and the network cut (proxies pointed
|
|
26
|
+
* at a closed port, which leaves the cache key alone), that command — pinned, unpinned or
|
|
27
|
+
* `@latest` — hung silently past 45 s with zero bytes on stdout, so a host reports a handshake
|
|
28
|
+
* timeout. A kept `npm install --prefix` launched as `<abs node> <abs dist/cli.js>` answered in
|
|
29
|
+
* 0.11 s under a GUI-shaped PATH (`/usr/bin:/bin:/usr/sbin:/sbin`), where that install's own
|
|
30
|
+
* `.bin/bantamkit-mcp` shim exits 127 (`env: node: No such file or directory`). The user ruled
|
|
31
|
+
* (2026-09-15): `--install` makes a one-time kept install under `~/.bantamkit`, and the host
|
|
32
|
+
* launches it without the network. See `thisCommand`.
|
|
23
33
|
*/
|
|
24
34
|
import { spawnSync } from 'node:child_process';
|
|
25
35
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
26
36
|
import { homedir } from 'node:os';
|
|
27
37
|
import { dirname, join } from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
import { currentInstall, Undetermined } from './mcp/identity.js';
|
|
40
|
+
import { compareVersions, keptCli, keptManifest, keptPrefix, PACKAGE, runInstaller, shlexJoin } from './npminstall.js';
|
|
28
41
|
import { dumpJson, fromJs } from './pyjson.js';
|
|
42
|
+
export { keptCli, keptPrefix };
|
|
29
43
|
/** The four hosts, in the order they print in `--help`. */
|
|
30
44
|
export const HOSTS = ['claude', 'claude-desktop', 'copilot', 'cursor'];
|
|
31
45
|
/** The entry name written into every host. */
|
|
@@ -233,15 +247,95 @@ export function install(host, command, args, force = false) {
|
|
|
233
247
|
lines.push(` backup : ${copied}`);
|
|
234
248
|
return lines.join('\n');
|
|
235
249
|
}
|
|
250
|
+
function ownVersion() {
|
|
251
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
252
|
+
.version;
|
|
253
|
+
}
|
|
254
|
+
/** The kept install's version, or `null` when there is no readable manifest with one. */
|
|
255
|
+
function keptVersion(prefix) {
|
|
256
|
+
try {
|
|
257
|
+
const manifest = JSON.parse(readFileSync(keptManifest(prefix), 'utf8'));
|
|
258
|
+
return typeof manifest.version === 'string' ? manifest.version : null;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** The installer's last non-blank line — stderr's, when npm wrote any, since it is appended last. */
|
|
265
|
+
function lastLine(output) {
|
|
266
|
+
const lines = output.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== '');
|
|
267
|
+
return lines.length === 0 ? 'it printed nothing' : (lines[lines.length - 1] ?? '');
|
|
268
|
+
}
|
|
236
269
|
/**
|
|
237
|
-
* What a host should run to get THIS server.
|
|
270
|
+
* What a host should run to get THIS server: `<absolute node> <absolute dist/cli.js>`.
|
|
271
|
+
*
|
|
272
|
+
* NOT `npx -y bantamkit-mcp` ANY MORE — see the module header for the measurement: with a warm
|
|
273
|
+
* cache and no network that command hangs silently, so a host that launches it offline sees a
|
|
274
|
+
* handshake timeout. Not a bare `node` either, because a GUI host's PATH need not have one, and
|
|
275
|
+
* not a `.bin` shim, whose `#!/usr/bin/env node` fails the same way (measured: exit 127).
|
|
276
|
+
* `process.execPath` is the node binary running THIS process, which is known to work.
|
|
238
277
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
278
|
+
* WHICH `cli.js` DEPENDS ON THE INSTALL SHAPE, and the shape is `currentInstall()`'s answer:
|
|
279
|
+
*
|
|
280
|
+
* - `ephemeral` (an `npx` cache): this process's own `cli.js` lives in a cache npm may discard,
|
|
281
|
+
* so the recorded one is the KEPT install at `keptPrefix()`. If its manifest already reports
|
|
282
|
+
* this version OR A NEWER ONE and its `cli.js` is there, nothing runs — a second `--install`
|
|
283
|
+
* for another host works offline, and a stale `npx` cache never moves a kept install that
|
|
284
|
+
* `--update` took further back to its own version (J51-9a). Otherwise `npm install --prefix <kept> bantamkit-mcp@<this version>`
|
|
285
|
+
* runs once, through the same spawner `--update` uses, and the `cli.js` is confirmed after.
|
|
286
|
+
* npm creates a missing prefix directory itself (measured, npm 11.6.2), so nothing is made
|
|
287
|
+
* here first and a failed install leaves no empty directory of this module's making.
|
|
288
|
+
* - every other shape (`registry`, `local-file`, `linked`, `checkout`) is already kept, so it
|
|
289
|
+
* is this process's own `dist/cli.js`.
|
|
290
|
+
*
|
|
291
|
+
* Every failure is an `InstallError` thrown BEFORE any host configuration is read or written.
|
|
244
292
|
*/
|
|
245
|
-
export function thisCommand() {
|
|
246
|
-
|
|
293
|
+
export function thisCommand(options = {}) {
|
|
294
|
+
let shape;
|
|
295
|
+
try {
|
|
296
|
+
shape = (options.shape ?? (() => currentInstall().shape))();
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
if (!(e instanceof Undetermined))
|
|
300
|
+
throw e;
|
|
301
|
+
throw new InstallError(`--install could not tell how this install was made, so it will not guess which copy a host should launch: ${e.message}`);
|
|
302
|
+
}
|
|
303
|
+
const node = process.execPath;
|
|
304
|
+
if (shape !== 'ephemeral') {
|
|
305
|
+
return { command: node, args: [fileURLToPath(new URL('./cli.js', import.meta.url))] };
|
|
306
|
+
}
|
|
307
|
+
const version = options.version ?? ownVersion();
|
|
308
|
+
const prefix = keptPrefix();
|
|
309
|
+
const cli = keptCli(prefix);
|
|
310
|
+
const kept = keptVersion(prefix);
|
|
311
|
+
// NEVER LOWER THE KEPT INSTALL (J51-9a). An `npx` cache can be older than the kept install —
|
|
312
|
+
// `--update` moved the kept install on, and this cache was filled weeks ago — and every host
|
|
313
|
+
// already launches the kept one, so installing THIS version over it would move them all back.
|
|
314
|
+
// A kept version at or above this one is recorded as it is. `compareVersions` is total: a
|
|
315
|
+
// component that is not all digits sorts after every number, so a version it cannot read as
|
|
316
|
+
// numbers counts as newer and is left alone rather than overwritten. A blank one is no version.
|
|
317
|
+
if (kept !== null && kept.trim() !== '' && compareVersions(kept, version) >= 0 && existsSync(cli)) {
|
|
318
|
+
return { command: node, args: [cli] };
|
|
319
|
+
}
|
|
320
|
+
const command = ['npm', 'install', '--prefix', prefix, `${PACKAGE}@${version}`];
|
|
321
|
+
const rendered = shlexJoin(command);
|
|
322
|
+
const [code, output] = (options.installer ?? runInstaller)(command);
|
|
323
|
+
const refused = `could not make the kept install at ${prefix}, so no host configuration was changed: ${rendered} exited ${code}`;
|
|
324
|
+
if (code !== 0)
|
|
325
|
+
throw new InstallError(`${refused}: ${lastLine(output)}`);
|
|
326
|
+
if (!existsSync(cli))
|
|
327
|
+
throw new InstallError(`${refused} but left no ${cli}`);
|
|
328
|
+
return { command: node, args: [cli] };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* `--install <host>` whole: resolve the command (making the kept install if this is an `npx`
|
|
332
|
+
* cache), THEN touch the host. The order is the property: a refusal from npm leaves the host's
|
|
333
|
+
* file byte-unchanged with no backup, because `install` has not been reached.
|
|
334
|
+
*/
|
|
335
|
+
export function installSelf(host, force = false, options = {}) {
|
|
336
|
+
if (!HOSTS.includes(host)) {
|
|
337
|
+
throw new InstallError(`unknown host '${host}'; choose one of: ${HOSTS.join(', ')}`);
|
|
338
|
+
}
|
|
339
|
+
const { command, args } = thisCommand(options);
|
|
340
|
+
return install(host, command, args, force);
|
|
247
341
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { absolutise, blockKey, claimSlot, collapse, dream, formatFixed3, mergeBo
|
|
|
18
18
|
export type { DateHit, DreamMerge, DreamResult, SimilarPair, Superseded } from './memory/dream.js';
|
|
19
19
|
export { CAP_BYTES, DEFAULT_RELATIVE_PATH, defaultPath, encodeRecord, EVENT_LOG_ENV, EventLog, formatTimestamp, resolvePath, SCHEMA_VERSION, } from './eventlog.js';
|
|
20
20
|
export type { DetailValue } from './eventlog.js';
|
|
21
|
-
export { DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
21
|
+
export { BANTAMKIT_GITIGNORE_TEXT, DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, ensureBantamkitGitignore, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
22
22
|
export type { MemoryStoreOptions, SaveResult, StoreInternals } from './memory/store.js';
|
|
23
23
|
export { asPyOSError, cmpCodepoint, matchesMd, normcase, pyDecodeUtf8, pyCwd, pyExists, pyExpanduser, pyHome, pyIsAbsolute, pyIsDir, pyIsFile, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, PyOSError, pyParent, pyParents, pyReadText, pyReplace, pyRepr, pyResolve, PyRuntimeError, pyScandirNames, PyUnicodeDecodeError, pyAppendText, pyStatIsDir, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, sortedPathNames, sortedPathParts, STRERROR_NAMES, } from './memory/pyfs.js';
|
|
24
24
|
export { clockIn, clockOut, HISTORY_RING_SIZE, SCHEMA_NAME, status, TERMINAL_UNIT_STATUS, timestamp, } from './shiftwork.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,6 @@ export { countFacts, discoverProjectStore, loadGrants, MEMORY_DIR_ENV, PROJECT_S
|
|
|
9
9
|
export { layerLabel, Memory, normalizeName, profileStore } from './memory/component.js';
|
|
10
10
|
export { absolutise, blockKey, claimSlot, collapse, dream, formatFixed3, mergeBodies, mergeDescriptions, mergeLinks, PROFILE_LAYER, PROJECT_LAYER, pyIntDigits, sortedNames, splitBlocks, SUPERSEDED_HEADING, } from './memory/dream.js';
|
|
11
11
|
export { CAP_BYTES, DEFAULT_RELATIVE_PATH, defaultPath, encodeRecord, EVENT_LOG_ENV, EventLog, formatTimestamp, resolvePath, SCHEMA_VERSION, } from './eventlog.js';
|
|
12
|
-
export { DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
12
|
+
export { BANTAMKIT_GITIGNORE_TEXT, DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, ensureBantamkitGitignore, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
13
13
|
export { asPyOSError, cmpCodepoint, matchesMd, normcase, pyDecodeUtf8, pyCwd, pyExists, pyExpanduser, pyHome, pyIsAbsolute, pyIsDir, pyIsFile, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, PyOSError, pyParent, pyParents, pyReadText, pyReplace, pyRepr, pyResolve, PyRuntimeError, pyScandirNames, PyUnicodeDecodeError, pyAppendText, pyStatIsDir, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, sortedPathNames, sortedPathParts, STRERROR_NAMES, } from './memory/pyfs.js';
|
|
14
14
|
export { clockIn, clockOut, HISTORY_RING_SIZE, SCHEMA_NAME, status, TERMINAL_UNIT_STATUS, timestamp, } from './shiftwork.js';
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import { BantamError } from '../errors.js';
|
|
2
2
|
import type { Fact, FactValue } from './factfile.js';
|
|
3
3
|
export type { Fact } from './factfile.js';
|
|
4
|
+
/**
|
|
5
|
+
* The exact bytes written into a `.bantamkit/.gitignore` the first time such a directory is
|
|
6
|
+
* brought into existence (user ruling 2026-09-15, `.shiftwork/notes-job51/P0-probes.md`).
|
|
7
|
+
* Both runtimes hold this text byte-for-byte -- a conformance case pins it against the
|
|
8
|
+
* Python literal -- so it is spelled once, here, rather than re-typed at each call site.
|
|
9
|
+
* `runtime-py/src/bantamkit/memory/store.py`'s `BANTAMKIT_GITIGNORE_TEXT` is the reference.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BANTAMKIT_GITIGNORE_TEXT: string;
|
|
12
|
+
/**
|
|
13
|
+
* Best-effort, idempotent: give `directory` a self-ignoring `.gitignore` if it is literally
|
|
14
|
+
* named `.bantamkit` AND `created` says THIS CALLER'S OWN mkdir is what just brought it into
|
|
15
|
+
* existence.
|
|
16
|
+
*
|
|
17
|
+
* USER RULING #2 (2026-09-15, after J51-8 measured the fallout of the first rule): the
|
|
18
|
+
* ignore file is written only when bantamkit itself creates the `.bantamkit` directory. An
|
|
19
|
+
* existing `.bantamkit` -- made by an earlier bantamkit, by hand, or checked out from git;
|
|
20
|
+
* with or without a `.gitignore` already in it -- is never given one. That is why `created`
|
|
21
|
+
* is the caller's job, not this function's: only the caller knows whether `.bantamkit`
|
|
22
|
+
* existed immediately before ITS mkdir, because by the time this function runs the directory
|
|
23
|
+
* unconditionally exists either way. (Was: written whenever the ignore file itself was
|
|
24
|
+
* absent, regardless of whether `.bantamkit` predated the call -- which meant deleting the
|
|
25
|
+
* file never stuck, and a store a team already commits would silently start ignoring new
|
|
26
|
+
* fact files after an upgrade. `P0-probes.md` ruling 4.)
|
|
27
|
+
*
|
|
28
|
+
* THE PROPERTY, not the mechanism: whenever a `.bantamkit` directory is brought into
|
|
29
|
+
* existence -- by `MemoryStore.ensureDirs`, or by anything else in this runtime that creates
|
|
30
|
+
* one, such as `EventLog`'s own append-time `mkdirSync` when the event log is on and the
|
|
31
|
+
* project store was never saved to -- this is the one place that decides whether it gets
|
|
32
|
+
* ignored. A second creator that skipped this call, or that got `created` wrong, would leave
|
|
33
|
+
* a `.bantamkit` that git can see (or ignore one it should not), which is the whole bug this
|
|
34
|
+
* closes.
|
|
35
|
+
*
|
|
36
|
+
* NEVER REWRITES. A `.gitignore` that already exists -- whatever its bytes -- is left
|
|
37
|
+
* exactly as it is, kept as a second guard even when `created` is `true`: an operator who
|
|
38
|
+
* deleted it to commit the directory keeps that decision, and this never diffs its own
|
|
39
|
+
* output against what is on disk.
|
|
40
|
+
*
|
|
41
|
+
* NEVER RAISES. Failing to write the ignore file must never be why a fact does not get
|
|
42
|
+
* saved -- the parent write still has to succeed exactly as it does today. An unwritable
|
|
43
|
+
* filesystem, a permissions error, or the directory disappearing under this call are all
|
|
44
|
+
* swallowed, mirroring the reference's `except OSError: pass`.
|
|
45
|
+
*
|
|
46
|
+
* NEVER CREATES `directory` ITSELF. A write into a missing parent fails like any other
|
|
47
|
+
* filesystem error here, so calling this before `directory` exists is a no-op, not a way to
|
|
48
|
+
* bring `.bantamkit` into existence early.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ensureBantamkitGitignore(directory: string, created: boolean): void;
|
|
4
51
|
/** `sorted(VALID_TYPES)` is what the error message interpolates, so the order is load-bearing. */
|
|
5
52
|
export declare const VALID_TYPES: readonly ["feedback", "project", "reference", "user"];
|
|
6
53
|
/**
|
package/dist/memory/store.js
CHANGED
|
@@ -42,7 +42,67 @@ import { basename } from 'node:path';
|
|
|
42
42
|
import { BantamError } from '../errors.js';
|
|
43
43
|
import { formatFact, parseFrontmatter, pySplit, pyStrip, todayLocal } from './factfile.js';
|
|
44
44
|
import { PyScalar } from './pyyaml.js';
|
|
45
|
-
import { asPyOSError, cmpCodepoint, matchesMd, pyExists, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, pyReadText, pyReplace, pyScandirNames, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, PyOSError, sortedPathNames, } from './pyfs.js';
|
|
45
|
+
import { asPyOSError, cmpCodepoint, matchesMd, pyExists, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, pyParent, pyReadText, pyReplace, pyScandirNames, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, PyOSError, sortedPathNames, } from './pyfs.js';
|
|
46
|
+
/**
|
|
47
|
+
* The exact bytes written into a `.bantamkit/.gitignore` the first time such a directory is
|
|
48
|
+
* brought into existence (user ruling 2026-09-15, `.shiftwork/notes-job51/P0-probes.md`).
|
|
49
|
+
* Both runtimes hold this text byte-for-byte -- a conformance case pins it against the
|
|
50
|
+
* Python literal -- so it is spelled once, here, rather than re-typed at each call site.
|
|
51
|
+
* `runtime-py/src/bantamkit/memory/store.py`'s `BANTAMKIT_GITIGNORE_TEXT` is the reference.
|
|
52
|
+
*/
|
|
53
|
+
export const BANTAMKIT_GITIGNORE_TEXT = '# Created by bantamkit: this directory is local state. Delete this file to commit it.\n' +
|
|
54
|
+
'*\n';
|
|
55
|
+
/**
|
|
56
|
+
* Best-effort, idempotent: give `directory` a self-ignoring `.gitignore` if it is literally
|
|
57
|
+
* named `.bantamkit` AND `created` says THIS CALLER'S OWN mkdir is what just brought it into
|
|
58
|
+
* existence.
|
|
59
|
+
*
|
|
60
|
+
* USER RULING #2 (2026-09-15, after J51-8 measured the fallout of the first rule): the
|
|
61
|
+
* ignore file is written only when bantamkit itself creates the `.bantamkit` directory. An
|
|
62
|
+
* existing `.bantamkit` -- made by an earlier bantamkit, by hand, or checked out from git;
|
|
63
|
+
* with or without a `.gitignore` already in it -- is never given one. That is why `created`
|
|
64
|
+
* is the caller's job, not this function's: only the caller knows whether `.bantamkit`
|
|
65
|
+
* existed immediately before ITS mkdir, because by the time this function runs the directory
|
|
66
|
+
* unconditionally exists either way. (Was: written whenever the ignore file itself was
|
|
67
|
+
* absent, regardless of whether `.bantamkit` predated the call -- which meant deleting the
|
|
68
|
+
* file never stuck, and a store a team already commits would silently start ignoring new
|
|
69
|
+
* fact files after an upgrade. `P0-probes.md` ruling 4.)
|
|
70
|
+
*
|
|
71
|
+
* THE PROPERTY, not the mechanism: whenever a `.bantamkit` directory is brought into
|
|
72
|
+
* existence -- by `MemoryStore.ensureDirs`, or by anything else in this runtime that creates
|
|
73
|
+
* one, such as `EventLog`'s own append-time `mkdirSync` when the event log is on and the
|
|
74
|
+
* project store was never saved to -- this is the one place that decides whether it gets
|
|
75
|
+
* ignored. A second creator that skipped this call, or that got `created` wrong, would leave
|
|
76
|
+
* a `.bantamkit` that git can see (or ignore one it should not), which is the whole bug this
|
|
77
|
+
* closes.
|
|
78
|
+
*
|
|
79
|
+
* NEVER REWRITES. A `.gitignore` that already exists -- whatever its bytes -- is left
|
|
80
|
+
* exactly as it is, kept as a second guard even when `created` is `true`: an operator who
|
|
81
|
+
* deleted it to commit the directory keeps that decision, and this never diffs its own
|
|
82
|
+
* output against what is on disk.
|
|
83
|
+
*
|
|
84
|
+
* NEVER RAISES. Failing to write the ignore file must never be why a fact does not get
|
|
85
|
+
* saved -- the parent write still has to succeed exactly as it does today. An unwritable
|
|
86
|
+
* filesystem, a permissions error, or the directory disappearing under this call are all
|
|
87
|
+
* swallowed, mirroring the reference's `except OSError: pass`.
|
|
88
|
+
*
|
|
89
|
+
* NEVER CREATES `directory` ITSELF. A write into a missing parent fails like any other
|
|
90
|
+
* filesystem error here, so calling this before `directory` exists is a no-op, not a way to
|
|
91
|
+
* bring `.bantamkit` into existence early.
|
|
92
|
+
*/
|
|
93
|
+
export function ensureBantamkitGitignore(directory, created) {
|
|
94
|
+
if (pyName(directory) !== '.bantamkit' || !created)
|
|
95
|
+
return;
|
|
96
|
+
const gitignore = pyJoin(directory, '.gitignore');
|
|
97
|
+
if (pyExists(gitignore))
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
pyWriteText(gitignore, BANTAMKIT_GITIGNORE_TEXT);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* best-effort: a missing ignore file is not worth a lost fact */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
46
106
|
/** `sorted(VALID_TYPES)` is what the error message interpolates, so the order is load-bearing. */
|
|
47
107
|
export const VALID_TYPES = ['feedback', 'project', 'reference', 'user'];
|
|
48
108
|
/**
|
|
@@ -535,6 +595,11 @@ export class MemoryStore {
|
|
|
535
595
|
* what lets one conformance case pin this line byte for byte.
|
|
536
596
|
*/
|
|
537
597
|
ensureDirs() {
|
|
598
|
+
// Checked BEFORE the mkdir below, per user ruling #2: this is the one moment that can
|
|
599
|
+
// tell whether `.bantamkit` already existed. After the mkdir it unconditionally exists
|
|
600
|
+
// either way, so the answer has to be captured now or not at all.
|
|
601
|
+
const bantamkitDir = pyParent(this.root);
|
|
602
|
+
const bantamkitDirExistedBefore = pyExists(bantamkitDir);
|
|
538
603
|
try {
|
|
539
604
|
pyMkdirParents(pyJoin(this.root, 'facts'));
|
|
540
605
|
pyMkdirParents(pyJoin(this.root, 'archive'));
|
|
@@ -545,6 +610,7 @@ export class MemoryStore {
|
|
|
545
610
|
throw new MemoryValidationError(`memory store could not be created: ${this.root}; the directory is not` +
|
|
546
611
|
' there and this filesystem would not make it, so nothing was written');
|
|
547
612
|
}
|
|
613
|
+
ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
|
|
548
614
|
}
|
|
549
615
|
// ---- ops ----
|
|
550
616
|
/**
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** The npm package this install would upgrade. Divergent by construction (`DISTRIBUTION`). */
|
|
2
|
+
export declare const PACKAGE = "bantamkit-mcp";
|
|
3
|
+
/**
|
|
4
|
+
* Where `--install` keeps the one-time install a host launches: `<homedir>/.bantamkit/mcp`.
|
|
5
|
+
*
|
|
6
|
+
* DEFINED ONCE, HERE, because two flags must agree on it byte for byte: `--install` makes it and
|
|
7
|
+
* records its `cli.js`, and `--update` from an `npx` cache patches it in place. A second spelling
|
|
8
|
+
* of this path would be a second place for the two to disagree about a user's install.
|
|
9
|
+
*
|
|
10
|
+
* `homedir()` and nothing else — `HOME` on POSIX, `USERPROFILE` on Windows — so pointing those at
|
|
11
|
+
* a scratch directory moves it, which is what makes the tests possible.
|
|
12
|
+
*/
|
|
13
|
+
export declare function keptPrefix(home?: string): string;
|
|
14
|
+
/** The `dist/cli.js` inside the kept install — what a host is told to run with node. */
|
|
15
|
+
export declare function keptCli(prefix?: string): string;
|
|
16
|
+
/** The `package.json` inside the kept install, whose `version` says whether it is current. */
|
|
17
|
+
export declare function keptManifest(prefix?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* `shlex.join`: `shlex.quote` each word and space them.
|
|
20
|
+
*
|
|
21
|
+
* Hand-rolled from CPython's own rule — `_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII)`
|
|
22
|
+
* — because the rendered command is printed to the operator by `UPDATING`, by `COMMAND_FAILED`
|
|
23
|
+
* and by the `local-file` route, and a prefix path with a space in it is the normal case on
|
|
24
|
+
* macOS and on Windows. Single quotes, with an embedded `'` closed and reopened the way
|
|
25
|
+
* `shlex.quote` does it, so the line can be pasted back into a shell unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shlexJoin(command: readonly string[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Run the installer, capture what it said, return both.
|
|
30
|
+
*
|
|
31
|
+
* CAPTURED RATHER THAN INHERITED so the report has one shape whether or not anybody is
|
|
32
|
+
* watching, and so a test can inject a substitute and assert on the command WITHOUT a real
|
|
33
|
+
* install ever running. On the `--install` path this also keeps npm's progress off stdout,
|
|
34
|
+
* which is the operator's report and nothing else.
|
|
35
|
+
*
|
|
36
|
+
* THE ONE PLACE THIS IS NOT THE REFERENCE'S BEHAVIOUR: `stderr=STDOUT` genuinely interleaves
|
|
37
|
+
* the two streams in the order they happened, and `spawnSync` has no fd-dup, so the two pipes
|
|
38
|
+
* are concatenated instead — stdout, then stderr. npm writes its progress to stderr and its
|
|
39
|
+
* result to stdout, so an operator reading a failure still gets both, in two blocks rather than
|
|
40
|
+
* one. The installer's own words are never compared across runtimes; the sentences around them
|
|
41
|
+
* are.
|
|
42
|
+
*
|
|
43
|
+
* `shell: true` ON WINDOWS ONLY, the idiom `hostinstall.installViaClaudeCli` established for
|
|
44
|
+
* the same reason: npm is a `npm.cmd` batch shim there and `spawnSync` returns ENOENT for a
|
|
45
|
+
* batch file without a shell. Arguments are quoted because `cmd.exe` gets a string.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runInstaller(command: readonly string[]): [number, string];
|
|
48
|
+
/**
|
|
49
|
+
* -1 when the index is ahead, 0 when they agree, 1 when the installed version is ahead.
|
|
50
|
+
*
|
|
51
|
+
* The shorter of the two is padded with numeric zeros, so `0.30` and `0.30.0` agree — which is
|
|
52
|
+
* what a person means by them and what both registries would print for one release.
|
|
53
|
+
*/
|
|
54
|
+
export declare function compareVersions(installed: string, latest: string): number;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The npm spawner and the kept-install location, shared by `--install` and `--update`.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS ITS OWN MODULE. `selfupdate.ts` owns the one network request in this runtime, and
|
|
5
|
+
* `test/selfupdate.test.mjs` pins that `cli.ts` is the ONLY module in `src/` importing it. Job51
|
|
6
|
+
* made `--install` spawn npm too (the kept install below), and the brief was to SHARE the spawner
|
|
7
|
+
* rather than write a second one. Moving it here lets `hostinstall.ts` use it without reaching
|
|
8
|
+
* `selfupdate.ts`; `selfupdate.ts` re-exports all three names, so its surface is unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here touches the network by itself. `runInstaller` spawns whatever argv it is handed,
|
|
11
|
+
* and only the two operator-typed flags hand it one.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
/** The npm package this install would upgrade. Divergent by construction (`DISTRIBUTION`). */
|
|
17
|
+
export const PACKAGE = 'bantamkit-mcp';
|
|
18
|
+
/**
|
|
19
|
+
* Where `--install` keeps the one-time install a host launches: `<homedir>/.bantamkit/mcp`.
|
|
20
|
+
*
|
|
21
|
+
* DEFINED ONCE, HERE, because two flags must agree on it byte for byte: `--install` makes it and
|
|
22
|
+
* records its `cli.js`, and `--update` from an `npx` cache patches it in place. A second spelling
|
|
23
|
+
* of this path would be a second place for the two to disagree about a user's install.
|
|
24
|
+
*
|
|
25
|
+
* `homedir()` and nothing else — `HOME` on POSIX, `USERPROFILE` on Windows — so pointing those at
|
|
26
|
+
* a scratch directory moves it, which is what makes the tests possible.
|
|
27
|
+
*/
|
|
28
|
+
export function keptPrefix(home = homedir()) {
|
|
29
|
+
return join(home, '.bantamkit', 'mcp');
|
|
30
|
+
}
|
|
31
|
+
/** The `dist/cli.js` inside the kept install — what a host is told to run with node. */
|
|
32
|
+
export function keptCli(prefix = keptPrefix()) {
|
|
33
|
+
return join(prefix, 'node_modules', PACKAGE, 'dist', 'cli.js');
|
|
34
|
+
}
|
|
35
|
+
/** The `package.json` inside the kept install, whose `version` says whether it is current. */
|
|
36
|
+
export function keptManifest(prefix = keptPrefix()) {
|
|
37
|
+
return join(prefix, 'node_modules', PACKAGE, 'package.json');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `shlex.join`: `shlex.quote` each word and space them.
|
|
41
|
+
*
|
|
42
|
+
* Hand-rolled from CPython's own rule — `_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII)`
|
|
43
|
+
* — because the rendered command is printed to the operator by `UPDATING`, by `COMMAND_FAILED`
|
|
44
|
+
* and by the `local-file` route, and a prefix path with a space in it is the normal case on
|
|
45
|
+
* macOS and on Windows. Single quotes, with an embedded `'` closed and reopened the way
|
|
46
|
+
* `shlex.quote` does it, so the line can be pasted back into a shell unchanged.
|
|
47
|
+
*/
|
|
48
|
+
export function shlexJoin(command) {
|
|
49
|
+
return command
|
|
50
|
+
.map((word) => {
|
|
51
|
+
if (word === '')
|
|
52
|
+
return "''";
|
|
53
|
+
if (/^[\w@%+=:,./-]+$/.test(word))
|
|
54
|
+
return word;
|
|
55
|
+
return `'${word.replace(/'/g, `'"'"'`)}'`;
|
|
56
|
+
})
|
|
57
|
+
.join(' ');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Run the installer, capture what it said, return both.
|
|
61
|
+
*
|
|
62
|
+
* CAPTURED RATHER THAN INHERITED so the report has one shape whether or not anybody is
|
|
63
|
+
* watching, and so a test can inject a substitute and assert on the command WITHOUT a real
|
|
64
|
+
* install ever running. On the `--install` path this also keeps npm's progress off stdout,
|
|
65
|
+
* which is the operator's report and nothing else.
|
|
66
|
+
*
|
|
67
|
+
* THE ONE PLACE THIS IS NOT THE REFERENCE'S BEHAVIOUR: `stderr=STDOUT` genuinely interleaves
|
|
68
|
+
* the two streams in the order they happened, and `spawnSync` has no fd-dup, so the two pipes
|
|
69
|
+
* are concatenated instead — stdout, then stderr. npm writes its progress to stderr and its
|
|
70
|
+
* result to stdout, so an operator reading a failure still gets both, in two blocks rather than
|
|
71
|
+
* one. The installer's own words are never compared across runtimes; the sentences around them
|
|
72
|
+
* are.
|
|
73
|
+
*
|
|
74
|
+
* `shell: true` ON WINDOWS ONLY, the idiom `hostinstall.installViaClaudeCli` established for
|
|
75
|
+
* the same reason: npm is a `npm.cmd` batch shim there and `spawnSync` returns ENOENT for a
|
|
76
|
+
* batch file without a shell. Arguments are quoted because `cmd.exe` gets a string.
|
|
77
|
+
*/
|
|
78
|
+
export function runInstaller(command) {
|
|
79
|
+
const [program, ...args] = command;
|
|
80
|
+
const win = process.platform === 'win32';
|
|
81
|
+
const quoted = win ? args.map((a) => (/[\s"^&|<>]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)) : args;
|
|
82
|
+
const done = spawnSync(program ?? '', quoted, { encoding: 'utf8', shell: win });
|
|
83
|
+
if (done.error) {
|
|
84
|
+
// A missing `npm` is not a stack trace: it is a command that failed, and `COMMAND_FAILED`
|
|
85
|
+
// is the sentence that says so and hands back what went wrong. 127 is the shell's own
|
|
86
|
+
// code for "command not found", so the number means something to the person reading it.
|
|
87
|
+
return [127, done.error.message];
|
|
88
|
+
}
|
|
89
|
+
return [done.status ?? 1, `${done.stdout ?? ''}${done.stderr ?? ''}`];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A dotted version as something orderable, deterministically, without claiming PEP 440.
|
|
93
|
+
*
|
|
94
|
+
* THE REFERENCE'S RULE, REPRODUCED EXACTLY, INCLUDING THE PART THAT IS WRONG. Split on `.`; a
|
|
95
|
+
* component that is all digits sorts as a NUMBER, anything else sorts as a STRING after every
|
|
96
|
+
* number in that position. So `0.9.0 < 0.10.0` — the thing a plain string compare gets wrong,
|
|
97
|
+
* and the reason this exists — and `0.31.0 < 0.31.0rc1`, which is WRONG BY SEMVER AND BY PEP
|
|
98
|
+
* 440 and is kept anyway.
|
|
99
|
+
*
|
|
100
|
+
* A CORRECT COMPARISON HERE WOULD BE A DIVERGENCE, NOT AN IMPROVEMENT. `_version_key`'s
|
|
101
|
+
* docstring in the reference gives the reason: bantamkit has never published a prerelease to
|
|
102
|
+
* either registry, and implementing PEP 440 there in order to reproduce it here would be a
|
|
103
|
+
* second, larger thing to keep byte-identical in service of a case neither index can currently
|
|
104
|
+
* return. What matters is that both runtimes are wrong in the SAME direction, which a
|
|
105
|
+
* conformance case can pin and a reader can check. If a prerelease is ever published, this is
|
|
106
|
+
* the function to fix — in both runtimes, in one job.
|
|
107
|
+
*
|
|
108
|
+
* `/^\d+$/` is ASCII-only, where CPython's `str.isdigit()` is not. That narrowing is the safe
|
|
109
|
+
* direction: the strings `str.isdigit()` accepts and `int()` then REFUSES (superscripts, for
|
|
110
|
+
* one) raise in the reference and sort as strings here, and no registry can answer with one.
|
|
111
|
+
*/
|
|
112
|
+
function versionKey(version) {
|
|
113
|
+
return version.split('.').map((part) => (/^\d+$/.test(part) ? [0, Number(part), ''] : [1, 0, part]));
|
|
114
|
+
}
|
|
115
|
+
const PAD = [0, 0, ''];
|
|
116
|
+
function comparePart(left, right) {
|
|
117
|
+
if (left[0] !== right[0])
|
|
118
|
+
return left[0] < right[0] ? -1 : 1;
|
|
119
|
+
if (left[1] !== right[1])
|
|
120
|
+
return left[1] < right[1] ? -1 : 1;
|
|
121
|
+
if (left[2] === right[2])
|
|
122
|
+
return 0;
|
|
123
|
+
return left[2] < right[2] ? -1 : 1;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* -1 when the index is ahead, 0 when they agree, 1 when the installed version is ahead.
|
|
127
|
+
*
|
|
128
|
+
* The shorter of the two is padded with numeric zeros, so `0.30` and `0.30.0` agree — which is
|
|
129
|
+
* what a person means by them and what both registries would print for one release.
|
|
130
|
+
*/
|
|
131
|
+
export function compareVersions(installed, latest) {
|
|
132
|
+
const left = versionKey(installed);
|
|
133
|
+
const right = versionKey(latest);
|
|
134
|
+
const width = Math.max(left.length, right.length);
|
|
135
|
+
for (let i = 0; i < width; i += 1) {
|
|
136
|
+
const order = comparePart(left[i] ?? PAD, right[i] ?? PAD);
|
|
137
|
+
if (order !== 0)
|
|
138
|
+
return order;
|
|
139
|
+
}
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
package/dist/selfupdate.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { compareVersions, PACKAGE, runInstaller, shlexJoin } from './npminstall.js';
|
|
2
|
+
export { compareVersions, PACKAGE, runInstaller, shlexJoin };
|
|
1
3
|
/**
|
|
2
4
|
* The command the operator typed, on BOTH runtimes. Not the package name — see the module
|
|
3
5
|
* docstring: PyPI has `bantamkit`, npm has `bantamkit-mcp`, and only this word is the same on
|
|
4
6
|
* both, so only this word can appear in a sentence the reference and this file share verbatim.
|
|
5
7
|
*/
|
|
6
8
|
export declare const PROGRAM = "bantamkit-mcp";
|
|
7
|
-
/** The npm package this install would upgrade. Divergent by construction (`DISTRIBUTION`). */
|
|
8
|
-
export declare const PACKAGE = "bantamkit-mcp";
|
|
9
9
|
/**
|
|
10
10
|
* The one URL this toolbox ever fetches. Divergent by construction (`INDEX_URL`).
|
|
11
11
|
*
|
|
@@ -98,6 +98,9 @@ export declare const NO_RECORDED_ROUTE = "There is no recorded update route for
|
|
|
98
98
|
* byte-identical config can be running builds resolved weeks apart. So this row is true of the
|
|
99
99
|
* cache that actually exists rather than of a fresh resolve that does not happen, and it names
|
|
100
100
|
* the only thing that changes it: the spec on the host's command line.
|
|
101
|
+
*
|
|
102
|
+
* Since J51-5 this row is reached only when there is NO kept install (`keptInstall`): with one,
|
|
103
|
+
* `runUpdate` updates that install instead, so the sentence stays true of what it describes.
|
|
101
104
|
*/
|
|
102
105
|
export declare const ROUTES: Record<string, string>;
|
|
103
106
|
/**
|
|
@@ -174,16 +177,6 @@ export declare function installEnvironment(runningFile?: string): Environment;
|
|
|
174
177
|
* suite asserts against by name, and a silent passthrough is how one ships.
|
|
175
178
|
*/
|
|
176
179
|
export declare function fill(template: string, values: Readonly<Record<string, string | number>>): string;
|
|
177
|
-
/**
|
|
178
|
-
* `shlex.join`: `shlex.quote` each word and space them.
|
|
179
|
-
*
|
|
180
|
-
* Hand-rolled from CPython's own rule — `_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII)`
|
|
181
|
-
* — because the rendered command is printed to the operator by `UPDATING`, by `COMMAND_FAILED`
|
|
182
|
-
* and by the `local-file` route, and a prefix path with a space in it is the normal case on
|
|
183
|
-
* macOS and on Windows. Single quotes, with an embedded `'` closed and reopened the way
|
|
184
|
-
* `shlex.quote` does it, so the line can be pasted back into a shell unchanged.
|
|
185
|
-
*/
|
|
186
|
-
export declare function shlexJoin(command: readonly string[]): string;
|
|
187
180
|
/**
|
|
188
181
|
* The version string out of the registry's JSON, or a named refusal. Pure — no network here.
|
|
189
182
|
*
|
|
@@ -234,32 +227,6 @@ export declare function fetchIndex(url?: string, timeout?: number): Promise<stri
|
|
|
234
227
|
* this code's.
|
|
235
228
|
*/
|
|
236
229
|
export declare function upgradeCommand(environment: Environment): string[];
|
|
237
|
-
/**
|
|
238
|
-
* Run the installer, capture what it said, return both. The only side effect in this module.
|
|
239
|
-
*
|
|
240
|
-
* CAPTURED RATHER THAN INHERITED so the report has one shape whether or not anybody is
|
|
241
|
-
* watching, and so a test can inject a substitute and assert on the command WITHOUT a real
|
|
242
|
-
* install ever running.
|
|
243
|
-
*
|
|
244
|
-
* THE ONE PLACE THIS IS NOT THE REFERENCE'S BEHAVIOUR: `stderr=STDOUT` genuinely interleaves
|
|
245
|
-
* the two streams in the order they happened, and `spawnSync` has no fd-dup, so the two pipes
|
|
246
|
-
* are concatenated instead — stdout, then stderr. npm writes its progress to stderr and its
|
|
247
|
-
* result to stdout, so an operator reading a failure still gets both, in two blocks rather than
|
|
248
|
-
* one. The installer's own words are never compared across runtimes; the sentences around them
|
|
249
|
-
* are.
|
|
250
|
-
*
|
|
251
|
-
* `shell: true` ON WINDOWS ONLY, the idiom `hostinstall.installViaClaudeCli` established for
|
|
252
|
-
* the same reason: npm is a `npm.cmd` batch shim there and `spawnSync` returns ENOENT for a
|
|
253
|
-
* batch file without a shell. Arguments are quoted because `cmd.exe` gets a string.
|
|
254
|
-
*/
|
|
255
|
-
export declare function runInstaller(command: readonly string[]): [number, string];
|
|
256
|
-
/**
|
|
257
|
-
* -1 when the index is ahead, 0 when they agree, 1 when the installed version is ahead.
|
|
258
|
-
*
|
|
259
|
-
* The shorter of the two is padded with numeric zeros, so `0.30` and `0.30.0` agree — which is
|
|
260
|
-
* what a person means by them and what both registries would print for one release.
|
|
261
|
-
*/
|
|
262
|
-
export declare function compareVersions(installed: string, latest: string): number;
|
|
263
230
|
/**
|
|
264
231
|
* `10` rather than `10.0`, because the sentence is read by a person, not parsed.
|
|
265
232
|
*
|
|
@@ -275,7 +242,41 @@ export interface UpdateOptions {
|
|
|
275
242
|
readonly url?: string;
|
|
276
243
|
readonly timeout?: number;
|
|
277
244
|
readonly environment?: Environment;
|
|
245
|
+
/**
|
|
246
|
+
* Which install is running, as `runUpdate` asks it. Default: `currentInstall()`, and nothing
|
|
247
|
+
* else in `src/` passes one. A seam so the `ephemeral` arms are reachable from a test running
|
|
248
|
+
* in a checkout; `update()` itself never reads it.
|
|
249
|
+
*/
|
|
250
|
+
readonly install?: () => {
|
|
251
|
+
readonly shape: string;
|
|
252
|
+
readonly source: string | null;
|
|
253
|
+
};
|
|
278
254
|
}
|
|
255
|
+
/** The kept install `--update` targets from an `npx` cache: its version and its `--prefix` tree. */
|
|
256
|
+
export interface KeptInstall {
|
|
257
|
+
readonly version: string;
|
|
258
|
+
readonly environment: Environment;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The kept install `--install` left at `keptPrefix()` (J51-4), or `null` when there is none to
|
|
262
|
+
* patch. A local read and nothing more — no network, no spawn.
|
|
263
|
+
*
|
|
264
|
+
* WHY `--update` LOOKS FOR IT. From an `npx` cache there is nothing to update in place, but the
|
|
265
|
+
* hosts do not launch the cache: `--install` recorded the kept install's `cli.js`. So an operator
|
|
266
|
+
* who types `npx -y bantamkit-mcp@latest --update` is asking about THAT install, and it is a
|
|
267
|
+
* `--prefix` tree `npm install --prefix` updates in place (measured: 0.32.1 to 0.33.0, exit 0).
|
|
268
|
+
*
|
|
269
|
+
* THERE IS ONE ONLY WHEN ALL OF THIS HOLDS, and anything short of it is `null` — which leaves
|
|
270
|
+
* today's `ephemeral` refusal exactly as it was:
|
|
271
|
+
*
|
|
272
|
+
* - the kept manifest (`keptManifest`, never respelled) is readable JSON with a non-blank
|
|
273
|
+
* string `version`;
|
|
274
|
+
* - `installEnvironment` on the kept `cli.js` says it is NOT the global tree. npm writes a
|
|
275
|
+
* `package.json` at a `--prefix` it installed into, so a real kept install always passes.
|
|
276
|
+
* One without it would render `npm install --global`, which updates some other install and
|
|
277
|
+
* reports success about this one — worse than refusing.
|
|
278
|
+
*/
|
|
279
|
+
export declare function keptInstall(prefix?: string): KeptInstall | null;
|
|
279
280
|
/**
|
|
280
281
|
* The whole flag: ask the index, compare, and act — or refuse, by name.
|
|
281
282
|
*
|
package/dist/selfupdate.js
CHANGED
|
@@ -40,19 +40,23 @@
|
|
|
40
40
|
* `test/selfupdate.test.mjs` — and it calls `runUpdate` from the flag and returns before a
|
|
41
41
|
* memory store or a transport exists, the same shape `--assets-root` and `--install` use.
|
|
42
42
|
*/
|
|
43
|
-
import { spawnSync } from 'node:child_process';
|
|
44
43
|
import { readFileSync } from 'node:fs';
|
|
45
44
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
46
45
|
import { fileURLToPath } from 'node:url';
|
|
47
46
|
import { currentInstall, Undetermined } from './mcp/identity.js';
|
|
47
|
+
import { compareVersions, keptCli, keptManifest, keptPrefix, PACKAGE, runInstaller, shlexJoin } from './npminstall.js';
|
|
48
|
+
// `PACKAGE`, `shlexJoin` and `runInstaller` moved to `npminstall.ts` in job51 so `--install` can
|
|
49
|
+
// share the npm spawner without importing this module (see that file's header). Re-exported so
|
|
50
|
+
// this module's surface — and every test importing these from `dist/selfupdate.js` — is unchanged.
|
|
51
|
+
// `compareVersions` followed in J51-9a, for the same reason: `--install` from an `npx` cache orders
|
|
52
|
+
// the kept install's version against its own, and must not import this module to do it.
|
|
53
|
+
export { compareVersions, PACKAGE, runInstaller, shlexJoin };
|
|
48
54
|
/**
|
|
49
55
|
* The command the operator typed, on BOTH runtimes. Not the package name — see the module
|
|
50
56
|
* docstring: PyPI has `bantamkit`, npm has `bantamkit-mcp`, and only this word is the same on
|
|
51
57
|
* both, so only this word can appear in a sentence the reference and this file share verbatim.
|
|
52
58
|
*/
|
|
53
59
|
export const PROGRAM = 'bantamkit-mcp';
|
|
54
|
-
/** The npm package this install would upgrade. Divergent by construction (`DISTRIBUTION`). */
|
|
55
|
-
export const PACKAGE = 'bantamkit-mcp';
|
|
56
60
|
/**
|
|
57
61
|
* The one URL this toolbox ever fetches. Divergent by construction (`INDEX_URL`).
|
|
58
62
|
*
|
|
@@ -164,6 +168,9 @@ export const NO_RECORDED_ROUTE = 'There is no recorded update route for a {shape
|
|
|
164
168
|
* byte-identical config can be running builds resolved weeks apart. So this row is true of the
|
|
165
169
|
* cache that actually exists rather than of a fresh resolve that does not happen, and it names
|
|
166
170
|
* the only thing that changes it: the spec on the host's command line.
|
|
171
|
+
*
|
|
172
|
+
* Since J51-5 this row is reached only when there is NO kept install (`keptInstall`): with one,
|
|
173
|
+
* `runUpdate` updates that install instead, so the sentence stays true of what it describes.
|
|
167
174
|
*/
|
|
168
175
|
export const ROUTES = {
|
|
169
176
|
'local-file': 'It was installed from the file {source}, which is not the package index: reinstall it ' +
|
|
@@ -234,26 +241,6 @@ export function fill(template, values) {
|
|
|
234
241
|
return String(values[key]);
|
|
235
242
|
});
|
|
236
243
|
}
|
|
237
|
-
/**
|
|
238
|
-
* `shlex.join`: `shlex.quote` each word and space them.
|
|
239
|
-
*
|
|
240
|
-
* Hand-rolled from CPython's own rule — `_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII)`
|
|
241
|
-
* — because the rendered command is printed to the operator by `UPDATING`, by `COMMAND_FAILED`
|
|
242
|
-
* and by the `local-file` route, and a prefix path with a space in it is the normal case on
|
|
243
|
-
* macOS and on Windows. Single quotes, with an embedded `'` closed and reopened the way
|
|
244
|
-
* `shlex.quote` does it, so the line can be pasted back into a shell unchanged.
|
|
245
|
-
*/
|
|
246
|
-
export function shlexJoin(command) {
|
|
247
|
-
return command
|
|
248
|
-
.map((word) => {
|
|
249
|
-
if (word === '')
|
|
250
|
-
return "''";
|
|
251
|
-
if (/^[\w@%+=:,./-]+$/.test(word))
|
|
252
|
-
return word;
|
|
253
|
-
return `'${word.replace(/'/g, `'"'"'`)}'`;
|
|
254
|
-
})
|
|
255
|
-
.join(' ');
|
|
256
|
-
}
|
|
257
244
|
/**
|
|
258
245
|
* The version string out of the registry's JSON, or a named refusal. Pure — no network here.
|
|
259
246
|
*
|
|
@@ -358,88 +345,6 @@ export function upgradeCommand(environment) {
|
|
|
358
345
|
return ['npm', 'install', '--global', `${PACKAGE}@latest`];
|
|
359
346
|
return ['npm', 'install', '--prefix', environment.root, `${PACKAGE}@latest`];
|
|
360
347
|
}
|
|
361
|
-
/**
|
|
362
|
-
* Run the installer, capture what it said, return both. The only side effect in this module.
|
|
363
|
-
*
|
|
364
|
-
* CAPTURED RATHER THAN INHERITED so the report has one shape whether or not anybody is
|
|
365
|
-
* watching, and so a test can inject a substitute and assert on the command WITHOUT a real
|
|
366
|
-
* install ever running.
|
|
367
|
-
*
|
|
368
|
-
* THE ONE PLACE THIS IS NOT THE REFERENCE'S BEHAVIOUR: `stderr=STDOUT` genuinely interleaves
|
|
369
|
-
* the two streams in the order they happened, and `spawnSync` has no fd-dup, so the two pipes
|
|
370
|
-
* are concatenated instead — stdout, then stderr. npm writes its progress to stderr and its
|
|
371
|
-
* result to stdout, so an operator reading a failure still gets both, in two blocks rather than
|
|
372
|
-
* one. The installer's own words are never compared across runtimes; the sentences around them
|
|
373
|
-
* are.
|
|
374
|
-
*
|
|
375
|
-
* `shell: true` ON WINDOWS ONLY, the idiom `hostinstall.installViaClaudeCli` established for
|
|
376
|
-
* the same reason: npm is a `npm.cmd` batch shim there and `spawnSync` returns ENOENT for a
|
|
377
|
-
* batch file without a shell. Arguments are quoted because `cmd.exe` gets a string.
|
|
378
|
-
*/
|
|
379
|
-
export function runInstaller(command) {
|
|
380
|
-
const [program, ...args] = command;
|
|
381
|
-
const win = process.platform === 'win32';
|
|
382
|
-
const quoted = win ? args.map((a) => (/[\s"^&|<>]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)) : args;
|
|
383
|
-
const done = spawnSync(program ?? '', quoted, { encoding: 'utf8', shell: win });
|
|
384
|
-
if (done.error) {
|
|
385
|
-
// A missing `npm` is not a stack trace: it is a command that failed, and `COMMAND_FAILED`
|
|
386
|
-
// is the sentence that says so and hands back what went wrong. 127 is the shell's own
|
|
387
|
-
// code for "command not found", so the number means something to the person reading it.
|
|
388
|
-
return [127, done.error.message];
|
|
389
|
-
}
|
|
390
|
-
return [done.status ?? 1, `${done.stdout ?? ''}${done.stderr ?? ''}`];
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* A dotted version as something orderable, deterministically, without claiming PEP 440.
|
|
394
|
-
*
|
|
395
|
-
* THE REFERENCE'S RULE, REPRODUCED EXACTLY, INCLUDING THE PART THAT IS WRONG. Split on `.`; a
|
|
396
|
-
* component that is all digits sorts as a NUMBER, anything else sorts as a STRING after every
|
|
397
|
-
* number in that position. So `0.9.0 < 0.10.0` — the thing a plain string compare gets wrong,
|
|
398
|
-
* and the reason this exists — and `0.31.0 < 0.31.0rc1`, which is WRONG BY SEMVER AND BY PEP
|
|
399
|
-
* 440 and is kept anyway.
|
|
400
|
-
*
|
|
401
|
-
* A CORRECT COMPARISON HERE WOULD BE A DIVERGENCE, NOT AN IMPROVEMENT. `_version_key`'s
|
|
402
|
-
* docstring in the reference gives the reason: bantamkit has never published a prerelease to
|
|
403
|
-
* either registry, and implementing PEP 440 there in order to reproduce it here would be a
|
|
404
|
-
* second, larger thing to keep byte-identical in service of a case neither index can currently
|
|
405
|
-
* return. What matters is that both runtimes are wrong in the SAME direction, which a
|
|
406
|
-
* conformance case can pin and a reader can check. If a prerelease is ever published, this is
|
|
407
|
-
* the function to fix — in both runtimes, in one job.
|
|
408
|
-
*
|
|
409
|
-
* `/^\d+$/` is ASCII-only, where CPython's `str.isdigit()` is not. That narrowing is the safe
|
|
410
|
-
* direction: the strings `str.isdigit()` accepts and `int()` then REFUSES (superscripts, for
|
|
411
|
-
* one) raise in the reference and sort as strings here, and no registry can answer with one.
|
|
412
|
-
*/
|
|
413
|
-
function versionKey(version) {
|
|
414
|
-
return version.split('.').map((part) => (/^\d+$/.test(part) ? [0, Number(part), ''] : [1, 0, part]));
|
|
415
|
-
}
|
|
416
|
-
const PAD = [0, 0, ''];
|
|
417
|
-
function comparePart(left, right) {
|
|
418
|
-
if (left[0] !== right[0])
|
|
419
|
-
return left[0] < right[0] ? -1 : 1;
|
|
420
|
-
if (left[1] !== right[1])
|
|
421
|
-
return left[1] < right[1] ? -1 : 1;
|
|
422
|
-
if (left[2] === right[2])
|
|
423
|
-
return 0;
|
|
424
|
-
return left[2] < right[2] ? -1 : 1;
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* -1 when the index is ahead, 0 when they agree, 1 when the installed version is ahead.
|
|
428
|
-
*
|
|
429
|
-
* The shorter of the two is padded with numeric zeros, so `0.30` and `0.30.0` agree — which is
|
|
430
|
-
* what a person means by them and what both registries would print for one release.
|
|
431
|
-
*/
|
|
432
|
-
export function compareVersions(installed, latest) {
|
|
433
|
-
const left = versionKey(installed);
|
|
434
|
-
const right = versionKey(latest);
|
|
435
|
-
const width = Math.max(left.length, right.length);
|
|
436
|
-
for (let i = 0; i < width; i += 1) {
|
|
437
|
-
const order = comparePart(left[i] ?? PAD, right[i] ?? PAD);
|
|
438
|
-
if (order !== 0)
|
|
439
|
-
return order;
|
|
440
|
-
}
|
|
441
|
-
return 0;
|
|
442
|
-
}
|
|
443
348
|
/**
|
|
444
349
|
* `10` rather than `10.0`, because the sentence is read by a person, not parsed.
|
|
445
350
|
*
|
|
@@ -451,6 +356,40 @@ export function compareVersions(installed, latest) {
|
|
|
451
356
|
export function seconds(timeout) {
|
|
452
357
|
return String(timeout);
|
|
453
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* The kept install `--install` left at `keptPrefix()` (J51-4), or `null` when there is none to
|
|
361
|
+
* patch. A local read and nothing more — no network, no spawn.
|
|
362
|
+
*
|
|
363
|
+
* WHY `--update` LOOKS FOR IT. From an `npx` cache there is nothing to update in place, but the
|
|
364
|
+
* hosts do not launch the cache: `--install` recorded the kept install's `cli.js`. So an operator
|
|
365
|
+
* who types `npx -y bantamkit-mcp@latest --update` is asking about THAT install, and it is a
|
|
366
|
+
* `--prefix` tree `npm install --prefix` updates in place (measured: 0.32.1 to 0.33.0, exit 0).
|
|
367
|
+
*
|
|
368
|
+
* THERE IS ONE ONLY WHEN ALL OF THIS HOLDS, and anything short of it is `null` — which leaves
|
|
369
|
+
* today's `ephemeral` refusal exactly as it was:
|
|
370
|
+
*
|
|
371
|
+
* - the kept manifest (`keptManifest`, never respelled) is readable JSON with a non-blank
|
|
372
|
+
* string `version`;
|
|
373
|
+
* - `installEnvironment` on the kept `cli.js` says it is NOT the global tree. npm writes a
|
|
374
|
+
* `package.json` at a `--prefix` it installed into, so a real kept install always passes.
|
|
375
|
+
* One without it would render `npm install --global`, which updates some other install and
|
|
376
|
+
* reports success about this one — worse than refusing.
|
|
377
|
+
*/
|
|
378
|
+
export function keptInstall(prefix = keptPrefix()) {
|
|
379
|
+
let version;
|
|
380
|
+
try {
|
|
381
|
+
version = JSON.parse(readFileSync(keptManifest(prefix), 'utf8')).version;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
if (typeof version !== 'string' || version.trim() === '')
|
|
387
|
+
return null;
|
|
388
|
+
const environment = installEnvironment(keptCli(prefix));
|
|
389
|
+
if (environment.global)
|
|
390
|
+
return null;
|
|
391
|
+
return { version, environment };
|
|
392
|
+
}
|
|
454
393
|
/**
|
|
455
394
|
* The whole flag: ask the index, compare, and act — or refuse, by name.
|
|
456
395
|
*
|
|
@@ -566,7 +505,7 @@ export async function runUpdate(out, err, installed, packageDirectory, options =
|
|
|
566
505
|
let shape;
|
|
567
506
|
let recorded;
|
|
568
507
|
try {
|
|
569
|
-
const install = currentInstall();
|
|
508
|
+
const install = (options.install ?? currentInstall)();
|
|
570
509
|
shape = install.shape;
|
|
571
510
|
recorded = install.source;
|
|
572
511
|
}
|
|
@@ -580,9 +519,24 @@ export async function runUpdate(out, err, installed, packageDirectory, options =
|
|
|
580
519
|
// path could not be read — `registry`, where the route is this flag itself, and `checkout`,
|
|
581
520
|
// whose tree IS the thing to update. The running package directory is not a guess: it is
|
|
582
521
|
// where the code being executed lives, and it is the tree the route says to `git pull`.
|
|
583
|
-
|
|
522
|
+
let origin = { shape, source: recorded ?? packageDirectory };
|
|
523
|
+
let current = installed;
|
|
524
|
+
let chosen = options;
|
|
525
|
+
// J51-5: an `npx` cache WITH a kept install updates the kept install — its version is the one
|
|
526
|
+
// compared and printed, and its prefix is where npm points — through exactly the registry
|
|
527
|
+
// route's lines. Decided HERE and not in `update()`, so `update()` stays a function of its
|
|
528
|
+
// arguments and never of whatever `~/.bantamkit/mcp` the machine running it happens to hold.
|
|
529
|
+
// No kept install: nothing below changes and the `ephemeral` refusal is today's.
|
|
530
|
+
if (shape === 'ephemeral') {
|
|
531
|
+
const kept = keptInstall();
|
|
532
|
+
if (kept !== null) {
|
|
533
|
+
origin = { shape: 'registry', source: kept.environment.root };
|
|
534
|
+
current = kept.version;
|
|
535
|
+
chosen = { ...options, environment: kept.environment };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
584
538
|
try {
|
|
585
|
-
out(`${await update(
|
|
539
|
+
out(`${await update(current, origin, chosen)}\n`);
|
|
586
540
|
}
|
|
587
541
|
catch (e) {
|
|
588
542
|
if (!(e instanceof UpdateRefused))
|