bantamkit-mcp 0.29.1 → 0.30.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # bantamkit-mcp
2
2
 
3
3
  The bantamkit MCP server as a pure-Node package: `npx bantamkit-mcp`, no Python, no
4
- `uv`, no `pipx`, no interpreter bootstrap. It serves the same eleven tools, the same
4
+ `uv`, no `pipx`, no interpreter bootstrap. It serves the same thirteen tools, the same
5
5
  `bantamkit_status` prompt and the same two resource templates as `runtime-py`'s server,
6
6
  reads and writes the same memory store, and is checked against the Python server frame by
7
7
  frame — 4500+ conformance cases, with every intentional difference written down as a
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "memory_dream",
3
+ "description": "Consolidate the memories the project layer and the machine-wide profile layer hold under the SAME NAME, and say exactly what changed. Byte-identical copies collapse to one; a pair that has diverged is UNIONED — every claim from both copies survives, never a winner-takes-all — and a relative date in a body is annotated with the day it resolves to against that fact's own mtime, never against today. The survivor stays in the writable project layer and the profile copy MOVES to that store's archive/: nothing is deleted and any consumed memory can be restored by name. This is a correctness pass, not a token saving: the profile index is derived at read time and is not loaded from a prompt, so consolidating it frees approximately no bytes — what it buys is one copy of a ruling instead of two that can disagree. It defaults to a DRY RUN because the profile store is machine-wide: a memory archived out of it stops answering for every other project on this machine that has no store of its own. Read the plan first, then call it again with dry_run false to apply it.",
4
+ "surfaces": ["mcp"],
5
+ "parameters": {
6
+ "type": "object",
7
+ "properties": {
8
+ "dry_run": {
9
+ "type": "boolean",
10
+ "description": "Preview only. True (the default) performs every read, merge and projection and writes nothing. Pass false to apply the plan."
11
+ }
12
+ }
13
+ },
14
+ "output_schema": {
15
+ "properties": { "result": { "title": "Result", "type": "string" } },
16
+ "required": ["result"],
17
+ "type": "object",
18
+ "title": "memory_dreamOutput"
19
+ }
20
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "repo_map",
3
+ "description": "A ranked map of a source tree: every file's definitions, ordered by how central that file is to the files you name in `focus`, truncated to a byte budget. Call it when you are about to work on a file and want to know which OTHER files matter to it — the ports, the callers, the module it reaches through a private helper — rather than reading a directory listing and guessing. `focus` is the file or files you are editing, relative to `root` and POSIX-separated; they are excluded from the listing because you already have them open, and an empty focus gives plain centrality over the whole tree. This is a PRECISION pass and it is not a token saving: the gate this feature was supposed to clear was refuted by measurement — discovery is 0.114% of real prompt tokens, because 97.8% of the bill is cache_read — so a map does not make a session cheaper. What it buys is the right file found sooner. Nothing the scanner cannot read is dropped silently: every unread file resolves into a named omission (unknown-language, unreadable-bytes, size-cap, no-definitions, unreachable, per-file-cap, budget) counted in a footer that is NOT charged to the budget. The budget is UTF-8 BYTES of the listing, not tokens; there is no model tokenizer in either runtime and this tool will not pretend to one.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "type": "object",
9
+ "required": [
10
+ "root"
11
+ ],
12
+ "properties": {
13
+ "root": {
14
+ "type": "string",
15
+ "description": "Directory to map, absolute or relative to the server's working directory"
16
+ },
17
+ "focus": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "string"
21
+ },
22
+ "description": "The files you are working on, relative to `root` and POSIX-separated. They are excluded from the listing. A name that is not a scanned source is ignored rather than refused."
23
+ },
24
+ "budget": {
25
+ "type": "integer",
26
+ "minimum": 0,
27
+ "description": "UTF-8 bytes of listing to render; default 4000. The omission footer is not charged against it."
28
+ }
29
+ }
30
+ },
31
+ "output_schema": {
32
+ "properties": {
33
+ "result": {
34
+ "title": "Result",
35
+ "type": "string"
36
+ }
37
+ },
38
+ "required": [
39
+ "result"
40
+ ],
41
+ "type": "object",
42
+ "title": "repo_mapOutput"
43
+ }
44
+ }
package/dist/docread.d.ts CHANGED
@@ -34,6 +34,7 @@
34
34
  * (`utf8Scan`).
35
35
  */
36
36
  import { BantamError } from './errors.js';
37
+ import { cmpCodepoint, pyRepr, pyStrip } from './pysem.js';
37
38
  export declare const NS_S = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}";
38
39
  export declare const NS_W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}";
39
40
  export declare const NS_R = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}";
@@ -52,19 +53,15 @@ export declare const OMIT_UNMAPPED = "unmapped-text";
52
53
  export declare const OMIT_UNREAD_TAIL = "unread-tail";
