bantamkit-mcp 0.32.1 → 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 +196 -23
- package/assets/schemas/shiftwork-checkpoint.json +5 -1
- package/assets/tools/bantamkit_read.json +1 -3
- package/assets/tools/repo_map.json +1 -3
- package/assets/tools/shiftwork_clock_in.json +1 -1
- package/assets/tools/shiftwork_clock_out.json +38 -3
- package/assets/tools/skill_audit.json +1 -1
- package/assets/tools/token_ledger.json +1 -1
- package/dist/cli.js +5 -4
- package/dist/eventlog.js +29 -5
- 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/mcp/pyargs.js +5 -0
- package/dist/mcp/server.d.ts +20 -8
- package/dist/mcp/server.js +47 -14
- package/dist/memory/cli.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/dist/shiftwork.d.ts +19 -2
- package/dist/shiftwork.js +268 -10
- package/package.json +1 -1
|
@@ -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))
|
package/dist/shiftwork.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { type PyValue } from './pyjson.js';
|
|
2
2
|
export declare const SCHEMA_NAME = "shiftwork-checkpoint";
|
|
3
|
+
/**
|
|
4
|
+
* J50-9A/9B: the one sentence for a pack that cannot supply the accounting shape. FIXED —
|
|
5
|
+
* no path, no exception text, no unit — read out of `ACCOUNTING_SHAPE_UNREADABLE` in the
|
|
6
|
+
* Python module byte for byte, so a differential case can compare the two; which of the
|
|
7
|
+
* shapes failed is not said, because the property is the same for all of them (see
|
|
8
|
+
* `accountingSchema`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const ACCOUNTING_SHAPE_UNREADABLE: string;
|
|
3
11
|
/** The driver's SUCCESS test. */
|
|
4
12
|
export declare const TERMINAL_UNIT_STATUS: ReadonlySet<string>;
|
|
5
13
|
/** The schema's `maxItems` — older entries fall off the ring. */
|
|
6
14
|
export declare const HISTORY_RING_SIZE = 5;
|
|
15
|
+
/**
|
|
16
|
+
* F6: the `event` value of the ledger line `clockIn` appends when it issues a brief. The word
|
|
17
|
+
* is the register's own — `clock_in` answers `result: "brief"` — so the ledger names the
|
|
18
|
+
* thing it recorded with the same word the wire used. `BRIEF_EVENT` in `shiftwork.py`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const BRIEF_EVENT = "brief";
|
|
7
21
|
/**
|
|
8
22
|
* `time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now))` — UTC, SECOND precision.
|
|
9
23
|
*
|
|
@@ -30,7 +44,7 @@ export interface ClockOptions {
|
|
|
30
44
|
* all-terminal test before the cursor lookup — so an empty plan reports success rather than
|
|
31
45
|
* escalating on its dangling cursor, because `all([])` is true.
|
|
32
46
|
*/
|
|
33
|
-
export declare function clockIn(checkpoint: string): PyValue;
|
|
47
|
+
export declare function clockIn(checkpoint: string, options?: ClockOptions): PyValue;
|
|
34
48
|
/**
|
|
35
49
|
* Apply the cursor unit's result, validate the WHOLE mutated document, write atomically.
|
|
36
50
|
*
|
|
@@ -40,7 +54,10 @@ export declare function clockIn(checkpoint: string): PyValue;
|
|
|
40
54
|
* any mutation and before the accounting line. Extended 2026-09-11 (job47): so is a
|
|
41
55
|
* `job.roles` value for that role that is not a list of model identifiers — an unreadable
|
|
42
56
|
* declaration allows no model, and it is refused in the same place, by the same structured
|
|
43
|
-
* return, writing nothing.
|
|
57
|
+
* return, writing nothing. Extended 2026-09-13 (job50/F5): and so is an accounting line
|
|
58
|
+
* that does not fit the shape the `shiftwork_clock_out` asset declares — checked after the
|
|
59
|
+
* roles gate, before any mutation, same structured return, nothing written; a null
|
|
60
|
+
* accounting is not validated and stays legal. Mutations: set the unit's status, advance
|
|
44
61
|
* `plan.cursor` to the first non-terminal unit in PLAN order (`depends_on` is ignored),
|
|
45
62
|
* shallow-merge `handoffPatch` into `handoff`, push `historyEntry` onto the 5-entry ring.
|
|
46
63
|
*
|