bantamkit-mcp 0.29.2 → 0.31.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 +83 -2
- package/assets/pricing/default.json +7 -0
- package/assets/schemas/shiftwork-checkpoint.json +12 -1
- package/assets/tools/memory_dream.json +20 -0
- package/assets/tools/repo_map.json +44 -0
- package/assets/tools/shiftwork_clock_out.json +1 -1
- package/assets/tools/token_ledger.json +40 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +135 -4
- package/dist/eventlog.js +6 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/mcp/identity.d.ts +80 -0
- package/dist/mcp/identity.js +466 -4
- package/dist/mcp/pyargs.d.ts +1 -1
- package/dist/mcp/pyargs.js +78 -0
- package/dist/mcp/server.d.ts +37 -6
- package/dist/mcp/server.js +209 -5
- package/dist/mcp/status.d.ts +54 -10
- package/dist/mcp/status.js +86 -10
- 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 +101 -1
- package/dist/memory/store.js +178 -4
- package/dist/pricing.d.ts +100 -0
- package/dist/pricing.js +357 -0
- package/dist/repomap.d.ts +247 -0
- package/dist/repomap.js +1040 -0
- package/dist/selfupdate.d.ts +320 -0
- package/dist/selfupdate.js +594 -0
- package/dist/shiftwork.d.ts +5 -3
- package/dist/shiftwork.js +82 -4
- package/dist/tokenledger.d.ts +91 -0
- package/dist/tokenledger.js +372 -0
- package/package.json +1 -1
package/dist/mcp/pyargs.js
CHANGED
|
@@ -126,6 +126,38 @@ 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
|
+
},
|
|
151
|
+
// `token_ledger(root: str, model: str | None = None, prices: str | None = None)` on the
|
|
152
|
+
// reference (job46, AS-1(c)). All three are the strict `str` every other string field is —
|
|
153
|
+
// `123` is `string_type`, not a coerced `'123'` — and an explicit `null` is the default for
|
|
154
|
+
// the two optional ones. There is no `int` here at all, which is why this entry is the
|
|
155
|
+
// shortest on the surface: the tool's every number is READ off the corpus rather than
|
|
156
|
+
// passed in, so there is no lax-int arm to measure.
|
|
157
|
+
token_ledger: {
|
|
158
|
+
model: 'token_ledgerArguments',
|
|
159
|
+
fields: [req('root', 'str'), opt('model', 'str'), opt('prices', 'str')],
|
|
160
|
+
},
|
|
129
161
|
skill_audit: {
|
|
130
162
|
model: 'skill_auditArguments',
|
|
131
163
|
fields: [
|
|
@@ -178,6 +210,13 @@ function truncateRepr(text) {
|
|
|
178
210
|
tail += 1;
|
|
179
211
|
return `${decoder.decode(bytes.slice(0, head))}...${decoder.decode(bytes.slice(tail))}`;
|
|
180
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* The two closed sets pydantic-core matches a string against for a lax `bool`, lowercased.
|
|
215
|
+
*
|
|
216
|
+
* No stripping: `' true '` is `bool_parsing` on the reference, measured over the wire.
|
|
217
|
+
*/
|
|
218
|
+
const BOOL_TRUE = new Set(['1', 'on', 't', 'true', 'y', 'yes']);
|
|
219
|
+
const BOOL_FALSE = new Set(['0', 'off', 'f', 'false', 'n', 'no']);
|
|
181
220
|
/** i64, which is the width pydantic parses an int into before it complains about size. */
|
|
182
221
|
const I64_MAX = 9223372036854775807n;
|
|
183
222
|
const I64_MIN = -9223372036854775808n;
|
|
@@ -258,6 +297,45 @@ function checkField(spec, value) {
|
|
|
258
297
|
}
|
|
259
298
|
return bad.length > 0 ? bad : { value };
|
|
260
299
|
}
|
|
300
|
+
case 'bool': {
|
|
301
|
+
const parsing = () => [
|
|
302
|
+
{
|
|
303
|
+
loc: spec.name,
|
|
304
|
+
type: 'bool_parsing',
|
|
305
|
+
msg: 'Input should be a valid boolean, unable to interpret input',
|
|
306
|
+
input: value,
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
const wrongType = () => [
|
|
310
|
+
{ loc: spec.name, type: 'bool_type', msg: 'Input should be a valid boolean', input: value },
|
|
311
|
+
];
|
|
312
|
+
if (value.t === 'bool')
|
|
313
|
+
return { value };
|
|
314
|
+
if (value.t === 'int') {
|
|
315
|
+
if (value.v === 0n || value.v === 1n)
|
|
316
|
+
return { value: { t: 'bool', v: value.v === 1n } };
|
|
317
|
+
return parsing();
|
|
318
|
+
}
|
|
319
|
+
if (value.t === 'float') {
|
|
320
|
+
// A FRACTIONAL float is `bool_type` and an out-of-range integral one is
|
|
321
|
+
// `bool_parsing`. Measured, and not a distinction anyone would guess: `0.5` and
|
|
322
|
+
// `2.0` are both floats and they refuse under different frames.
|
|
323
|
+
if (!Number.isInteger(value.v))
|
|
324
|
+
return wrongType();
|
|
325
|
+
if (value.v === 0 || value.v === 1)
|
|
326
|
+
return { value: { t: 'bool', v: value.v === 1 } };
|
|
327
|
+
return parsing();
|
|
328
|
+
}
|
|
329
|
+
if (value.t === 'str') {
|
|
330
|
+
const key = value.v.toLowerCase();
|
|
331
|
+
if (BOOL_TRUE.has(key))
|
|
332
|
+
return { value: { t: 'bool', v: true } };
|
|
333
|
+
if (BOOL_FALSE.has(key))
|
|
334
|
+
return { value: { t: 'bool', v: false } };
|
|
335
|
+
return parsing();
|
|
336
|
+
}
|
|
337
|
+
return wrongType();
|
|
338
|
+
}
|
|
261
339
|
case 'int': {
|
|
262
340
|
const checked = checkInt(spec.name, value);
|
|
263
341
|
if (!('value' in checked))
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
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
|
-
*
|
|
10
|
+
* beside `memory_save` where a reader would look for it, `bantamkit_read` after that,
|
|
11
|
+
* `skill_audit` after that, `memory_dream` after that, `repo_map` after that and
|
|
12
|
+
* `token_ledger` after that, for the
|
|
13
|
+
* same reason the reference appends all six: registration order IS the served order, and
|
|
14
|
+
* appending is the only edit that leaves the other twelve where every existing declaration
|
|
15
|
+
* says they are.
|
|
13
16
|
*/
|
|
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"];
|
|
17
|
+
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", "token_ledger"];
|
|
15
18
|
/**
|
|
16
19
|
* The advertisement for one tool, out of its manifest, with the surface checked.
|
|
17
20
|
*
|
|
@@ -28,6 +31,34 @@ export declare const MCP_TOOLS: readonly ["memory_save", "memory_recall", "valid
|
|
|
28
31
|
* that detects drift. Registered here, not fixed here; `runtime-py` is the reference.
|
|
29
32
|
*/
|
|
30
33
|
export declare function fromManifest(name: string): Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* The last paragraph of every `repo_map` reply, refusal excepted. FIXED AND MANDATORY, and
|
|
36
|
+
* byte-identical to the reference's `REPO_MAP_TAIL`.
|
|
37
|
+
*
|
|
38
|
+
* Roadmap row 10's build gate was "build only after #4 shows discovery tokens dominate",
|
|
39
|
+
* and #4 REFUTED it: discovery is 0.114 % of real prompt tokens because 97.8 % of the bill
|
|
40
|
+
* is `cache_read`. The feature ships on an explicit ruling to build it anyway, as a
|
|
41
|
+
* PRECISION feature. A surface that let a caller believe the map is a token saving would
|
|
42
|
+
* say the one thing the measurement forbids, so the refutation travels with every answer
|
|
43
|
+
* rather than living only in a doc nobody reads at call time.
|
|
44
|
+
*
|
|
45
|
+
* The second sentence is the budget's unit, for the same reason: `DEFAULT_BUDGET = 4000` is
|
|
46
|
+
* "1 K tokens" only at the char/4 convention, whose error bar is unmeasured because
|
|
47
|
+
* measuring it needs the tokenizer the pure-node ruling forbids.
|
|
48
|
+
*/
|
|
49
|
+
export declare const REPO_MAP_TAIL: string;
|
|
50
|
+
/** What a listing says when there is nothing to list. The reference's `REPO_MAP_EMPTY`. */
|
|
51
|
+
export declare const REPO_MAP_EMPTY = "(nothing listed: no file under this root scanned into a definition)";
|
|
52
|
+
/**
|
|
53
|
+
* The `repo_map` tool's prose, byte for byte, from the structured result.
|
|
54
|
+
*
|
|
55
|
+
* Split out of the handler for the same reason the reference splits `repo_map_reply` out of
|
|
56
|
+
* its own: one shape to reproduce rather than a format string buried in a `case`, and the
|
|
57
|
+
* `repomap` conformance suite compares the rendered reply without standing up a server. No
|
|
58
|
+
* float is ever rendered here — `repomap`'s trap (8); every number on the header line is an
|
|
59
|
+
* integer, and `String(1.0)` is `'1'` where `repr(1.0)` is `'1.0'`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function repoMapReply(result: repomap.RepoMap): string;
|
|
31
62
|
/**
|
|
32
63
|
* Assemble the MCP server around one Memory instance (the per-person state).
|
|
33
64
|
*
|
package/dist/mcp/server.js
CHANGED
|
@@ -31,7 +31,10 @@ 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';
|
|
36
|
+
import { PriceTableError } from '../pricing.js';
|
|
37
|
+
import * as tokenledger from '../tokenledger.js';
|
|
35
38
|
import { EventLog } from '../eventlog.js';
|
|
36
39
|
import { PyOSError, asPyOSError, pyReadText } from '../memory/pyfs.js';
|
|
37
40
|
import { fromJs, parseJson, reprValue, toJs } from '../pyjson.js';
|
|
@@ -41,13 +44,15 @@ import { ARG_MODELS, PyValidationFailure, validateArguments } from './pyargs.js'
|
|
|
41
44
|
import { sdkJson } from './sdkjson.js';
|
|
42
45
|
import { degradedConditions, degradedNotice, SERVED_PROMPTS, SERVED_RESOURCE_TEMPLATES, statusReport, STATUS_NAME, STATUS_PROMPT_DESCRIPTION, STATUS_PROMPT_TAIL, STATUS_PROMPT_TITLE, } from './status.js';
|
|
43
46
|
/**
|
|
44
|
-
* The
|
|
47
|
+
* The thirteen, in the order `build_server` lists them — which is the order `tools/list` emits.
|
|
45
48
|
*
|
|
46
49
|
* `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
|
-
*
|
|
50
|
+
* beside `memory_save` where a reader would look for it, `bantamkit_read` after that,
|
|
51
|
+
* `skill_audit` after that, `memory_dream` after that, `repo_map` after that and
|
|
52
|
+
* `token_ledger` after that, for the
|
|
53
|
+
* same reason the reference appends all six: registration order IS the served order, and
|
|
54
|
+
* appending is the only edit that leaves the other twelve where every existing declaration
|
|
55
|
+
* says they are.
|
|
51
56
|
*/
|
|
52
57
|
export const MCP_TOOLS = [
|
|
53
58
|
'memory_save',
|
|
@@ -61,6 +66,9 @@ export const MCP_TOOLS = [
|
|
|
61
66
|
'memory_compact',
|
|
62
67
|
'bantamkit_read',
|
|
63
68
|
'skill_audit',
|
|
69
|
+
'memory_dream',
|
|
70
|
+
'repo_map',
|
|
71
|
+
'token_ledger',
|
|
64
72
|
];
|
|
65
73
|
/**
|
|
66
74
|
* The advertisement for one tool, out of its manifest, with the surface checked.
|
|
@@ -147,6 +155,65 @@ const asText = (value) => (value && value.t === 'str' ? value.v : '');
|
|
|
147
155
|
function asInt(value) {
|
|
148
156
|
return value && value.t === 'int' ? Number(value.v) : null;
|
|
149
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* The errno family `Path.exists()` and `Path.is_dir()` answer `False` for instead of
|
|
160
|
+
* raising — CPython `pathlib`'s `_IGNORED_ERRNOS`, spelled as Node's `code` strings.
|
|
161
|
+
*
|
|
162
|
+
* Not a guess and not a superset: anything OUTSIDE this set makes both predicates raise on
|
|
163
|
+
* the reference, so it must reach `recordRaise` here too. A `statSync` wrapped in a bare
|
|
164
|
+
* `catch { }` would turn a permission failure into "no such directory", which is a
|
|
165
|
+
* different sentence about a different problem.
|
|
166
|
+
*/
|
|
167
|
+
const EXISTS_IGNORED = new Set(['ENOENT', 'ENOTDIR', 'EBADF', 'ELOOP']);
|
|
168
|
+
/**
|
|
169
|
+
* The last paragraph of every `repo_map` reply, refusal excepted. FIXED AND MANDATORY, and
|
|
170
|
+
* byte-identical to the reference's `REPO_MAP_TAIL`.
|
|
171
|
+
*
|
|
172
|
+
* Roadmap row 10's build gate was "build only after #4 shows discovery tokens dominate",
|
|
173
|
+
* and #4 REFUTED it: discovery is 0.114 % of real prompt tokens because 97.8 % of the bill
|
|
174
|
+
* is `cache_read`. The feature ships on an explicit ruling to build it anyway, as a
|
|
175
|
+
* PRECISION feature. A surface that let a caller believe the map is a token saving would
|
|
176
|
+
* say the one thing the measurement forbids, so the refutation travels with every answer
|
|
177
|
+
* rather than living only in a doc nobody reads at call time.
|
|
178
|
+
*
|
|
179
|
+
* The second sentence is the budget's unit, for the same reason: `DEFAULT_BUDGET = 4000` is
|
|
180
|
+
* "1 K tokens" only at the char/4 convention, whose error bar is unmeasured because
|
|
181
|
+
* measuring it needs the tokenizer the pure-node ruling forbids.
|
|
182
|
+
*/
|
|
183
|
+
export const REPO_MAP_TAIL = 'This is a precision pass, not a token saving: this feature\'s build gate was REFUTED ' +
|
|
184
|
+
'by measurement — discovery is 0.114% of real prompt tokens, because 97.8% of the ' +
|
|
185
|
+
'bill is cache_read — so a map does not make a session cheaper. What it buys is the ' +
|
|
186
|
+
'right file found sooner.\n' +
|
|
187
|
+
'The budget above is UTF-8 BYTES of listing, not tokens: neither runtime carries a ' +
|
|
188
|
+
'model tokenizer and this tool will not pretend to one.';
|
|
189
|
+
/** What a listing says when there is nothing to list. The reference's `REPO_MAP_EMPTY`. */
|
|
190
|
+
export const REPO_MAP_EMPTY = '(nothing listed: no file under this root scanned into a definition)';
|
|
191
|
+
/**
|
|
192
|
+
* The `repo_map` tool's prose, byte for byte, from the structured result.
|
|
193
|
+
*
|
|
194
|
+
* Split out of the handler for the same reason the reference splits `repo_map_reply` out of
|
|
195
|
+
* its own: one shape to reproduce rather than a format string buried in a `case`, and the
|
|
196
|
+
* `repomap` conformance suite compares the rendered reply without standing up a server. No
|
|
197
|
+
* float is ever rendered here — `repomap`'s trap (8); every number on the header line is an
|
|
198
|
+
* integer, and `String(1.0)` is `'1'` where `repr(1.0)` is `'1.0'`.
|
|
199
|
+
*/
|
|
200
|
+
export function repoMapReply(result) {
|
|
201
|
+
const focus = result.focus.length > 0
|
|
202
|
+
? result.focus.join(', ')
|
|
203
|
+
: '(none) — plain centrality over the whole tree';
|
|
204
|
+
const head = `repo map: ${result.nodes} files scanned, ${result.definitions} definitions, ` +
|
|
205
|
+
`${result.edges} edges.\n` +
|
|
206
|
+
`focus: ${focus}\n` +
|
|
207
|
+
`budget: ${result.budget} UTF-8 bytes; listing ${result.listingBytes} bytes; ` +
|
|
208
|
+
`rendered ${result.filesRendered} files, ` +
|
|
209
|
+
`${result.definitionsRendered} definitions.`;
|
|
210
|
+
const body = result.text !== '' ? result.text : REPO_MAP_EMPTY;
|
|
211
|
+
return `${head}\n\n${body}\n\n${REPO_MAP_TAIL}`;
|
|
212
|
+
}
|
|
213
|
+
/** `bool | None` after validation: the tagged bool, or `null` for the default. */
|
|
214
|
+
function asBool(value) {
|
|
215
|
+
return value && value.t === 'bool' ? value.v : null;
|
|
216
|
+
}
|
|
150
217
|
/**
|
|
151
218
|
* Run `body` and, if it throws, record the exception's TYPE before letting it fly on.
|
|
152
219
|
*
|
|
@@ -448,6 +515,29 @@ function runTool(name, memory, args, version, log, documents) {
|
|
|
448
515
|
});
|
|
449
516
|
return { value: { t: 'str', v: noted(outcome.reply) }, wrapped: true };
|
|
450
517
|
}
|
|
518
|
+
case 'memory_dream': {
|
|
519
|
+
// Consolidate what the project layer and the machine-wide profile layer both hold
|
|
520
|
+
// under one name (job45, roadmap row 5). `dryRun` DEFAULTS TO TRUE and the default
|
|
521
|
+
// lives HERE rather than in the component, exactly as the reference's handler carries
|
|
522
|
+
// it: a client that omits the argument sends nothing, `asBool` answers `null`, and
|
|
523
|
+
// turning that into the SAFE answer is this handler's job. It is the only tool on
|
|
524
|
+
// this surface that writes into the user's home directory, and the only one whose
|
|
525
|
+
// effect is machine-wide — a fact archived out of the profile store stops answering
|
|
526
|
+
// for every other project on this machine with no store of its own.
|
|
527
|
+
//
|
|
528
|
+
// THE RECORD IS A DECISION. `DreamOutcome.status` is read off `DreamResult.applied`,
|
|
529
|
+
// `.overBudget` and `.changes` — never off the reply, whose prose is allowed to
|
|
530
|
+
// improve without moving a single byte of the event log.
|
|
531
|
+
const dryRun = asBool(args.get('dry_run'));
|
|
532
|
+
const outcome = recordRaise(log, 'memory_dream', () => memory.dreamOutcome(dryRun === null ? true : dryRun));
|
|
533
|
+
log.record('memory_dream', outcome.status, {
|
|
534
|
+
absolutised: outcome.absolutised,
|
|
535
|
+
consumed: outcome.consumed,
|
|
536
|
+
dry_run: outcome.dryRun,
|
|
537
|
+
merged: outcome.merged,
|
|
538
|
+
});
|
|
539
|
+
return { value: { t: 'str', v: noted(outcome.reply) }, wrapped: true };
|
|
540
|
+
}
|
|
451
541
|
case 'bantamkit_read': {
|
|
452
542
|
// The reader on the MCP surface (job43): `docread` digests, `contract` words it. The
|
|
453
543
|
// handler makes the SAME `contract` calls the reference's `bantamkit_read` makes,
|
|
@@ -645,6 +735,120 @@ function runTool(name, memory, args, version, log, documents) {
|
|
|
645
735
|
return { value: { t: 'str', v: noted(result.asJson()) }, wrapped: true };
|
|
646
736
|
});
|
|
647
737
|
}
|
|
738
|
+
case 'repo_map': {
|
|
739
|
+
// The ranked definition map on the MCP surface (job45 row 10): `repomap` measures,
|
|
740
|
+
// this serves it. Byte for byte the reference's `repo_map` handler.
|
|
741
|
+
//
|
|
742
|
+
// THE THREE REFUSALS LIVE HERE AND NOT IN `repomap.ts`, and that is deliberate.
|
|
743
|
+
// `repoMap()` over a root that does not exist answers an EMPTY map on both runtimes
|
|
744
|
+
// — `os.walk` yields nothing for a missing directory and `walkSources`' `readdirSync`
|
|
745
|
+
// catch does the same — which is the right answer for a library and the wrong one for
|
|
746
|
+
// a tool: a caller who typed the path wrong would be told the tree holds no source.
|
|
747
|
+
// So the argument checks are the SURFACE's, and the engine J45-9/J45-10 proved
|
|
748
|
+
// byte-identical is not touched.
|
|
749
|
+
//
|
|
750
|
+
// THE `statSync` ARM IS `Path.exists()` / `Path.is_dir()`, NOT A SHORTCUT FOR THEM.
|
|
751
|
+
// Both pathlib predicates SWALLOW the not-here errno family and re-raise anything
|
|
752
|
+
// else, so `EXISTS_IGNORED` below is that family spelled out: a dangling symlink and
|
|
753
|
+
// `a-file.py/sub` are "no such directory" on both runtimes, while a permission
|
|
754
|
+
// failure flies to `recordRaise` rather than being dressed up as a missing tree.
|
|
755
|
+
//
|
|
756
|
+
// THE RECORD IS A DECISION AND HOLDS NO PATH. `root` is what the operator typed and
|
|
757
|
+
// `focus` is the name of the file they are editing; neither is a decision this
|
|
758
|
+
// handler made, so neither is written down. The counts are.
|
|
759
|
+
const root = asText(args.get('root'));
|
|
760
|
+
const focusArg = args.get('focus');
|
|
761
|
+
const focus = focusArg !== undefined && focusArg.t === 'list'
|
|
762
|
+
? focusArg.v.map((item) => (item.t === 'str' ? item.v : ''))
|
|
763
|
+
: [];
|
|
764
|
+
const budgetArg = args.get('budget');
|
|
765
|
+
const budget = budgetArg !== undefined && budgetArg.t === 'int' ? Number(budgetArg.v) : repomap.DEFAULT_BUDGET;
|
|
766
|
+
return recordRaise(log, 'repo_map', () => {
|
|
767
|
+
const refuse = (detail) => {
|
|
768
|
+
log.record('repo_map', 'refused');
|
|
769
|
+
return { value: { t: 'str', v: noted(toolFailed('repo_map', detail)) }, wrapped: true };
|
|
770
|
+
};
|
|
771
|
+
if (root === '')
|
|
772
|
+
return refuse('root must not be empty; name the directory to map');
|
|
773
|
+
if (budget < 0)
|
|
774
|
+
return refuse(`budget must not be negative; got ${budget}`);
|
|
775
|
+
let stat = null;
|
|
776
|
+
try {
|
|
777
|
+
stat = statSync(root);
|
|
778
|
+
}
|
|
779
|
+
catch (e) {
|
|
780
|
+
const code = e?.code;
|
|
781
|
+
if (!EXISTS_IGNORED.has(String(code)))
|
|
782
|
+
throw asPyOSError(e, root);
|
|
783
|
+
}
|
|
784
|
+
if (stat === null)
|
|
785
|
+
return refuse(`no such directory: ${root}`);
|
|
786
|
+
if (!stat.isDirectory())
|
|
787
|
+
return refuse(`${root} is a file, not a directory to map`);
|
|
788
|
+
const result = repomap.repoMap(root, { focus, budget });
|
|
789
|
+
log.record('repo_map', 'mapped', {
|
|
790
|
+
definitions: result.definitions,
|
|
791
|
+
edges: result.edges,
|
|
792
|
+
files_rendered: result.filesRendered,
|
|
793
|
+
listing_bytes: result.listingBytes,
|
|
794
|
+
nodes: result.nodes,
|
|
795
|
+
});
|
|
796
|
+
return { value: { t: 'str', v: noted(repoMapReply(result)) }, wrapped: true };
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
case 'token_ledger': {
|
|
800
|
+
// The transcript ledger on the MCP surface (job46, AS-1(c)): `tokenledger` measures,
|
|
801
|
+
// this serves it. Byte for byte the reference's `token_ledger` handler.
|
|
802
|
+
//
|
|
803
|
+
// THE REPLY IS A JSON DOCUMENT AND NOT PROSE, the same shape and for the same reason as
|
|
804
|
+
// `skill_audit` above: a caller comparing `totals` before and after a change, or
|
|
805
|
+
// deciding whether `omissions` explain a total that looks too small, has to read
|
|
806
|
+
// numbers rather than parse a sentence back out of English. `asJson` is the reply
|
|
807
|
+
// verbatim and the only string this layer authors is the refusal.
|
|
808
|
+
//
|
|
809
|
+
// THE FOUR REFUSALS ARE ARGUMENT FAILURES — an empty `root`, a `root` that is missing,
|
|
810
|
+
// a `root` that is a file, an empty `model` — so they go through `toolFailed`, and
|
|
811
|
+
// unlike `repo_map` above they live in the MODULE rather than here: `read()` over a
|
|
812
|
+
// missing root does not answer an empty ledger that a caller could mistake for a real
|
|
813
|
+
// one, it refuses, so there is nothing for this layer to add.
|
|
814
|
+
//
|
|
815
|
+
// `PriceTableError` is caught BESIDE `TokenLedgerError` and is not the same kind of
|
|
816
|
+
// thing: it is an operator configuration fault reached only when `model` names a price
|
|
817
|
+
// table that will not load. It is still an argument failure from the CALLER's side —
|
|
818
|
+
// it names the path they passed — so it is refused rather than crashing the request.
|
|
819
|
+
//
|
|
820
|
+
// THE RECORD IS A DECISION, NEVER A REPLY. `read` carries the four counts the host
|
|
821
|
+
// cannot see and `refused` carries nothing at all, and no token count is recorded
|
|
822
|
+
// either: the event log is a record of what this server DID, and the numbers are the
|
|
823
|
+
// reply.
|
|
824
|
+
const root = asText(args.get('root'));
|
|
825
|
+
const modelArg = args.get('model');
|
|
826
|
+
const model = modelArg !== undefined && modelArg.t === 'str' ? modelArg.v : null;
|
|
827
|
+
const pricesArg = args.get('prices');
|
|
828
|
+
const prices = pricesArg !== undefined && pricesArg.t === 'str' ? pricesArg.v : null;
|
|
829
|
+
return recordRaise(log, 'token_ledger', () => {
|
|
830
|
+
let result;
|
|
831
|
+
try {
|
|
832
|
+
result = tokenledger.read(root, { model, prices });
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
// `except (tokenledger.TokenLedgerError, PriceTableError, OSError)`. Both classes
|
|
836
|
+
// extend `BantamError` and both already carry the reference's own sentence.
|
|
837
|
+
if (!(e instanceof tokenledger.TokenLedgerError) && !(e instanceof PriceTableError)) {
|
|
838
|
+
throw e;
|
|
839
|
+
}
|
|
840
|
+
log.record('token_ledger', 'refused');
|
|
841
|
+
return { value: { t: 'str', v: noted(toolFailed('token_ledger', e)) }, wrapped: true };
|
|
842
|
+
}
|
|
843
|
+
log.record('token_ledger', 'read', {
|
|
844
|
+
transcripts: result.transcripts,
|
|
845
|
+
lines: result.lines,
|
|
846
|
+
requests: result.requests,
|
|
847
|
+
sessions: result.sessions.length,
|
|
848
|
+
});
|
|
849
|
+
return { value: { t: 'str', v: noted(tokenledger.asJson(result)) }, wrapped: true };
|
|
850
|
+
});
|
|
851
|
+
}
|
|
648
852
|
default:
|
|
649
853
|
// `tool_manager.call_tool` raises `ToolError(f"Unknown tool: {name}")`, which the
|
|
650
854
|
// handler turns into an isError result rather than a JSON-RPC error.
|
package/dist/mcp/status.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { EventLog } from '../eventlog.js';
|
|
2
2
|
import { Memory } from '../memory/component.js';
|
|
3
|
+
import { INDEX_PRESSURE_PERCENT } from '../memory/store.js';
|
|
4
|
+
import type { Install } from './identity.js';
|
|
3
5
|
/**
|
|
4
6
|
* The tool AND the prompt answer to this one name. Deliberately the same word: the operator
|
|
5
7
|
* who read `bantamkit_status` in a footer must find it in their prompt menu without
|
|
@@ -31,14 +33,13 @@ export declare const STATUS_PROMPT_TAIL: string;
|
|
|
31
33
|
export declare const SERVED_PROMPTS = 1;
|
|
32
34
|
export declare const SERVED_RESOURCE_TEMPLATES = 2;
|
|
33
35
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* on opposite sides of the line through a float they rounded differently.
|
|
36
|
+
* `INDEX_PRESSURE_PERCENT` is re-exported here, where it used to be DEFINED, so that
|
|
37
|
+
* `import { INDEX_PRESSURE_PERCENT } from './mcp/status.js'` keeps resolving. It moved down
|
|
38
|
+
* to `memory/store.ts` in job46 (J46-5, mirroring the reference's J46-4):
|
|
39
|
+
* `MemoryStore.compact` is the remedy the sentence below names, and it cannot clear a warning
|
|
40
|
+
* whose line it cannot see. The comment that says why the number is 90 moved with it.
|
|
40
41
|
*/
|
|
41
|
-
export
|
|
42
|
+
export { INDEX_PRESSURE_PERCENT };
|
|
42
43
|
/**
|
|
43
44
|
* One thing that is wrong, carried in the two forms the two surfaces need.
|
|
44
45
|
*
|
|
@@ -135,6 +136,20 @@ export declare function unreadableLayerCondition(memory: Memory): Condition | nu
|
|
|
135
136
|
* not a divergence and is not fixed here; it is registered in `docs/porting.md`. The sentence
|
|
136
137
|
* says "archive or shorten facts", which is what the operator has to do either way.
|
|
137
138
|
*
|
|
139
|
+
* AMENDMENT (job46, J46-5): THE PARAGRAPH ABOVE IS A RECORD OF A DEFECT THAT IS NOW CLOSED,
|
|
140
|
+
* and it is left standing because it is the measurement that motivated the fix. What it says
|
|
141
|
+
* was true until this job: the band was real, and on this machine's own project store
|
|
142
|
+
* (101 facts, index.md 21819 bytes of a 24000-byte budget = 90.91%, largest index line 361
|
|
143
|
+
* bytes so the old target was 23639 = 98.50%) `compact()` answered `archived: []` with this
|
|
144
|
+
* very sentence on screen. What has changed is the second half — "is not fixed here". It is
|
|
145
|
+
* fixed now, in the layer that owns it: `MemoryStore.compact`'s default `reserve` is measured
|
|
146
|
+
* from `undegradedIndexCeiling(budget)` rather than from `budget`, so running the command
|
|
147
|
+
* this sentence names clears the condition this sentence reports. Both runtimes moved in the
|
|
148
|
+
* same job (the reference in `87cc1f7`), because a band that closed on one side only would be
|
|
149
|
+
* the divergence the paragraph above correctly said this was not. The register entry's own
|
|
150
|
+
* closure is not written here — that is a `docs/porting.md` edit, and it waits on a
|
|
151
|
+
* conformance case comparing the two answers.
|
|
152
|
+
*
|
|
138
153
|
* THIS SENTENCE IS FOR THE OPERATOR; THE MODEL HAS ITS OWN REMEDY. Since job42 the model
|
|
139
154
|
* can call `memory_compact` (the ninth served tool) when a save is actually refused, and
|
|
140
155
|
* the refused-budget reply in `memory/component.ts` names it. That does not change this
|
|
@@ -152,13 +167,40 @@ export declare function indexPressureCondition(memory: Memory): Condition | null
|
|
|
152
167
|
* BOTH on and known to have lost a record is a problem anyone can act on.
|
|
153
168
|
*/
|
|
154
169
|
export declare function eventLogCondition(log: EventLog): Condition | null;
|
|
170
|
+
/**
|
|
171
|
+
* This install came from a path on this machine, and that path is gone.
|
|
172
|
+
*
|
|
173
|
+
* THE ONE CONDITION THAT NAMES A PATH, and the exception is deliberate. Every other sentence
|
|
174
|
+
* here refuses one because `assetsRoot()` resolves differently in the two runtimes by
|
|
175
|
+
* construction, so a path would be an uncomparable value bought for nothing. This path is not
|
|
176
|
+
* the server's own location — it is the origin THE INSTALLER WROTE DOWN, it is the entire
|
|
177
|
+
* actionable content of the finding (AS-7's measured case is a tarball under a
|
|
178
|
+
* `/private/tmp/.../scratchpad` that no longer exists), and a sentence saying "something is
|
|
179
|
+
* missing" without saying what would be a sentence nobody can act on.
|
|
180
|
+
*
|
|
181
|
+
* THE REMEDY IS THE ONE THAT ACTUALLY MOVES SOMETHING. J46-4 spent a unit removing a condition
|
|
182
|
+
* whose remedy exited 0 having changed nothing, and this is exactly the shape that invites
|
|
183
|
+
* another: `npm update` where a dangling `file:` install lives is a no-op BY CONSTRUCTION. So
|
|
184
|
+
* the sentence sends the reader at a reinstall BY NAME from a registry, which replaces the
|
|
185
|
+
* install rather than trying to refresh it in place. `bantamkit` is the PRODUCT name and is
|
|
186
|
+
* copied verbatim from the reference — it is not the npm package id, and turning it into one
|
|
187
|
+
* is how `git_commit` diverged on a noun.
|
|
188
|
+
*
|
|
189
|
+
* THE DERIVED HALF IS MEMOISED AND THIS HALF IS NOT: the shape cannot change under a running
|
|
190
|
+
* process, the path's existence can, and it is the one that has to be read now.
|
|
191
|
+
*/
|
|
192
|
+
export declare function installSourceCondition(install: Install | null): Condition | null;
|
|
155
193
|
/**
|
|
156
194
|
* Everything wrong right now, worst first. An empty array means healthy.
|
|
157
195
|
*
|
|
158
196
|
* ORDER IS SEVERITY AND IT IS LOAD-BEARING, because the footer shows the first one: a pack
|
|
159
197
|
* that vanished breaks every asset-backed surface; an unreadable layer makes recall ANSWER
|
|
160
198
|
* WRONGLY rather than fail; a full index refuses the next save; a broken event log costs
|
|
161
|
-
* diagnostics only
|
|
199
|
+
* diagnostics only; and a dangling install origin costs nothing AT ALL right now — the server
|
|
200
|
+
* is serving correctly, and what is broken is the next attempt to update it. That is why it is
|
|
201
|
+
* last despite being the one that went unnoticed for five releases
|
|
202
|
+
* (`docs/roadmap-agent-stack.md` AS-7): severity here is what is failing, not what has been
|
|
203
|
+
* failing longest.
|
|
162
204
|
*
|
|
163
205
|
* EVERY CONDITION IS OBSERVED, NOT INFERRED — no heartbeat, no timer, no last-seen timestamp.
|
|
164
206
|
* Each one is a state a test can construct and then watch this report: delete the pack, make a
|
|
@@ -167,8 +209,10 @@ export declare function eventLogCondition(log: EventLog): Condition | null;
|
|
|
167
209
|
* claimed.
|
|
168
210
|
*
|
|
169
211
|
* THE COST, because this runs on every tool call: one `stat` for the index, one `stat` for the
|
|
170
|
-
* pack, one directory listing per memory layer (two to four),
|
|
171
|
-
* fact file is opened
|
|
212
|
+
* pack, one directory listing per memory layer (two to four), a field read for the log, and one
|
|
213
|
+
* `stat` for the install origin. No fact file is opened, no index is parsed, and
|
|
214
|
+
* `node_modules` is NOT walked for a hidden lockfile — `currentInstall` does that once per
|
|
215
|
+
* process, because the bytes that were imported cannot change under a running one.
|
|
172
216
|
*/
|
|
173
217
|
export declare function degradedConditions(memory: Memory, log: EventLog): Condition[];
|
|
174
218
|
/**
|