bantamkit-mcp 0.34.1 → 0.34.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ intentional difference written down as a ruling.
30
30
  | [The operator CLI: `bantamkit-memory`](#the-operator-cli-bantamkit-memory) | The second bin: status, lint, compact, archive, restore |
31
31
  | [Memory stores are layered by default](#memory-stores-are-layered-by-default) | Why not to add `--store` by reflex |
32
32
  | [The asset pack](#the-asset-pack) | What ships in `assets/` and how it is found |
33
- | [Sharing a store with the Python server](#sharing-a-store-with-the-python-server) | Concurrent writes, and one hand-edit to avoid |
33
+ | [Where the Python and Node servers differ](#where-the-python-and-node-servers-differ) | One store; concurrent writes, one hand-edit to avoid |
34
34
  | [Development](#development) | Build, test, conformance |
35
35
  | [Measurements and history](#measurements-and-history) | Where the measured numbers behind this page live |
36
36
 
@@ -261,7 +261,7 @@ dependency in a temp directory that will be deleted, and `npm update` cannot hel
261
261
  | Two machines with one config run different builds | [Silent version float](#silent-version-float): compare `build_id` |
262
262
  | `bantamkit_status` still shows the old version after an update | The host has not reconnected; restart the server |
263
263
  | Recall finds nothing, or the wrong store | Set `BANTAMKIT_MEMORY_DIR`; the reply names the store it searched |
264
- | `malformed fact file …` | A hand-edited fact has a bare non-string field; see [Sharing a store](#sharing-a-store-with-the-python-server) |
264
+ | `malformed fact file …` | A hand-edited fact has a bare non-string field; see [Where the Python and Node servers differ](#where-the-python-and-node-servers-differ) |
265
265
  | `AssetNotFound: no assets directory found; set BANTAMKIT_ASSETS` | See [The asset pack](#the-asset-pack) |
266
266
 
267
267
  ## Configuration
@@ -381,9 +381,12 @@ the pack ships whole — 91 files / 238,751 bytes on 2026-09-15 — or `assets_d
381
381
  3. `<repo>/assets/` — two levels above `dist/`, for a dev checkout with nothing vendored.
382
382
  4. Otherwise `AssetNotFound: no assets directory found; set BANTAMKIT_ASSETS`.
383
383
 
384
- ## Sharing a store with the Python server
384
+ ## Where the Python and Node servers differ
385
385
 
386
- Both servers read and write the same memory store; byte-compatibility is the product.
386
+ Both servers read and write the same on-disk store; byte-compatibility is the product. Every
387
+ feature lands in both in one change, and a conformance case compares their answers. Differences
388
+ are deliberate and each is ruled in the
389
+ [divergence table](https://github.com/Ink01101011/bantamkit/blob/main/docs/porting.md#where-the-two-runtimes-deliberately-differ):
387
390
 
388
391
  - **Concurrent writes are safe for the memory store and lossy for the checkpoint, in both
389
392
  runtimes.** `index.md` is derived from `facts/`, so the last writer re-enumerates everything
package/dist/eventlog.js CHANGED
@@ -55,8 +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, pyExists, PyOSError, pyJoin, pyName, pyParent, pyParents } from './memory/pyfs.js';
59
- import { ensureBantamkitGitignore } from './memory/store.js';
58
+ import { cmpCodepoint, osErrorClassName, pyExists, PyOSError, pyJoin, pyName, pyParent } from './memory/pyfs.js';
59
+ import { bantamkitDirFor, ensureBantamkitGitignore } from './memory/store.js';
60
60
  /**
61
61
  * Environment switch. Unset or `off`/`0`/`false`/`no`/empty -> disabled. `on`/`1`/`true`/
62
62
  * `yes` -> the default file inside the memory store. Anything else is taken as the literal
@@ -254,13 +254,12 @@ export class EventLog {
254
254
  // USER RULING #2 (J51-8b): the ignore file is written only when THIS call is what
255
255
  // creates `.bantamkit`, so existence has to be checked BEFORE the mkdir below -- after
256
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
- }
257
+ //
258
+ // J54-3: the walk moved into `bantamkitDirFor` beside the writer it feeds, so the two
259
+ // creators in this runtime ask the same question in the same words rather than keeping a
260
+ // copy each. The answer here is unchanged -- a log file is never itself named
261
+ // `.bantamkit`, so self-or-ancestors and ancestors give the same directory.
262
+ const bantamkitDir = bantamkitDirFor(path);
264
263
  const bantamkitDirExistedBefore = bantamkitDir !== null && pyExists(bantamkitDir);
265
264
  // `pyParent` and not `node:path`'s `dirname`: this module is reached from a Windows
266
265
  // host through the same code, and the reference's `Path(...).parent` is the spelling
@@ -37,6 +37,7 @@ import { homedir } from 'node:os';
37
37
  import { dirname, join } from 'node:path';
38
38
  import { fileURLToPath } from 'node:url';
39
39
  import { currentInstall, Undetermined } from './mcp/identity.js';
40
+ import { bantamkitDirFor, ensureBantamkitGitignore } from './memory/store.js';
40
41
  import { compareVersions, keptCli, keptManifest, keptPrefix, PACKAGE, runInstaller, shlexJoin } from './npminstall.js';
41
42
  import { dumpJson, fromJs } from './pyjson.js';
42
43
  export { keptCli, keptPrefix };
@@ -319,7 +320,28 @@ export function thisCommand(options = {}) {
319
320
  }
320
321
  const command = ['npm', 'install', '--prefix', prefix, `${PACKAGE}@${version}`];
321
322
  const rendered = shlexJoin(command);
323
+ // THE THIRD `.bantamkit` CREATOR (J54-3), and the one that creates the most of it: npm makes
324
+ // a missing `--prefix` itself, so this line — not `MemoryStore`, not `EventLog` — is what
325
+ // brings `<homedir>/.bantamkit` into existence on a machine that has never saved a memory.
326
+ // Measured on the machine this was found on: 28 MB of install tree under a `~/.bantamkit`
327
+ // with no `.gitignore`, every byte of it untracked in a `$HOME` that is a git repository.
328
+ //
329
+ // Checked BEFORE npm runs, per user ruling #2, for the reason both other creators check it
330
+ // before their own mkdir: afterwards the directory exists either way and the question
331
+ // "did THIS call create it" is unanswerable. A `~/.bantamkit` that was already there —
332
+ // holding an older kept install, a memory store, or an ignore file the operator deleted on
333
+ // purpose — is left exactly as it is.
334
+ const bantamkitDir = bantamkitDirFor(prefix);
335
+ const bantamkitDirExistedBefore = bantamkitDir !== null && existsSync(bantamkitDir);
322
336
  const [code, output] = (options.installer ?? runInstaller)(command);
337
+ // BEFORE THE REFUSALS, AND THAT ORDER IS MEASURED, not tidy. A `npm install --prefix` that
338
+ // FAILS still leaves the prefix behind: measured 2026-09-18 with `npm_config_offline=true`
339
+ // against an empty cache — exit 1, and `<home>/.bantamkit/mcp` there afterwards. A refusal
340
+ // that returned first would leave exactly the directory this closes over: made by bantamkit's
341
+ // own command, holding nothing anybody asked for, and visible to git. The helper never
342
+ // creates `bantamkitDir` itself, so an installer that made nothing writes nothing here.
343
+ if (bantamkitDir !== null)
344
+ ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
323
345
  const refused = `could not make the kept install at ${prefix}, so no host configuration was changed: ${rendered} exited ${code}`;
324
346
  if (code !== 0)
325
347
  throw new InstallError(`${refused}: ${lastLine(output)}`);
@@ -33,6 +33,14 @@ export declare const BANTAMKIT_GITIGNORE_TEXT: string;
33
33
  * a `.bantamkit` that git can see (or ignore one it should not), which is the whole bug this
34
34
  * closes.
35
35
  *
36
+ * THE CREATORS, ENUMERATED (J54-3), because "anything else" is how one got missed: in THIS
37
+ * runtime there are exactly three — `MemoryStore.ensureDirs`, `EventLog`'s append-time
38
+ * `mkdirSync`, and `hostinstall.thisCommand`, where `--install` from an `npx` cache has npm
39
+ * create `<homedir>/.bantamkit/mcp` (npm makes a missing `--prefix` itself). The first two ask
40
+ * `bantamkitDirFor` which directory the decision is about rather than assuming the root's
41
+ * parent is it; the third is the one the reference does not have at all, because the offline
42
+ * install is Node-only by user ruling. `docs/porting.md` carries the row.
43
+ *
36
44
  * NEVER REWRITES. A `.gitignore` that already exists -- whatever its bytes -- is left
37
45
  * exactly as it is, kept as a second guard even when `created` is `true`: an operator who
38
46
  * deleted it to commit the directory keeps that decision, and this never diffs its own
@@ -48,6 +56,27 @@ export declare const BANTAMKIT_GITIGNORE_TEXT: string;
48
56
  * bring `.bantamkit` into existence early.
49
57
  */
50
58
  export declare function ensureBantamkitGitignore(directory: string, created: boolean): void;
59
+ /**
60
+ * The `.bantamkit` directory a recursive mkdir of `path` would create or fill, or `null` when
61
+ * there is none: `path` itself if it is named `.bantamkit`, otherwise its nearest ancestor
62
+ * that is. `runtime-py/src/bantamkit/memory/store.py`'s `bantamkit_dir_for` is the reference.
63
+ *
64
+ * WHY A WALK AND NOT `pyParent(path)` (J54-3). Every creator has to hand
65
+ * `ensureBantamkitGitignore` the directory the decision is ABOUT, and the parent is the right
66
+ * answer for exactly one shape of path, `<x>/.bantamkit/memory`. A store rooted AT the
67
+ * `.bantamkit` directory (`--store ~/.bantamkit`) or nested deeper under it creates a
68
+ * `.bantamkit` whose gitignore decision was taken about the wrong directory — so no decision
69
+ * was taken at all, and git could see the whole tree. MEASURED, not hypothetical: `~/.bantamkit`
70
+ * on the machine this was found on holds an empty `facts/` and `archive/` beside `memory/`,
71
+ * left by a store once rooted at it, and no `.gitignore`.
72
+ *
73
+ * NEAREST AND NOT OUTERMOST, which is the choice `EventLog`'s append already made and this
74
+ * function now carries for both creators.
75
+ *
76
+ * READ-ONLY. It looks at names, never at the filesystem, so it is equally valid before or after
77
+ * the mkdir — but `created` is answered by checking THIS path's existence BEFORE.
78
+ */
79
+ export declare function bantamkitDirFor(path: string): string | null;
51
80
  /** `sorted(VALID_TYPES)` is what the error message interpolates, so the order is load-bearing. */
52
81
  export declare const VALID_TYPES: readonly ["feedback", "project", "reference", "user"];
53
82
  /**
@@ -42,7 +42,7 @@ 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, pyParent, pyReadText, pyReplace, pyScandirNames, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, PyOSError, sortedPathNames, } from './pyfs.js';
45
+ import { asPyOSError, cmpCodepoint, matchesMd, pyExists, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, pyParent, pyParents, pyReadText, pyReplace, pyScandirNames, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, PyOSError, sortedPathNames, } from './pyfs.js';
46
46
  /**
47
47
  * The exact bytes written into a `.bantamkit/.gitignore` the first time such a directory is
48
48
  * brought into existence (user ruling 2026-09-15, `.shiftwork/notes-job51/P0-probes.md`).
@@ -76,6 +76,14 @@ export const BANTAMKIT_GITIGNORE_TEXT = '# Created by bantamkit: this directory
76
76
  * a `.bantamkit` that git can see (or ignore one it should not), which is the whole bug this
77
77
  * closes.
78
78
  *
79
+ * THE CREATORS, ENUMERATED (J54-3), because "anything else" is how one got missed: in THIS
80
+ * runtime there are exactly three — `MemoryStore.ensureDirs`, `EventLog`'s append-time
81
+ * `mkdirSync`, and `hostinstall.thisCommand`, where `--install` from an `npx` cache has npm
82
+ * create `<homedir>/.bantamkit/mcp` (npm makes a missing `--prefix` itself). The first two ask
83
+ * `bantamkitDirFor` which directory the decision is about rather than assuming the root's
84
+ * parent is it; the third is the one the reference does not have at all, because the offline
85
+ * install is Node-only by user ruling. `docs/porting.md` carries the row.
86
+ *
79
87
  * NEVER REWRITES. A `.gitignore` that already exists -- whatever its bytes -- is left
80
88
  * exactly as it is, kept as a second guard even when `created` is `true`: an operator who
81
89
  * deleted it to commit the directory keeps that decision, and this never diffs its own
@@ -103,6 +111,33 @@ export function ensureBantamkitGitignore(directory, created) {
103
111
  /* best-effort: a missing ignore file is not worth a lost fact */
104
112
  }
105
113
  }
114
+ /**
115
+ * The `.bantamkit` directory a recursive mkdir of `path` would create or fill, or `null` when
116
+ * there is none: `path` itself if it is named `.bantamkit`, otherwise its nearest ancestor
117
+ * that is. `runtime-py/src/bantamkit/memory/store.py`'s `bantamkit_dir_for` is the reference.
118
+ *
119
+ * WHY A WALK AND NOT `pyParent(path)` (J54-3). Every creator has to hand
120
+ * `ensureBantamkitGitignore` the directory the decision is ABOUT, and the parent is the right
121
+ * answer for exactly one shape of path, `<x>/.bantamkit/memory`. A store rooted AT the
122
+ * `.bantamkit` directory (`--store ~/.bantamkit`) or nested deeper under it creates a
123
+ * `.bantamkit` whose gitignore decision was taken about the wrong directory — so no decision
124
+ * was taken at all, and git could see the whole tree. MEASURED, not hypothetical: `~/.bantamkit`
125
+ * on the machine this was found on holds an empty `facts/` and `archive/` beside `memory/`,
126
+ * left by a store once rooted at it, and no `.gitignore`.
127
+ *
128
+ * NEAREST AND NOT OUTERMOST, which is the choice `EventLog`'s append already made and this
129
+ * function now carries for both creators.
130
+ *
131
+ * READ-ONLY. It looks at names, never at the filesystem, so it is equally valid before or after
132
+ * the mkdir — but `created` is answered by checking THIS path's existence BEFORE.
133
+ */
134
+ export function bantamkitDirFor(path) {
135
+ for (const candidate of [path, ...pyParents(path)]) {
136
+ if (pyName(candidate) === '.bantamkit')
137
+ return candidate;
138
+ }
139
+ return null;
140
+ }
106
141
  /** `sorted(VALID_TYPES)` is what the error message interpolates, so the order is load-bearing. */
107
142
  export const VALID_TYPES = ['feedback', 'project', 'reference', 'user'];
108
143
  /**
@@ -598,8 +633,13 @@ export class MemoryStore {
598
633
  // Checked BEFORE the mkdir below, per user ruling #2: this is the one moment that can
599
634
  // tell whether `.bantamkit` already existed. After the mkdir it unconditionally exists
600
635
  // 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);
636
+ //
637
+ // J54-3: `bantamkitDirFor`, not `pyParent`. The mkdirs below create every missing
638
+ // component of `this.root` — a root that IS the `.bantamkit` directory, or one nested
639
+ // deeper under it, brought a `.bantamkit` into existence while the decision was being
640
+ // taken about some other directory, which meant no decision at all.
641
+ const bantamkitDir = bantamkitDirFor(this.root);
642
+ const bantamkitDirExistedBefore = bantamkitDir !== null && pyExists(bantamkitDir);
603
643
  try {
604
644
  pyMkdirParents(pyJoin(this.root, 'facts'));
605
645
  pyMkdirParents(pyJoin(this.root, 'archive'));
@@ -610,7 +650,8 @@ export class MemoryStore {
610
650
  throw new MemoryValidationError(`memory store could not be created: ${this.root}; the directory is not` +
611
651
  ' there and this filesystem would not make it, so nothing was written');
612
652
  }
613
- ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
653
+ if (bantamkitDir !== null)
654
+ ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
614
655
  }
615
656
  // ---- ops ----
616
657
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bantamkit-mcp",
3
- "version": "0.34.1",
3
+ "version": "0.34.3",
4
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
5
  "keywords": [
6
6
  "mcp",