bantamkit-mcp 0.32.0 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -80,7 +80,7 @@ import { packFileCount } from './mcp/identity.js';
80
80
  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
- import { pyRepr } from './memory/pyfs.js';
83
+ import { pyJoin, PyOSError, pyRepr, pyStatIsDir } from './memory/pyfs.js';
84
84
  import { HOSTS, install as installHost, InstallError, thisCommand } from './hostinstall.js';
85
85
  import { runUpdate } from './selfupdate.js';
86
86
  import { statusLine } from './statusline.js';
@@ -330,10 +330,86 @@ function buildMemory(options) {
330
330
  if (options.store !== null) {
331
331
  if (options.store === '')
332
332
  throw new Refusal('--store requires a non-empty path');
333
+ checkStoreFlag(options.store);
333
334
  return new Memory(options.store, shared);
334
335
  }
335
336
  return Memory.layered(options.start, shared);
336
337
  }
338
+ /**
339
+ * `--store` may name a store that is missing, never one that is not a store.
340
+ *
341
+ * THIS CHECK IS RESTORED, NOT INVENTED, and the distinction is the point. Until the project
342
+ * layer was built lazily there WAS a `--store` validation, and it was entirely accidental: the
343
+ * constructor ran `pyMkdirParents`, so a `--store` pointing at a regular file or at a path
344
+ * under a directory that does not exist took the process down with a `PyOSError` stack out of
345
+ * `pyfs`. The reference's `test_statusline.py::
346
+ * test_the_flag_returns_before_anything_a_server_would_touch` depends on it — it arms
347
+ * `--store <a regular file>` as a trap and shows `--statusline` walking past it — and a lazy
348
+ * layer disarms the trap by making that same argv exit 0. The honest way to keep that test
349
+ * true is to mean the refusal on purpose.
350
+ *
351
+ * IT IS `layers.pinnedStore`'s CHECK MINUS ONE ARM, and the missing arm is deliberate and
352
+ * measured. The pin refuses a path that is not there ("nothing was created"); this flag does
353
+ * NOT, and must not, for two reasons that are both already pinned elsewhere in this
354
+ * repository:
355
+ *
356
+ * - The sibling CLI's identical flag is contracted to CREATE one. `tools/conformance/suites/
357
+ * memorycli.mjs` carries `status-creates-a-missing-store`, `--store {BED}/nowhere` over an
358
+ * empty bed, whose whole job is to say the two runtimes create the same two directories
359
+ * there. Refusing a missing `--store` here would put two flags of the same name, in two
360
+ * programs of the same product, in direct contradiction.
361
+ * - "Missing" is no longer a broken state anywhere in this program. The walk designates a
362
+ * project store without creating it, and the first save brings it into existence or refuses
363
+ * by name (`store.ensureDirs`). A `--store` at a path that is not there is that same
364
+ * designated state, reached by being told instead of by searching. MEASURED on the reference
365
+ * side: requiring existence reddened 14 tests that have nothing to do with this defect —
366
+ * `test_tool_manifest.py` ×7, `test_mcpserver.py` ×6, `test_mcp_endpoint.py` — every one of
367
+ * them a fixture naming a store under `tmp_path` it never made, because that is what this
368
+ * flag has always meant.
369
+ *
370
+ * What is left is the arm the accident actually covered and the only one it covered: a
371
+ * `--store` that names something which EXISTS and is NOT A DIRECTORY. That is not a store and
372
+ * never becomes one — `mkdir` under it is ENOTDIR on both runtimes and on every platform, the
373
+ * one row of the errno table where they already agree — so it is refused here, by name, before
374
+ * a transport exists.
375
+ *
376
+ * `FileNotFoundError` and not an errno comparison: the absent case is selected by the exception
377
+ * CPython raises for it, and `pyfs`' `PyOSError` carries that class as its `name`. Every other
378
+ * `stat` failure — a permission wall on the parent, a symlink loop — is a real fault about a
379
+ * path the operator named, and it keeps the pin's sentence.
380
+ *
381
+ * `pyStatIsDir` rather than an `existsSync`/`isDirectory` pair, and that too is `pinnedStore`'s
382
+ * reasoning: a predicate that swallows `PermissionError` and answers false would report an
383
+ * operator's real store as a typo. A single named path gets the accurate reason.
384
+ *
385
+ * `Refusal` and not `MemoryValidationError`, so that both halves of this flag's refusal family
386
+ * read the same way on the terminal: `--store requires a non-empty path` is already bare on
387
+ * stderr at exit 1, and the reference spells both as `SystemExit`.
388
+ */
389
+ function checkStoreFlag(raw) {
390
+ // NOT `pyExpanduser`, unlike the pin. `new Memory(store, ...)` builds the root from this
391
+ // string verbatim, so expanding here would check one path and serve another: `--store ~/x`
392
+ // left unexpanded by the shell would pass a check against the home directory and then build
393
+ // a store in a directory literally named `~`. The check must stat the path the store is
394
+ // going to be. `pyJoin` of the one argument is `Path(raw)`: the same normalisation
395
+ // `MemoryStore` applies to its root, so the path this stats and the path it names in a
396
+ // refusal are the path the store would have used.
397
+ const store = pyJoin(raw);
398
+ let isDir;
399
+ try {
400
+ isDir = pyStatIsDir(store);
401
+ }
402
+ catch (e) {
403
+ if (!(e instanceof PyOSError))
404
+ throw e;
405
+ // designated, not broken: the first save makes it or refuses by name
406
+ if (e.name === 'FileNotFoundError')
407
+ return;
408
+ throw new Refusal(`--store is unreachable: ${store}: ${e.strerror}; nothing was created`);
409
+ }
410
+ if (!isDir)
411
+ throw new Refusal(`--store is not a directory: ${store}`);
412
+ }
337
413
  /**
338
414
  * The version this build declares — the same bytes npm packs the tarball from.
339
415
  *
@@ -128,10 +128,14 @@ export declare class Memory {
128
128
  private readonly layers;
129
129
  private showLayers;
130
130
  /**
131
- * How this store came to be the store, captured once. It cannot be re-derived later: the
132
- * constructor above has already made the directory, so a store that was only DESIGNATED a
133
- * moment ago is indistinguishable on disk from one that was found empty. `null` means the
134
- * caller named the path outright and no resolution happened to report.
131
+ * How this store came to be the store, captured once. It is still captured here and not
132
+ * re-derived later, but the reason has changed with the constructor below and the old one is
133
+ * recorded rather than deleted: it WAS that the constructor had already made the directory,
134
+ * so a store that was only DESIGNATED a moment ago was indistinguishable on disk from one
135
+ * that was found empty. That complaint is now discharged — nothing is created — and the
136
+ * binding is kept because it also carries WHY this path (`origin`, `searchedFrom`), which no
137
+ * amount of looking at the disk recovers. `null` means the caller named the path outright
138
+ * and no resolution happened to report.
135
139
  */
