bantamkit-mcp 0.29.2 → 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 +1 -1
- package/assets/tools/memory_dream.json +20 -0
- package/assets/tools/repo_map.json +44 -0
- package/dist/eventlog.js +5 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/mcp/pyargs.d.ts +1 -1
- package/dist/mcp/pyargs.js +68 -0
- package/dist/mcp/server.d.ts +36 -6
- package/dist/mcp/server.js +152 -5
- package/dist/memory/cli.js +1 -1
- package/dist/memory/component.d.ts +73 -2
- package/dist/memory/component.js +157 -5
- package/dist/memory/dream.d.ts +247 -0
- package/dist/memory/dream.js +903 -0
- package/dist/memory/store.d.ts +45 -1
- package/dist/memory/store.js +107 -2
- package/dist/repomap.d.ts +247 -0
- package/dist/repomap.js +1040 -0
- package/package.json +1 -1
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
|
|
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/eventlog.js
CHANGED
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
* one stray write breaks the wire suite. Nothing in this module names `process.stdout`,
|
|
33
33
|
* `process.stderr` or `console`.
|
|
34
34
|
* * **Metadata only.** Never a tool argument's value, never a memory body, never a
|
|
35
|
-
* validated output, never a query, never a document row.
|
|
36
|
-
* unbounded free text and
|
|
37
|
-
* `skill_audit`'s `root` is another
|
|
38
|
-
* set — never the path, never a part
|
|
35
|
+
* validated output, never a query, never a document row. Seven of the thirteen tools take
|
|
36
|
+
* unbounded free text and six take absolute paths (`bantamkit_read`'s `path` is one,
|
|
37
|
+
* `skill_audit`'s `root` is another and `repo_map`'s `root` and `focus` are the third;
|
|
38
|
+
* each record carries counts and tokens from a closed set — never the path, never a part
|
|
39
|
+
* name, never a skill id, never a mapped file).
|
|
39
40
|
* Every value written here is an ASCII token from a closed set, a number, or a boolean.
|
|
40
41
|
* * **Never `String(err)`.** Only the constructor name. This is not hypothetical: the host
|
|
41
42
|
* itself has already persisted `input_value={'schema_path': '/Users/k...
|
package/dist/index.d.ts
CHANGED
|
@@ -13,11 +13,13 @@ export type { PyOrder, YamlScalar, YamlValue } from './memory/pyyaml.js';
|
|
|
13
13
|
export { countFacts, discoverProjectStore, loadGrants, MEMORY_DIR_ENV, PROJECT_STORE, resolveProjectStore, } from './memory/layers.js';
|
|
14
14
|
export type { StoreBinding } from './memory/layers.js';
|
|
15
15
|
export { layerLabel, Memory, normalizeName, profileStore } from './memory/component.js';
|
|
16
|
-
export type { CompactOutcome, MemoryOptions, RecallOutcome, SaveOutcome } from './memory/component.js';
|
|
16
|
+
export type { CompactOutcome, DreamOutcome, MemoryOptions, RecallOutcome, SaveOutcome, } from './memory/component.js';
|
|
17
|
+
export { absolutise, blockKey, claimSlot, collapse, dream, formatFixed3, mergeBodies, mergeDescriptions, mergeLinks, PROFILE_LAYER, PROJECT_LAYER, pyIntDigits, sortedNames, splitBlocks, SUPERSEDED_HEADING, } from './memory/dream.js';
|
|
18
|
+
export type { DateHit, DreamMerge, DreamResult, SimilarPair, Superseded } from './memory/dream.js';
|
|
17
19
|
export { CAP_BYTES, DEFAULT_RELATIVE_PATH, defaultPath, encodeRecord, EVENT_LOG_ENV, EventLog, formatTimestamp, resolvePath, SCHEMA_VERSION, } from './eventlog.js';
|
|
18
20
|
export type { DetailValue } from './eventlog.js';
|
|
19
|
-
export { DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
20
|
-
export type { MemoryStoreOptions, SaveResult } from './memory/store.js';
|
|
21
|
+
export { DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
22
|
+
export type { MemoryStoreOptions, SaveResult, StoreInternals } from './memory/store.js';
|
|
21
23
|
export { asPyOSError, cmpCodepoint, matchesMd, normcase, pyDecodeUtf8, pyCwd, pyExists, pyExpanduser, pyHome, pyIsAbsolute, pyIsDir, pyIsFile, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, PyOSError, pyParent, pyParents, pyReadText, pyReplace, pyRepr, pyResolve, PyRuntimeError, pyScandirNames, PyUnicodeDecodeError, pyAppendText, pyStatIsDir, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, sortedPathNames, sortedPathParts, STRERROR_NAMES, } from './memory/pyfs.js';
|
|
22
24
|
export { clockIn, clockOut, HISTORY_RING_SIZE, SCHEMA_NAME, status, TERMINAL_UNIT_STATUS, timestamp, } from './shiftwork.js';
|
|
23
25
|
export type { ClockOptions } from './shiftwork.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ export { decodeFactBytes, FactParseError, formatFact, parseFactText, parseFrontm
|
|
|
7
7
|
export { constructPlain, PyScalar, resolveImplicitTag, safeDumpMapping, YamlConstructError, YamlEmitError, } from './memory/pyyaml.js';
|
|
8
8
|
export { countFacts, discoverProjectStore, loadGrants, MEMORY_DIR_ENV, PROJECT_STORE, resolveProjectStore, } from './memory/layers.js';
|
|
9
9
|
export { layerLabel, Memory, normalizeName, profileStore } from './memory/component.js';
|
|
10
|
+
export { absolutise, blockKey, claimSlot, collapse, dream, formatFixed3, mergeBodies, mergeDescriptions, mergeLinks, PROFILE_LAYER, PROJECT_LAYER, pyIntDigits, sortedNames, splitBlocks, SUPERSEDED_HEADING, } from './memory/dream.js';
|
|
10
11
|
export { CAP_BYTES, DEFAULT_RELATIVE_PATH, defaultPath, encodeRecord, EVENT_LOG_ENV, EventLog, formatTimestamp, resolvePath, SCHEMA_VERSION, } from './eventlog.js';
|
|
11
|
-
export { DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
12
|
+
export { DEFAULT_INDEX_BUDGET, DUPLICATE_JACCARD, jaccard, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyCompareLt, pyEqualValue, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, sortScored, tokens, VALID_TYPES, } from './memory/store.js';
|
|
12
13
|
export { asPyOSError, cmpCodepoint, matchesMd, normcase, pyDecodeUtf8, pyCwd, pyExists, pyExpanduser, pyHome, pyIsAbsolute, pyIsDir, pyIsFile, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, PyOSError, pyParent, pyParents, pyReadText, pyReplace, pyRepr, pyResolve, PyRuntimeError, pyScandirNames, PyUnicodeDecodeError, pyAppendText, pyStatIsDir, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, sortedPathNames, sortedPathParts, STRERROR_NAMES, } from './memory/pyfs.js';
|
|
13
14
|
export { clockIn, clockOut, HISTORY_RING_SIZE, SCHEMA_NAME, status, TERMINAL_UNIT_STATUS, timestamp, } from './shiftwork.js';
|
package/dist/mcp/pyargs.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { type PyValue } from '../pyjson.js';
|
|
|
31
31
|
/** What a parameter accepts. `optional` is `| None = None` in the signature. */
|
|
32
32
|
export interface FieldSpec {
|
|
33
33
|
readonly name: string;
|
|
34
|
-
readonly kind: 'str' | 'int' | 'dict' | 'dictInt' | 'dictStr' | 'listStr';
|
|
34
|
+
readonly kind: 'str' | 'int' | 'bool' | 'dict' | 'dictInt' | 'dictStr' | 'listStr';
|
|
35
35
|
readonly optional: boolean;
|
|
36
36
|
/** `Field(le=...)`: an inclusive ceiling, checked AFTER the lax int parse succeeds. */
|
|
37
37
|
readonly le?: bigint;
|
package/dist/mcp/pyargs.js
CHANGED
|
@@ -126,6 +126,28 @@ export const ARG_MODELS = {
|
|
|
126
126
|
// `dict[str, str]`, and the reason `dictStr` exists beside it: a `str` field is STRICT even
|
|
127
127
|
// in lax mode, so `{'a': 1}` is `versions.a` / `string_type` where the same value under
|
|
128
128
|
// `usage` would validate.
|
|
129
|
+
// `dry_run: bool | None = None` on the reference (job45 row 5). LAX bool, and every arm
|
|
130
|
+
// below was MEASURED against the real stdio server, not read off pydantic's docs
|
|
131
|
+
// (`scratchpad` probe, 33 inputs): `0`/`1` and `0.0`/`1.0` coerce; any OTHER integral
|
|
132
|
+
// number — `2`, `-1`, `2.0` — is `bool_parsing`; a FRACTIONAL float is `bool_type`, a
|
|
133
|
+
// different frame from the integral one and the reason the float arm is split; a string
|
|
134
|
+
// is matched case-insensitively against two closed sets and is NOT stripped first, so
|
|
135
|
+
// `'true'` validates and `' true '` is `bool_parsing`; `''` is `bool_parsing`; a list or a
|
|
136
|
+
// dict is `bool_type`; and an explicit `null` is the default.
|
|
137
|
+
memory_dream: { model: 'memory_dreamArguments', fields: [opt('dry_run', 'bool')] },
|
|
138
|
+
// `repo_map(root: str, focus: list[str] | None = None, budget: int | None = None)` on
|
|
139
|
+
// the reference (job45 row 10). `root` is the strict `str` every other string field is —
|
|
140
|
+
// `123` is `string_type`. `focus` is the same `list[str]` as `skill_audit`'s `enabled`,
|
|
141
|
+
// so a non-list is `list_type` and a list holding a non-string is `focus.0` /
|
|
142
|
+
// `string_type`. `budget` is the lax `int` that `k`, `reserve` and `limit` are: `'2'`,
|
|
143
|
+
// `True` and `3.0` validate, `'2.5'` is `int_parsing`, `2.5` is `int_from_float`, and an
|
|
144
|
+
// explicit `null` is the default. No `le`, because the reference binds none — the
|
|
145
|
+
// manifest's `minimum: 0` is advisory to the client and the handler's own refusal is the
|
|
146
|
+
// negative one, exactly as `skill_audit`'s `budget` works.
|
|
147
|
+
repo_map: {
|
|
148
|
+
model: 'repo_mapArguments',
|
|
149
|
+
fields: [req('root', 'str'), opt('focus', 'listStr'), opt('budget', 'int')],
|
|
150
|
+
},
|
|
129
151
|
skill_audit: {
|
|
130
152
|
model: 'skill_auditArguments',
|
|
131
153
|
fields: [
|
|
@@ -178,6 +200,13 @@ function truncateRepr(text) {
|
|
|
178
200
|
tail += 1;
|
|
179
201
|
return `${decoder.decode(bytes.slice(0, head))}...${decoder.decode(bytes.slice(tail))}`;
|
|
180
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* The two closed sets pydantic-core matches a string against for a lax `bool`, lowercased.
|
|
205
|
+
*
|
|
206
|
+
* No stripping: `' true '` is `bool_parsing` on the reference, measured over the wire.
|
|
207
|
+
*/
|
|
208
|
+
const BOOL_TRUE = new Set(['1', 'on', 't', 'true', 'y', 'yes']);
|
|
209
|
+
const BOOL_FALSE = new Set(['0', 'off', 'f', 'false', 'n', 'no']);
|
|
181
210
|
/** i64, which is the width pydantic parses an int into before it complains about size. */
|
|
182
211
|
const I64_MAX = 9223372036854775807n;
|
|
183
212
|
const I64_MIN = -9223372036854775808n;
|
|
@@ -258,6 +287,45 @@ function checkField(spec, value) {
|
|
|
258
287
|
}
|
|
259
288
|
return bad.length > 0 ? bad : { value };
|
|
260
289
|
}
|
|
290
|
+
case 'bool': {
|
|
291
|
+
const parsing = () => [
|
|
292
|
+
{
|
|
293
|
+
loc: spec.name,
|
|
294
|
+
type: 'bool_parsing',
|
|
295
|
+
msg: 'Input should be a valid boolean, unable to interpret input',
|
|
296
|
+
input: value,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
const wrongType = () => [
|
|
300
|
+
{ loc: spec.name, type: 'bool_type', msg: 'Input should be a valid boolean', input: value },
|
|
301
|
+
];
|
|
302
|
+
if (value.t === 'bool')
|
|
303
|
+
return { value };
|
|
304
|
+
if (value.t === 'int') {
|
|
305
|
+
if (value.v === 0n || value.v === 1n)
|
|
306
|
+
return { value: { t: 'bool', v: value.v === 1n } };
|
|
307
|
+
return parsing();
|
|
308
|
+
}
|
|
309
|
+
if (value.t === 'float') {
|
|
310
|
+
// A FRACTIONAL float is `bool_type` and an out-of-range integral one is
|
|
311
|
+
// `bool_parsing`. Measured, and not a distinction anyone would guess: `0.5` and
|
|
312
|
+
// `2.0` are both floats and they refuse under different frames.
|
|
313
|
+
if (!Number.isInteger(value.v))
|
|
314
|
+
return wrongType();
|
|
315
|
+
if (value.v === 0 || value.v === 1)
|
|
316
|
+
return { value: { t: 'bool', v: value.v === 1 } };
|
|
317
|
+
return parsing();
|
|
318
|
+
}
|
|
319
|
+
if (value.t === 'str') {
|
|
320
|
+
const key = value.v.toLowerCase();
|
|
321
|
+
if (BOOL_TRUE.has(key))
|
|
322
|
+
return { value: { t: 'bool', v: true } };
|
|
323
|
+
if (BOOL_FALSE.has(key))
|
|
324
|
+
return { value: { t: 'bool', v: false } };
|
|
325
|
+
return parsing();
|
|
326
|
+
}
|
|
327
|
+
return wrongType();
|
|
328
|
+
}
|
|
261
329
|
case 'int': {
|
|
262
330
|
const checked = checkInt(spec.name, value);
|
|
263
331
|
if (!('value' in checked))
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import * as repomap from '../repomap.js';
|
|
2
3
|
import { EventLog } from '../eventlog.js';
|
|
3
4
|
import type { Memory } from '../memory/component.js';
|
|
4
5
|
import type { RawStdioTransport } from './transport.js';
|
|
5
6
|
/**
|
|
6
|
-
* The
|
|
7
|
+
* The thirteen, in the order `build_server` lists them — which is the order `tools/list` emits.
|
|
7
8
|
*
|
|
8
9
|
* `bantamkit_status` went LAST rather than first, `memory_compact` after it rather than
|
|
9
|
-
* beside `memory_save` where a reader would look for it, `bantamkit_read` after that
|
|
10
|
-
* `skill_audit` after that,
|
|
11
|
-
*
|
|
12
|
-
* every existing declaration
|
|
10
|
+
* beside `memory_save` where a reader would look for it, `bantamkit_read` after that,
|
|
11
|
+
* `skill_audit` after that, `memory_dream` after that and `repo_map` after that, for the
|
|
12
|
+
* same reason the reference appends all six: registration order IS the served order, and
|
|
13
|
+
* appending is the only edit that leaves the other twelve where every existing declaration
|
|
14
|
+
* says they are.
|
|
13
15
|
*/
|
|
14
|
-
export declare const MCP_TOOLS: readonly ["memory_save", "memory_recall", "validate_json", "shiftwork_clock_in", "shiftwork_clock_out", "shiftwork_status", "build_identity", "bantamkit_status", "memory_compact", "bantamkit_read", "skill_audit"];
|
|
16
|
+
export declare const MCP_TOOLS: readonly ["memory_save", "memory_recall", "validate_json", "shiftwork_clock_in", "shiftwork_clock_out", "shiftwork_status", "build_identity", "bantamkit_status", "memory_compact", "bantamkit_read", "skill_audit", "memory_dream", "repo_map"];
|
|
15
17
|
/**
|
|
16
18
|
* The advertisement for one tool, out of its manifest, with the surface checked.
|
|
17
19
|
*
|
|
@@ -28,6 +30,34 @@ export declare const MCP_TOOLS: readonly ["memory_save", "memory_recall", "valid
|
|
|
28
30
|
* that detects drift. Registered here, not fixed here; `runtime-py` is the reference.
|
|
29
31
|
*/
|
|
30
32
|
export declare function fromManifest(name: string): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* The last paragraph of every `repo_map` reply, refusal excepted. FIXED AND MANDATORY, and
|
|
35
|
+
* byte-identical to the reference's `REPO_MAP_TAIL`.
|
|
36
|
+
*
|
|
37
|
+
* Roadmap row 10's build gate was "build only after #4 shows discovery tokens dominate",
|
|
38
|
+
* and #4 REFUTED it: discovery is 0.114 % of real prompt tokens because 97.8 % of the bill
|
|
39
|
+
* is `cache_read`. The feature ships on an explicit ruling to build it anyway, as a
|
|
40
|
+
* PRECISION feature. A surface that let a caller believe the map is a token saving would
|
|
41
|
+
* say the one thing the measurement forbids, so the refutation travels with every answer
|
|
42
|
+
* rather than living only in a doc nobody reads at call time.
|
|
43
|
+
*
|
|
44
|
+
* The second sentence is the budget's unit, for the same reason: `DEFAULT_BUDGET = 4000` is
|
|
45
|
+
* "1 K tokens" only at the char/4 convention, whose error bar is unmeasured because
|
|
46
|
+
* measuring it needs the tokenizer the pure-node ruling forbids.
|
|
47
|
+
*/
|
|
48
|
+
export declare const REPO_MAP_TAIL: string;
|
|
49
|
+
/** What a listing says when there is nothing to list. The reference's `REPO_MAP_EMPTY`. */
|
|
50
|
+
export declare const REPO_MAP_EMPTY = "(nothing listed: no file under this root scanned into a definition)";
|
|
51
|
+
/**
|
|
52
|
+
* The `repo_map` tool's prose, byte for byte, from the structured result.
|
|
53
|
+
*
|
|
54
|
+
* Split out of the handler for the same reason the reference splits `repo_map_reply` out of
|
|
55
|
+
* its own: one shape to reproduce rather than a format string buried in a `case`, and the
|
|
56
|
+
* `repomap` conformance suite compares the rendered reply without standing up a server. No
|
|
57
|
+
* float is ever rendered here — `repomap`'s trap (8); every number on the header line is an
|
|
58
|
+
* integer, and `String(1.0)` is `'1'` where `repr(1.0)` is `'1.0'`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function repoMapReply(result: repomap.RepoMap): string;
|
|
31
61
|
/**
|
|
32
62
|
* Assemble the MCP server around one Memory instance (the per-person state).
|
|
33
63
|
*
|
package/dist/mcp/server.js
CHANGED
|
@@ -31,6 +31,7 @@ import { AssetNotFound, assetsRoot, loadSkill, loadToolAsset } from '../assets.j
|
|
|
31
31
|
import { BantamError } from '../errors.js';
|
|
32
32
|
import { bantamkitReadUnknownPart, documentError, documentManifest, documentOffsetPastEnd, documentPage, schemaError, toolFailed, } from '../contract.js';
|
|
33
33
|
import * as docread from '../docread.js';
|
|
34
|
+
import * as repomap from '../repomap.js';
|
|
34
35
|
import * as skillaudit from '../skillaudit.js';
|
|
35
36
|
import { EventLog } from '../eventlog.js';
|
|
36
37
|
import { PyOSError, asPyOSError, pyReadText } from '../memory/pyfs.js';
|
|
@@ -41,13 +42,14 @@ import { ARG_MODELS, PyValidationFailure, validateArguments } from './pyargs.js'
|
|
|
41
42
|
import { sdkJson } from './sdkjson.js';
|
|
42
43
|
import { degradedConditions, degradedNotice, SERVED_PROMPTS, SERVED_RESOURCE_TEMPLATES, statusReport, STATUS_NAME, STATUS_PROMPT_DESCRIPTION, STATUS_PROMPT_TAIL, STATUS_PROMPT_TITLE, } from './status.js';
|
|
43
44
|
/**
|
|
44
|
-
* The
|
|
45
|
+
* The thirteen, in the order `build_server` lists them — which is the order `tools/list` emits.
|
|
45
46
|
*
|
|
46
47
|
* `bantamkit_status` went LAST rather than first, `memory_compact` after it rather than
|
|
47
|
-
* beside `memory_save` where a reader would look for it, `bantamkit_read` after that
|
|
48
|
-
* `skill_audit` after that,
|
|
49
|
-
*
|
|
50
|
-
* every existing declaration
|
|
48
|
+
* beside `memory_save` where a reader would look for it, `bantamkit_read` after that,
|
|
49
|
+
* `skill_audit` after that, `memory_dream` after that and `repo_map` after that, for the
|
|
50
|
+
* same reason the reference appends all six: registration order IS the served order, and
|
|
51
|
+
* appending is the only edit that leaves the other twelve where every existing declaration
|
|
52
|
+
* says they are.
|
|
51
53
|
*/
|
|
52
54
|
export const MCP_TOOLS = [
|
|
53
55
|
'memory_save',
|
|
@@ -61,6 +63,8 @@ export const MCP_TOOLS = [
|
|
|
61
63
|
'memory_compact',
|
|
62
64
|
'bantamkit_read',
|
|
63
65
|
'skill_audit',
|
|
66
|
+
'memory_dream',
|
|
67
|
+
'repo_map',
|
|
64
68
|
];
|
|
65
69
|
/**
|
|
66
70
|
* The advertisement for one tool, out of its manifest, with the surface checked.
|
|
@@ -147,6 +151,65 @@ const asText = (value) => (value && value.t === 'str' ? value.v : '');
|
|
|
147
151
|
function asInt(value) {
|
|
148
152
|
return value && value.t === 'int' ? Number(value.v) : null;
|
|
149
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The errno family `Path.exists()` and `Path.is_dir()` answer `False` for instead of
|
|
156
|
+
* raising — CPython `pathlib`'s `_IGNORED_ERRNOS`, spelled as Node's `code` strings.
|
|
157
|
+
*
|
|
158
|
+
* Not a guess and not a superset: anything OUTSIDE this set makes both predicates raise on
|
|
159
|
+
* the reference, so it must reach `recordRaise` here too. A `statSync` wrapped in a bare
|
|
160
|
+
* `catch { }` would turn a permission failure into "no such directory", which is a
|
|
161
|
+
* different sentence about a different problem.
|
|
162
|
+
*/
|
|
163
|
+
const EXISTS_IGNORED = new Set(['ENOENT', 'ENOTDIR', 'EBADF', 'ELOOP']);
|
|
164
|
+
/**
|
|
165
|
+
* The last paragraph of every `repo_map` reply, refusal excepted. FIXED AND MANDATORY, and
|
|
166
|
+
* byte-identical to the reference's `REPO_MAP_TAIL`.
|
|
167
|
+
*
|
|
168
|
+
* Roadmap row 10's build gate was "build only after #4 shows discovery tokens dominate",
|
|
169
|
+
* and #4 REFUTED it: discovery is 0.114 % of real prompt tokens because 97.8 % of the bill
|
|
170
|
+
* is `cache_read`. The feature ships on an explicit ruling to build it anyway, as a
|
|
171
|
+
* PRECISION feature. A surface that let a caller believe the map is a token saving would
|
|
172
|
+
* say the one thing the measurement forbids, so the refutation travels with every answer
|
|
173
|
+
* rather than living only in a doc nobody reads at call time.
|
|
174
|
+
*
|
|
175
|
+
* The second sentence is the budget's unit, for the same reason: `DEFAULT_BUDGET = 4000` is
|
|
176
|
+
* "1 K tokens" only at the char/4 convention, whose error bar is unmeasured because
|
|
177
|
+
* measuring it needs the tokenizer the pure-node ruling forbids.
|
|
178
|
+
*/
|
|
179
|
+
export const REPO_MAP_TAIL = 'This is a precision pass, not a token saving: this feature\'s build gate was REFUTED ' +
|
|
180
|
+
'by measurement — discovery is 0.114% of real prompt tokens, because 97.8% of the ' +
|
|
181
|
+
'bill is cache_read — so a map does not make a session cheaper. What it buys is the ' +
|
|
182
|
+
'right file found sooner.\n' +
|
|
183
|
+
'The budget above is UTF-8 BYTES of listing, not tokens: neither runtime carries a ' +
|
|
184
|
+
'model tokenizer and this tool will not pretend to one.';
|
|
185
|
+
/** What a listing says when there is nothing to list. The reference's `REPO_MAP_EMPTY`. */
|
|
186
|
+
export const REPO_MAP_EMPTY = '(nothing listed: no file under this root scanned into a definition)';
|
|
187
|
+
/**
|
|
188
|
+
* The `repo_map` tool's prose, byte for byte, from the structured result.
|
|
189
|
+
*
|
|
190
|
+
* Split out of the handler for the same reason the reference splits `repo_map_reply` out of
|
|
191
|
+
* its own: one shape to reproduce rather than a format string buried in a `case`, and the
|
|
192
|
+
* `repomap` conformance suite compares the rendered reply without standing up a server. No
|
|
193
|
+
* float is ever rendered here — `repomap`'s trap (8); every number on the header line is an
|
|
194
|
+
* integer, and `String(1.0)` is `'1'` where `repr(1.0)` is `'1.0'`.
|
|
195
|
+
*/
|
|
196
|
+
export function repoMapReply(result) {
|
|
197
|
+
const focus = result.focus.length > 0
|
|
198
|
+
? result.focus.join(', ')
|
|
199
|
+
: '(none) — plain centrality over the whole tree';
|
|
200
|
+
const head = `repo map: ${result.nodes} files scanned, ${result.definitions} definitions, ` +
|
|
201
|
+
`${result.edges} edges.\n` +
|
|
202
|
+
`focus: ${focus}\n` +
|
|
203
|
+
`budget: ${result.budget} UTF-8 bytes; listing ${result.listingBytes} bytes; ` +
|
|
204
|
+
`rendered ${result.filesRendered} files, ` +
|
|
205
|
+
`${result.definitionsRendered} definitions.`;
|
|
206
|
+
const body = result.text !== '' ? result.text : REPO_MAP_EMPTY;
|
|
207
|
+
return `${head}\n\n${body}\n\n${REPO_MAP_TAIL}`;
|
|
208
|
+
}
|
|
209
|
+
/** `bool | None` after validation: the tagged bool, or `null` for the default. */
|
|
210
|
+
function asBool(value) {
|
|
211
|
+
return value && value.t === 'bool' ? value.v : null;
|
|
212
|
+
}
|
|
150
213
|
/**
|
|
151
214
|
* Run `body` and, if it throws, record the exception's TYPE before letting it fly on.
|
|
152
215
|
*
|
|
@@ -448,6 +511,29 @@ function runTool(name, memory, args, version, log, documents) {
|
|
|
448
511
|
});
|
|
449
512
|
return { value: { t: 'str', v: noted(outcome.reply) }, wrapped: true };
|
|
450
513
|
}
|
|
514
|
+
case 'memory_dream': {
|
|
515
|
+
// Consolidate what the project layer and the machine-wide profile layer both hold
|
|
516
|
+
// under one name (job45, roadmap row 5). `dryRun` DEFAULTS TO TRUE and the default
|
|
517
|
+
// lives HERE rather than in the component, exactly as the reference's handler carries
|
|
518
|
+
// it: a client that omits the argument sends nothing, `asBool` answers `null`, and
|
|
519
|
+
// turning that into the SAFE answer is this handler's job. It is the only tool on
|
|
520
|
+
// this surface that writes into the user's home directory, and the only one whose
|
|
521
|
+
// effect is machine-wide — a fact archived out of the profile store stops answering
|
|
522
|
+
// for every other project on this machine with no store of its own.
|
|
523
|
+
//
|
|
524
|
+
// THE RECORD IS A DECISION. `DreamOutcome.status` is read off `DreamResult.applied`,
|
|
525
|
+
// `.overBudget` and `.changes` — never off the reply, whose prose is allowed to
|
|
526
|
+
// improve without moving a single byte of the event log.
|
|
527
|
+
const dryRun = asBool(args.get('dry_run'));
|
|
528
|
+
const outcome = recordRaise(log, 'memory_dream', () => memory.dreamOutcome(dryRun === null ? true : dryRun));
|
|
529
|
+
log.record('memory_dream', outcome.status, {
|
|
530
|
+
absolutised: outcome.absolutised,
|
|
531
|
+
consumed: outcome.consumed,
|
|
532
|
+
dry_run: outcome.dryRun,
|
|
533
|
+
merged: outcome.merged,
|
|
534
|
+
});
|
|
535
|
+
return { value: { t: 'str', v: noted(outcome.reply) }, wrapped: true };
|
|
536
|
+
}
|
|
451
537
|
case 'bantamkit_read': {
|
|
452
538
|
// The reader on the MCP surface (job43): `docread` digests, `contract` words it. The
|
|
453
539
|
// handler makes the SAME `contract` calls the reference's `bantamkit_read` makes,
|
|
@@ -645,6 +731,67 @@ function runTool(name, memory, args, version, log, documents) {
|
|
|
645
731
|
return { value: { t: 'str', v: noted(result.asJson()) }, wrapped: true };
|
|
646
732
|
});
|
|
647
733
|
}
|
|
734
|
+
case 'repo_map': {
|
|
735
|
+
// The ranked definition map on the MCP surface (job45 row 10): `repomap` measures,
|
|
736
|
+
// this serves it. Byte for byte the reference's `repo_map` handler.
|
|
737
|
+
//
|
|
738
|
+
// THE THREE REFUSALS LIVE HERE AND NOT IN `repomap.ts`, and that is deliberate.
|
|
739
|
+
// `repoMap()` over a root that does not exist answers an EMPTY map on both runtimes
|
|
740
|
+
// — `os.walk` yields nothing for a missing directory and `walkSources`' `readdirSync`
|
|
741
|
+
// catch does the same — which is the right answer for a library and the wrong one for
|
|
742
|
+
// a tool: a caller who typed the path wrong would be told the tree holds no source.
|
|
743
|
+
// So the argument checks are the SURFACE's, and the engine J45-9/J45-10 proved
|
|
744
|
+
// byte-identical is not touched.
|
|
745
|
+
//
|
|
746
|
+
// THE `statSync` ARM IS `Path.exists()` / `Path.is_dir()`, NOT A SHORTCUT FOR THEM.
|
|
747
|
+
// Both pathlib predicates SWALLOW the not-here errno family and re-raise anything
|
|
748
|
+
// else, so `EXISTS_IGNORED` below is that family spelled out: a dangling symlink and
|
|
749
|
+
// `a-file.py/sub` are "no such directory" on both runtimes, while a permission
|
|
750
|
+
// failure flies to `recordRaise` rather than being dressed up as a missing tree.
|
|
751
|
+
//
|
|
752
|
+
// THE RECORD IS A DECISION AND HOLDS NO PATH. `root` is what the operator typed and
|
|
753
|
+
// `focus` is the name of the file they are editing; neither is a decision this
|
|
754
|
+
// handler made, so neither is written down. The counts are.
|
|
755
|
+
const root = asText(args.get('root'));
|
|
756
|
+
const focusArg = args.get('focus');
|
|
757
|
+
const focus = focusArg !== undefined && focusArg.t === 'list'
|
|
758
|
+
? focusArg.v.map((item) => (item.t === 'str' ? item.v : ''))
|
|
759
|
+
: [];
|
|
760
|
+
const budgetArg = args.get('budget');
|
|
761
|
+
const budget = budgetArg !== undefined && budgetArg.t === 'int' ? Number(budgetArg.v) : repomap.DEFAULT_BUDGET;
|
|
762
|
+
return recordRaise(log, 'repo_map', () => {
|
|
763
|
+
const refuse = (detail) => {
|
|
764
|
+
log.record('repo_map', 'refused');
|
|
765
|
+
return { value: { t: 'str', v: noted(toolFailed('repo_map', detail)) }, wrapped: true };
|
|
766
|
+
};
|
|
767
|
+
if (root === '')
|
|
768
|
+
return refuse('root must not be empty; name the directory to map');
|
|
769
|
+
if (budget < 0)
|
|
770
|
+
return refuse(`budget must not be negative; got ${budget}`);
|
|
771
|
+
let stat = null;
|
|
772
|
+
try {
|
|
773
|
+
stat = statSync(root);
|
|
774
|
+
}
|
|
775
|
+
catch (e) {
|
|
776
|
+
const code = e?.code;
|
|
777
|
+
if (!EXISTS_IGNORED.has(String(code)))
|
|
778
|
+
throw asPyOSError(e, root);
|
|
779
|
+
}
|
|
780
|
+
if (stat === null)
|
|
781
|
+
return refuse(`no such directory: ${root}`);
|
|
782
|
+
if (!stat.isDirectory())
|
|
783
|
+
return refuse(`${root} is a file, not a directory to map`);
|
|
784
|
+
const result = repomap.repoMap(root, { focus, budget });
|
|
785
|
+
log.record('repo_map', 'mapped', {
|
|
786
|
+
definitions: result.definitions,
|
|
787
|
+
edges: result.edges,
|
|
788
|
+
files_rendered: result.filesRendered,
|
|
789
|
+
listing_bytes: result.listingBytes,
|
|
790
|
+
nodes: result.nodes,
|
|
791
|
+
});
|
|
792
|
+
return { value: { t: 'str', v: noted(repoMapReply(result)) }, wrapped: true };
|
|
793
|
+
});
|
|
794
|
+
}
|
|
648
795
|
default:
|
|
649
796
|
// `tool_manager.call_tool` raises `ToolError(f"Unknown tool: {name}")`, which the
|
|
650
797
|
// handler turns into an isError result rather than a JSON-RPC error.
|
package/dist/memory/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `docs/memory.md` states, as a design decision, that `lint`, `archived` and `restore`
|
|
6
6
|
* are **not** agent tools — "lifecycle is an operator decision, not a model decision" —
|
|
7
|
-
* and the
|
|
7
|
+
* and the thirteen-tool surface holds to it; `compact` is the one exception since job42
|
|
8
8
|
* (`memory_compact`, the on-refusal path the model reaches itself). `runtime-py` made that
|
|
9
9
|
* position coherent in August 2026 by giving the operator `python -m bantamkit.memory`.
|
|
10
10
|
* `runtime-ts` did not. MEASURED 2026-08-24 before this file existed: `node dist/cli.js -h`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type StoreBinding } from './layers.js';
|
|
2
2
|
import { MemoryStore } from './store.js';
|
|
3
|
+
import type { DreamResult } from './dream.js';
|
|
3
4
|
/**
|
|
4
5
|
* The model's spelling adapted to the store's contract: lowercase, `_`/space -> `-`.
|
|
5
6
|
*
|
|
@@ -97,6 +98,30 @@ export interface RecallOutcome {
|
|
|
97
98
|
readonly source: string | null;
|
|
98
99
|
readonly unreadable: number;
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* What `dream` DID, beside the sentence it says about it.
|
|
103
|
+
*
|
|
104
|
+
* The same seam as `SaveOutcome`, `RecallOutcome` and `CompactOutcome`: `status` is read off
|
|
105
|
+
* a decision the pass already made — `DreamResult.applied`, `.overBudget`, `.changes` — and
|
|
106
|
+
* never off the reply. `result` carries the whole diff for a caller that wants the numbers
|
|
107
|
+
* rather than the prose.
|
|
108
|
+
*
|
|
109
|
+
* `status` is one of `consolidated`, `previewed`, `nothing-to-consolidate`,
|
|
110
|
+
* `refused-budget`, `no-profile-layer`.
|
|
111
|
+
*/
|
|
112
|
+
export interface DreamOutcome {
|
|
113
|
+
readonly reply: string;
|
|
114
|
+
readonly status: string;
|
|
115
|
+
readonly dryRun: boolean;
|
|
116
|
+
readonly merged: number;
|
|
117
|
+
readonly consumed: number;
|
|
118
|
+
readonly absolutised: number;
|
|
119
|
+
readonly superseded: number;
|
|
120
|
+
readonly indexBefore: number;
|
|
121
|
+
readonly indexAfter: number;
|
|
122
|
+
readonly budget: number;
|
|
123
|
+
readonly result: DreamResult | null;
|
|
124
|
+
}
|
|
100
125
|
export declare class Memory {
|
|
101
126
|
readonly store: MemoryStore;
|
|
102
127
|
readonly k: number;
|
|
@@ -140,7 +165,7 @@ export declare class Memory {
|
|
|
140
165
|
* fact. The floor is the operator's configured default, not a constant, so a consumer who
|
|
141
166
|
* really wants top-1 says so once at construction.
|
|
142
167
|
*/
|
|
143
|
-
recall(query: string, k?: number | null): string;
|
|
168
|
+
recall(query: string, k?: number | null, minRatio?: number): string;
|
|
144
169
|
/**
|
|
145
170
|
* `recall`, carrying the walk it performed as numbers rather than as prose.
|
|
146
171
|
*
|
|
@@ -157,8 +182,18 @@ export declare class Memory {
|
|
|
157
182
|
* refuses to list contributes nothing and raises the `unreadable` count instead of being
|
|
158
183
|
* scored as empty; that distinction is the whole subject of `nothingToReport` below and
|
|
159
184
|
* must not be undone here.
|
|
185
|
+
*
|
|
186
|
+
* `minRatio` (roadmap #6) rides through to every layer's `MemoryStore.recall` unchanged,
|
|
187
|
+
* so the gate is measured against EACH LAYER's own best score and never across layers: a
|
|
188
|
+
* profile fact does not have to out-score the project store's top hit to be admitted,
|
|
189
|
+
* because the two stores are answering as two stores. Its default is
|
|
190
|
+
* `RECALL_MIN_SCORE_RATIO` = 0.0, which keeps every fact `recall` was going to return,
|
|
191
|
+
* and nothing on the tool path passes anything else today. A ratio outside `[0.0, 1.0]`
|
|
192
|
+
* raises out of the FIRST layer, which is the writable project store, so it surfaces as
|
|
193
|
+
* the error it is rather than as an `unreadable` count — the read-only-layer `catch`
|
|
194
|
+
* below would otherwise file a caller's bad argument as a corrupt grant.
|
|
160
195
|
*/
|
|
161
|
-
recallOutcome(query: string, k?: number | null): RecallOutcome;
|
|
196
|
+
recallOutcome(query: string, k?: number | null, minRatio?: number): RecallOutcome;
|
|
162
197
|
/**
|
|
163
198
|
* Archive the stalest facts until a fact as large as the biggest one kept will fit.
|
|
164
199
|
*
|
|
@@ -179,6 +214,42 @@ export declare class Memory {
|
|
|
179
214
|
* is — `MemoryStore.compact` builds it with `pyJoin`.
|
|
180
215
|
*/
|
|
181
216
|
compactOutcome(reserve?: number | null): CompactOutcome;
|
|
217
|
+
/** Consolidate what the project and profile layers hold under the same name. */
|
|
218
|
+
dream(dryRun?: boolean): string;
|
|
219
|
+
/**
|
|
220
|
+
* `dream`, with the decision it took carried beside the sentence it wrote.
|
|
221
|
+
*
|
|
222
|
+
* ONLY THE PROFILE LAYER IS CONSUMED. `layers` also carries read-only GRANTS, and a grant
|
|
223
|
+
* is another operator's store: consolidating a fact out of one is not this person's move
|
|
224
|
+
* to make, so `dream` never looks at them. The label is matched exactly (`profile`), never
|
|
225
|
+
* by prefix, because a grant is labelled `extra:<name>` and a prefix match on a directory
|
|
226
|
+
* called `profile-something` would reach one.
|
|
227
|
+
*
|
|
228
|
+
* A `Memory` constructed directly — not through `layered` — has no profile layer at all,
|
|
229
|
+
* and that is `no-profile-layer` rather than an error: there is nothing to consolidate
|
|
230
|
+
* ACROSS when only one layer is bound.
|
|
231
|
+
*
|
|
232
|
+
* `dryRun` DEFAULTS TO TRUE. This is the only op in this component that writes into the
|
|
233
|
+
* user's home directory, and it is the only one whose effect is machine-wide: a fact
|
|
234
|
+
* archived out of the profile store stops answering for every other project on this
|
|
235
|
+
* machine that has no store of its own. A destructive consolidation nobody can preview is
|
|
236
|
+
* not shippable, so the safe call is the short one.
|
|
237
|
+
*/
|
|
238
|
+
dreamOutcome(dryRun?: boolean): DreamOutcome;
|
|
239
|
+
/**
|
|
240
|
+
* The whole diff as prose: what merged, what was dated, what moved, what it cost.
|
|
241
|
+
*
|
|
242
|
+
* THE HONESTY SENTENCE AT THE END IS NOT DECORATION. Row 5 was planned as a token saving
|
|
243
|
+
* and J45-1 measured that it is not one: the profile store has no `index.md` on disk, its
|
|
244
|
+
* index is derived at read time, and nothing loads it into a prompt — so deduplicating it
|
|
245
|
+
* frees approximately zero prompt bytes. The value is that one ruling now has one copy
|
|
246
|
+
* instead of two that can disagree, and the reply says which of those two things the
|
|
247
|
+
* operator just bought.
|
|
248
|
+
*
|
|
249
|
+
* `formatFixed3` and not `toFixed(3)` for the two similarity figures: see the header of
|
|
250
|
+
* `dream.ts`. CPython's `.3f` rounds ties to EVEN and they are reachable.
|
|
251
|
+
*/
|
|
252
|
+
private static dreamReply;
|
|
182
253
|
/**
|
|
183
254
|
* `(index bytes, budget)` for the writable project store; bytes may be `null`.
|
|
184
255
|
*
|