53
54
  export declare const OMIT_SIZE_CAP = "size-cap";
54
55
  export declare const OMIT_UNPLACED_CELL = "unplaced-cell";
56
+ export declare const OMIT_DUPLICATE_CELL = "duplicate-cell";
55
57
  /** Extraction failed. The message names what was seen, never just the format's own error. */
56
58
  export declare class DocumentReadError extends BantamError {
57
59
  }
58
- /** `str.strip()` with no argument. */
59
- export declare function pyStrip(s: string): string;
60
+ export { cmpCodepoint, pyRepr, pyStrip };
60
61
  /** `str.split()` with no argument: runs of whitespace, no empty fields. */
61
62
  export declare function pySplit(s: string): string[];
62
63
  /** `str.splitlines()`: Python's line boundaries, terminators dropped. */
63
64
  export declare function pySplitlines(s: string): string[];
64
- /** `sorted()` on strings: by code point, which is not what `Array.sort` does above U+FFFF. */
65
- export declare function cmpCodepoint(a: string, b: string): number;
66
- /** `repr(str)`. */
67
- export declare function pyRepr(s: string): string;
68
65
  /** `repr(bytes)`. */
69
66
  export declare function bytesRepr(buf: Uint8Array): string;
70
67
  export interface Utf8Scan {
@@ -190,6 +187,35 @@ export declare class BadZipFile extends Error {
190
187
  constructor(message: string);
191
188
  }
192
189
  export declare function crc32(buf: Uint8Array): number;
190
+ /**
191
+ * How much ONE MEMBER of a zip this reader will decompress and parse. The ceiling on the
192
+ * PARSE, which is a different door from `XLSX_MAX_TEXT_BYTES`: that one bounds the text a
193
+ * workbook renders, and it never stands in front of this, because `readMember` decompresses
194
+ * the member whole and `parsePart` builds a tree from it before the first row is rendered.
195
+ *
196
+ * MEASURED 2026-09-06 on this machine, through `docread.extract` on THIS runtime at the
197
+ * shipped constants, on a sheet of N `<row><c t="inlineStr"><is><t>x</t></is></c></row>`
198
+ * deflated by `deflateRawSync`, peak RSS from `process.resourceUsage().maxRSS`:
199
+ * 400,000 rows are 58,378 bytes on disk, declare 19,600,112, and took RSS from 102.2 MB to
200
+ * 1,039.3 MB in 1,019 ms with NO omission; 1,000,000 rows are 143,939 bytes, declare
201
+ * 49,000,112, and took it from 158.6 MB to 2,290.8 MB in 2,667 ms. Linear and unbounded —
202
+ * the memory is the `XmlElement` tree, not the text, so a budget over the rendering could
203
+ * not see it. The reference measured the same shape at 342.2 MB and 838.8 MB through
204
+ * `tracemalloc`; the numbers are not comparable (one counts a Python allocator, the other a
205
+ * whole process) and the SHAPE is what both of them state.
206
+ *
207
+ * 16 MiB, the number `TEXT_MAX_BYTES` states, because "how much of a file this reader reads"
208
+ * is one number — but under its OWN NAME, because it bounds a member of a container and not a
209
+ * file on a disk, and a bar that varies one must not be varying the other. The reference
210
+ * measured the corpus behind the figure: 25 OOXML/ODF packages under the goal's roots, the
211
+ * largest single XML member among them 4,283,286 bytes, 3.9x under this.
212
+ *
213
+ * A REFUSAL and not a truncation, which is the one place this module departs from
214
+ * "disclose, never truncate": half an XML document is not a smaller XML document, and a tree
215
+ * built from a severed member would carry text that is not what the file says. So the reader
216
+ * stops and names the member, the number the file declares and its own ceiling.
217
+ */
218
+ export declare const ZIP_MEMBER_MAX_BYTES: number;
193
219
  /** `zipfile.ZipFile` read-only: the central directory, and `read(name)` with a CRC check. */
194
220
  export declare class ZipReader {
195
221
  private readonly data;
@@ -209,6 +235,8 @@ export declare class ZipReader {
209
235
  isDir: boolean;
210
236
  }[];
211
237
  has(name: string): boolean;
238
+ /** `getinfo(name).file_size`: what the central directory DECLARES, before any inflate. */
239
+ declaredSize(name: string): number;
212
240
  /** `ZipFile.read(name)`; a missing member is a `KeyError` — here `RangeError`, see `readMember`. */
213
241
  read(name: string): Buffer;
214
242
  }
@@ -293,6 +321,32 @@ export declare const XLSX_MAX_COLUMNS = 16384;
293
321
  */
294
322
  export declare const UNPLACED_SHAPE = "the column of a cell whose reference is not letters then digits";
295
323
  export declare const UNPLACED_RANGE = "the column of a cell past XFD, the last column the format has";
324
+ export declare const DUPLICATE_CELL = "the text of a cell a later cell in the same row and column replaced";
325
+ /**
326
+ * How much text ONE WORKBOOK may materialise, all sheets together. `XLSX_MAX_COLUMNS` bounds a
327
+ * ROW and nothing bounded the document, which is a ceiling with a hole in it: the width is
328
+ * bought a row at a time. MEASURED 2026-09-06 on both runtimes: 20,000 rows each holding one
329
+ * `XFD1` cell deflate to 53,967 bytes and materialise 327,680,000 bytes in ~9 s — 6,072x, out
330
+ * of a file small enough to mail.
331
+ *
332
+ * 16 MiB, the same figure `TEXT_MAX_BYTES` carries and for the same kind of reason, but under
333
+ * its OWN NAME because the two bound different things: bytes read off a disk there, bytes
334
+ * rendered out of a container here, and a bar that varies one must not be varying the other.
335
+ * MEASURED 2026-09-06 over the goal's roots: 15 `.xlsx`, the largest 8,664,227 bytes on disk,
336
+ * and the largest RENDERING among them 754,520 bytes — 22x under this budget, so no real
337
+ * workbook on this machine meets it.
338
+ *
339
+ * It bounds the RENDERING and nothing else. The sentence that stood here said it bounded the
340
+ * rendering "because the file is already bounded", and that was FALSE for the whole life of
341
+ * this constant: `readMember` decompressed a member whole and `parsePart` built a tree from
342
+ * it, both before the first row was rendered and both outside this budget, so 58,378 bytes on
343
+ * disk took RSS to 1,039.3 MB with no omission and this ceiling never saw it (review round 5,
344
+ * H1). What bounds the file is `ZIP_MEMBER_MAX_BYTES`, one door earlier; this bounds what
345
+ * comes out of it. The shortfall is disclosed as `OMIT_SIZE_CAP` counting the rows no sheet
346
+ * rendered: a row is what a caller addresses, and a byte count of text that was never built
347
+ * would be a number this reader cannot honestly produce.
348
+ */
349
+ export declare const XLSX_MAX_TEXT_BYTES: number;
296
350
  /**
297
351
  * `B7` -> `[1, '']`. The cell's own reference decides its column; XML order is a fallback.
298
352
  *
@@ -320,8 +374,22 @@ export declare const UNPLACED_RANGE = "the column of a cell past XFD, the last c
320
374
  * spends asking for more.
321
375
  */
322
376
  export declare function columnIndex(ref: string | undefined, fallback: number): [number, string];
323
- export declare function extractXlsx(path: string): Document;
324
- export declare function extractDocx(path: string): Document;
377
+ /**
378
+ * Every declared sheet, under ONE `XLSX_MAX_TEXT_BYTES` budget for the whole workbook.
379
+ *
380
+ * The budget is the document's, so it is made here and not in `sheetRows`, and the shortfall
381
+ * is disclosed at the document's grain for the same reason — it is not a fact about the sheet
382
+ * the budget happened to run out on. The cap is stated BEFORE the media tally, because the
383
+ * media a reader met is a count of what it met underneath the cap.
384
+ *
385
+ * `opened` is the reader `sniff` already had to build to decide this file is an OOXML package
386
+ * (entries (a)/(j)): before it was handed down, one `extract` read the whole archive off disk
387
+ * TWICE. It is a parameter and never a field, so its lifetime is this call — a reader kept
388
+ * across calls would answer from bytes no longer on disk, which is exactly what the document
389
+ * cache one layer up is keyed on (realpath, size, mtime_ns) to avoid.
390
+ */
391
+ export declare function extractXlsx(path: string, opened?: ZipReader | null): Document;
392
+ export declare function extractDocx(path: string, opened?: ZipReader | null): Document;
325
393
  /** `html.unescape`. */
326
394
  export declare function unescape(s: string): string;
327
395
  /** Rendered rows of one HTML fragment. A pure function of the string: no I/O, no host. */
@@ -349,6 +417,12 @@ export declare function a2bUu(line: Uint8Array): Buffer;
349
417
  */
350
418
  export declare function decodeUu(encoded: Buffer): Buffer;
351
419
  export declare function extractMhtml(path: string): Document;
420
+ /**
421
+ * Markup to rows, up to `TEXT_MAX_BYTES` of it, saying how much it did not read.
422
+ *
423
+ * BOUNDED, entry (k): this did `readFileSync(path)`, so a 1 GB `.html` was materialised whole
424
+ * where a 1 GB `.txt` had stopped at the cap and counted the rest since J10.
425
+ */
352
426
  export declare function extractHtml(path: string): Document;
353
427
  export declare function extractText(path: string): Document;
354
428
  /** Dispatch on what the file IS. Anything unreadable raises, naming the container. */