136
140
  private readonly binding;
137
141
  constructor(store: string, options?: MemoryOptions, binding?: StoreBinding | null);
@@ -140,8 +144,15 @@ export declare class Memory {
140
144
  *
141
145
  * `resolveProjectStore` rather than `discoverProjectStore`: the path both return is the
142
146
  * same path, but only the binding carries WHY it is that path and whether it holds
143
- * anything — and `recall` cannot recover either fact afterwards, because constructing the
144
- * store creates the directory.
147
+ * anything — and `recall` cannot recover either fact afterwards.
148
+ *
149
+ * AMENDMENT 2026-09-12 (job48, J48-2), correcting the reason the clause above used to give
150
+ * and not the clause itself. WAS: "because constructing the store creates the directory."
151
+ * NOW that is false — the project layer is built `create: false` (see the constructor) and
152
+ * constructing it creates nothing. The binding is still the only carrier of WHY, because
153
+ * `origin` and `searchedFrom` were never on the disk to begin with; what the disk has
154
+ * stopped being able to answer is merely "was this empty or brand new", which it now answers
155
+ * correctly for the first time.
145
156
  */
146
157
  static layered(start?: string | null, options?: MemoryOptions): Memory;
147
158
  /** The labels, in order. `_layers` is private in the reference too; this is for the tests. */
@@ -280,6 +291,12 @@ export declare class Memory {
280
291
  * at construction and has been saved to since really can answer. The DIAGNOSIS is keyed on
281
292
  * the binding, because "designated" stops being visible on disk the moment `MemoryStore`
282
293
  * creates the directory.
294
+ *
295
+ * AMENDMENT 2026-09-12 (job48, J48-2). The reason in the clause above no longer holds and
296
+ * the keying does: the project layer is built `create: false`, so a designated store IS now
297
+ * distinguishable on disk — it is absent. The DIAGNOSIS still keys on the binding because
298
+ * only the binding carries `origin` and `searchedFrom`, which the disk never held, and
299
+ * because an absent directory and an emptied one must not be told apart by a race.
283
300
  */
284
301
  private nothingToReport;
285
302
  /**
@@ -123,17 +123,45 @@ export class Memory {
123
123
  layers;
124
124
  showLayers = false;
125
125
  /**
126
- * How this store came to be the store, captured once. It cannot be re-derived later: the
127
- * constructor above has already made the directory, so a store that was only DESIGNATED a
128
- * moment ago is indistinguishable on disk from one that was found empty. `null` means the
129
- * caller named the path outright and no resolution happened to report.
126
+ * How this store came to be the store, captured once. It is still captured here and not
127
+ * re-derived later, but the reason has changed with the constructor below and the old one is
128
+ * recorded rather than deleted: it WAS that the constructor had already made the directory,
129
+ * so a store that was only DESIGNATED a moment ago was indistinguishable on disk from one
130
+ * that was found empty. That complaint is now discharged — nothing is created — and the
131
+ * binding is kept because it also carries WHY this path (`origin`, `searchedFrom`), which no
132
+ * amount of looking at the disk recovers. `null` means the caller named the path outright
133
+ * and no resolution happened to report.
130
134
  */
131
135
  binding;
132
136
  constructor(store, options = {}, binding = null) {
133
137
  const k = options.k ?? 3;
138
+ // `create: false`, AND IT IS THE WHOLE OF THE cwd-`/` FIX. This was the last eager layer —
139
+ // grants and the profile store have always been lazy — so a cwd that no directory can be
140
+ // created under was a STARTUP CRASH: every GUI MCP host launches its child with cwd `/`,
141
+ // measured live on this machine (`lsof -p <pid> -a -d cwd` on Claude Desktop and all four
142
+ // of its `bantamkit-mcp` children returns `/`), and the client saw only CONNECTION_CLOSED.
143
+ // Refusing at startup with a sentence was the other candidate and it is REFUTED by
144
+ // measurement: it leaves those same four processes dead, which is strictly worse than
145
+ // today. Built lazily, the server starts from `/`, binds `['project', 'profile']`, and
146
+ // answers out of the profile layer. The sentence still exists — it is what
147
+ // `MemoryStore.save`/`compact` say when a write actually needs the directory
148
+ // (`store.ensureDirs`).
149
+ //
150
+ // It is legal because every READ is already defined over an absent directory:
151
+ // `recall() -> []`, `archived() -> []`, `indexText() -> ''`. `facts()`' docstring says so
152
+ // outright, and `create: false` is one of the three callers it names.
153
+ //
154
+ // THE PRICE, stated rather than discovered: a bare server start in a perfectly good cwd no
155
+ // longer scatters `.bantamkit/memory/{facts,archive}` there before anything is saved. That
156
+ // moves TOWARD a stance this file already holds — see the binding comment above, and
157
+ // `cli.ts`'s bare-TTY guard.
158
+ //
159
+ // The reference is `component.py`'s `Memory.__init__` (job48, J48-1); this is the same
160
+ // change in the same place, and the two sentences below are spelled byte for byte.
134
161
  this.store = new MemoryStore(store, {
135
162
  indexBudget: options.indexBudget ?? DEFAULT_INDEX_BUDGET,
136
163
  k,
164
+ create: false,
137
165
  ...(options.today ? { today: options.today } : {}),
138
166
  });
139
167
  this.k = k;
@@ -145,8 +173,15 @@ export class Memory {
145
173
  *
146
174
  * `resolveProjectStore` rather than `discoverProjectStore`: the path both return is the
147
175
  * same path, but only the binding carries WHY it is that path and whether it holds
148
- * anything — and `recall` cannot recover either fact afterwards, because constructing the
149
- * store creates the directory.
176
+ * anything — and `recall` cannot recover either fact afterwards.
177
+ *
178
+ * AMENDMENT 2026-09-12 (job48, J48-2), correcting the reason the clause above used to give
179
+ * and not the clause itself. WAS: "because constructing the store creates the directory."
180
+ * NOW that is false — the project layer is built `create: false` (see the constructor) and
181
+ * constructing it creates nothing. The binding is still the only carrier of WHY, because
182
+ * `origin` and `searchedFrom` were never on the disk to begin with; what the disk has
183
+ * stopped being able to answer is merely "was this empty or brand new", which it now answers
184
+ * correctly for the first time.
150
185
  */
151
186
  static layered(start, options = {}) {
152
187
  const binding = resolveProjectStore(start);
@@ -566,6 +601,12 @@ export class Memory {
566
601
  * at construction and has been saved to since really can answer. The DIAGNOSIS is keyed on
567
602
  * the binding, because "designated" stops being visible on disk the moment `MemoryStore`
568
603
  * creates the directory.
604
+ *
605
+ * AMENDMENT 2026-09-12 (job48, J48-2). The reason in the clause above no longer holds and
606
+ * the keying does: the project layer is built `create: false`, so a designated store IS now
607
+ * distinguishable on disk — it is absent. The DIAGNOSIS still keys on the binding because
608
+ * only the binding carries `origin` and `searchedFrom`, which the disk never held, and
609
+ * because an absent directory and an emptied one must not be told apart by a race.
569
610
  */
570
611
  nothingToReport() {
571
612
  let status;
@@ -670,9 +711,19 @@ export class Memory {
670
711
  where = `The project store ${root} is empty.`;
671
712
  }
672
713
  else if (binding.state === 'designated') {
714
+ // AMENDED 2026-09-12 (job48, J48-2) because the sentence became FALSE, not because it
715
+ // read badly. WAS: "No memory store existed at or above <from>, so the empty <root> was
716
+ // created for this session." Under a lazily-built project layer nothing is created — the
717
+ // path is designated and stays absent until a save needs it — so the old sentence told
718
+ // an operator to go look for a directory that is not there. The new one borrows the
719
+ // pin's own closing clause, "nothing was created", because it is the same claim about
720
+ // the same thing, and it reuses the binding's existing `designated` vocabulary rather
721
+ // than minting a word for it. The remedy that follows — "otherwise save a memory to
722
+ // start this one" — is now literally true: the save is what brings the directory into
723
+ // existence. Byte for byte the reference's, `component.py`'s `_binding_diagnosis`.
673
724
  where =
674
- `No memory store existed at or above ${binding.searchedFrom}, so the ` +
675
- `empty ${root} was created for this session.`;
725
+ `No memory store existed at or above ${binding.searchedFrom}, so ${root} ` +
726
+ `was designated for this session; nothing was created there.`;
676
727
  }
677
728
  else if (binding.origin === 'pin') {
678
729
  where = `The project store ${root} is empty; ${MEMORY_DIR_ENV} pinned it.`;
@@ -213,6 +213,35 @@ export declare class MemoryStore {
213
213
  readonly k: number;
214
214
  private readonly today;
215
215
  constructor(root: string, options?: MemoryStoreOptions);
216
+ /**
217
+ * Bring the store's directories into existence, or refuse the write that needs them.
218
+ *
219
+ * THE PROPERTY: a store that cannot be brought into existence cannot be written to, and the
220
+ * refusal names which directory and why. It is a refusal and not a crash because the only
221
+ * callers are the two WRITES (`save`, `compact`); every read is already defined over an
222
+ * absent directory — `facts()`' own docstring rules it, "AN ABSENT DIRECTORY IS `[]`, NOT AN
223
+ * ERROR" — which is what makes a lazily-built project layer legal at all.
224
+ *
225
+ * THE PREDICATE IS THE OUTCOME AND NEVER AN ERRNO, and that is measured rather than tidy.
226
+ * One cwd, `/`, gives CPython EROFS(30) at `/.bantamkit` and Node ENOENT(-2) at
227
+ * `/.bantamkit/memory/facts`, because Node's recursive mkdir does not pass EROFS through and
228
+ * stats the missing path instead — re-measured here at cwd `/`,
229
+ * `mkdirSync('/.bantamkit', {recursive: true})` is ENOENT(-2) where `mkdirSync('/.bantamkit')`
230
+ * is EROFS(-30); a `chmod 555` directory gives EACCES(13) on both; and on Linux `/` is a
231
+ * writable root owned by root, so the same cwd gives EACCES there. Three platforms, three
232
+ * numbers, one fact — the directory could not be made — so the `catch` is the whole of
233
+ * `PyOSError`, which is this runtime's `OSError`, and the sentence carries no errno. A fix
234
+ * keyed on the number would have been green on Linux CI and wrong on the machine the bug was
235
+ * reported from, and on THIS side it would additionally have been keyed on a number libuv
236
+ * invented rather than on the one the filesystem returned.
237
+ *
238
+ * THE SENTENCE NAMES `this.root` AND NOTHING DEEPER, for the same reason. The two runtimes
239
+ * fail at different components of the same path (`/.bantamkit` against
240
+ * `/.bantamkit/memory/facts`) and report different strerrors for it, so a sentence carrying
241
+ * the failing leaf, or that strerror, would be a divergence manufactured by mkdir's
242
+ * internals. `this.root` is the path the caller named and both sides agree on it, which is
243
+ * what lets one conformance case pin this line byte for byte.
244
+ */
216
245
  private ensureDirs;
217
246
  /**
218
247
  * Write a fact, or refuse it as a near-duplicate of one already here.
@@ -505,9 +505,46 @@ export class MemoryStore {
505
505
  if (options.create ?? true)
506
506
  this.ensureDirs();
507
507
  }
508
+ /**
509
+ * Bring the store's directories into existence, or refuse the write that needs them.
510
+ *
511
+ * THE PROPERTY: a store that cannot be brought into existence cannot be written to, and the
512
+ * refusal names which directory and why. It is a refusal and not a crash because the only
513
+ * callers are the two WRITES (`save`, `compact`); every read is already defined over an
514
+ * absent directory — `facts()`' own docstring rules it, "AN ABSENT DIRECTORY IS `[]`, NOT AN
515
+ * ERROR" — which is what makes a lazily-built project layer legal at all.
516
+ *
517
+ * THE PREDICATE IS THE OUTCOME AND NEVER AN ERRNO, and that is measured rather than tidy.
518
+ * One cwd, `/`, gives CPython EROFS(30) at `/.bantamkit` and Node ENOENT(-2) at
519
+ * `/.bantamkit/memory/facts`, because Node's recursive mkdir does not pass EROFS through and
520
+ * stats the missing path instead — re-measured here at cwd `/`,
521
+ * `mkdirSync('/.bantamkit', {recursive: true})` is ENOENT(-2) where `mkdirSync('/.bantamkit')`
522
+ * is EROFS(-30); a `chmod 555` directory gives EACCES(13) on both; and on Linux `/` is a
523
+ * writable root owned by root, so the same cwd gives EACCES there. Three platforms, three
524
+ * numbers, one fact — the directory could not be made — so the `catch` is the whole of
525
+ * `PyOSError`, which is this runtime's `OSError`, and the sentence carries no errno. A fix
526
+ * keyed on the number would have been green on Linux CI and wrong on the machine the bug was
527
+ * reported from, and on THIS side it would additionally have been keyed on a number libuv
528
+ * invented rather than on the one the filesystem returned.
529
+ *
530
+ * THE SENTENCE NAMES `this.root` AND NOTHING DEEPER, for the same reason. The two runtimes
531
+ * fail at different components of the same path (`/.bantamkit` against
532
+ * `/.bantamkit/memory/facts`) and report different strerrors for it, so a sentence carrying
533
+ * the failing leaf, or that strerror, would be a divergence manufactured by mkdir's
534
+ * internals. `this.root` is the path the caller named and both sides agree on it, which is
535
+ * what lets one conformance case pin this line byte for byte.
536
+ */
508
537
  ensureDirs() {
509
- pyMkdirParents(pyJoin(this.root, 'facts'));
510
- pyMkdirParents(pyJoin(this.root, 'archive'));
538
+ try {
539
+ pyMkdirParents(pyJoin(this.root, 'facts'));
540
+ pyMkdirParents(pyJoin(this.root, 'archive'));
541
+ }
542
+ catch (e) {
543
+ if (!(e instanceof PyOSError))
544
+ throw e;
545
+ throw new MemoryValidationError(`memory store could not be created: ${this.root}; the directory is not` +
546
+ ' there and this filesystem would not make it, so nothing was written');
547
+ }
511
548
  }
512
549
  // ---- ops ----
513
550
  /**
@@ -742,6 +779,14 @@ export class MemoryStore {
742
779
  */
743
780
  compact(reserve = null) {
744
781
  const facts = this.facts();
782
+ // SECOND, NOT FIRST, and the order is the reference's own ruling kept intact: the listing
783
+ // still refuses an unreadable store before anything here moves a file. What this line adds
784
+ // is the other half of the same stance — this method WRITES (every `pyReplace` below lands
785
+ // in `archive/`, and `rebuildIndex` writes `index.md` into the root whether or not a single
786
+ // fact was archived), so a root that does not exist and cannot be made is refused here, by
787
+ // name, instead of surfacing as a `FileNotFoundError` out of the final write. `exist_ok`
788
+ // semantics make it free for every store that is already there.
789
+ this.ensureDirs();
745
790
  const sizes = new Map();
746
791
  for (const fact of facts) {
747
792
  sizes.set(pyHashKey(fact.name), Buffer.byteLength(this.indexLine(fact), 'utf8'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bantamkit-mcp",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "bantamkit MCP server — pure Node, runnable with npx",
5
5
  "type": "module",
6
6
  "bin": {