bantamkit-mcp 0.33.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  *
@@ -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
- const origin = { shape, source: recorded ?? packageDirectory };
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(installed, origin, options)}\n`);
539
+ out(`${await update(current, origin, chosen)}\n`);
586
540
  }
587
541
  catch (e) {
588
542
  if (!(e instanceof UpdateRefused))
package/package.json CHANGED
@@ -1,7 +1,26 @@
1
1
  {
2
2
  "name": "bantamkit-mcp",
3
- "version": "0.33.0",
4
- "description": "bantamkit MCP server — pure Node, runnable with npx",
3
+ "version": "0.34.1",
4
+ "description": "Memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop: per-person memory, JSON Schema validation, shift-work tools. Pure Node; install once with npx, run offline.",
5
+ "keywords": [
6
+ "mcp",
7
+ "mcp-server",
8
+ "model-context-protocol",
9
+ "claude",
10
+ "claude-code",
11
+ "claude-desktop",
12
+ "cursor",
13
+ "copilot",
14
+ "vscode",
15
+ "agent",
16
+ "memory",
17
+ "json-schema",
18
+ "llm"
19
+ ],
20
+ "homepage": "https://github.com/Ink01101011/bantamkit#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/Ink01101011/bantamkit/issues"
23
+ },
5
24
  "type": "module",
6
25
  "bin": {
7
26
  "bantamkit-mcp": "dist/cli.js",