bantamkit-mcp 0.35.6 → 0.35.8
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 +11 -7
- package/assets/tools/memory_compact.json +1 -2
- package/assets/tools/memory_recall.json +1 -3
- package/assets/tools/shiftwork_clock_in.json +1 -1
- package/assets/tools/shiftwork_clock_out.json +1 -1
- package/assets/tools/validate_json.json +1 -1
- package/assets/tools/work_plan.json +14 -8
- package/dist/cli.js +28 -0
- package/dist/contract.d.ts +0 -9
- package/dist/contract.js +51 -12
- package/dist/eventlog.d.ts +14 -1
- package/dist/eventlog.js +23 -3
- package/dist/hookadapter.js +260 -45
- package/dist/hostinstall.js +250 -79
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mcp/pyargs.d.ts +22 -6
- package/dist/mcp/pyargs.js +113 -39
- package/dist/mcp/server.js +18 -27
- package/dist/mcp/status.js +11 -4
- package/dist/mcpreport.d.ts +12 -0
- package/dist/mcpreport.js +67 -15
- package/dist/memory/cli.js +18 -3
- package/dist/memory/component.d.ts +17 -0
- package/dist/memory/component.js +51 -7
- package/dist/memory/factfile.d.ts +21 -3
- package/dist/memory/factfile.js +351 -12
- package/dist/memory/layers.d.ts +19 -0
- package/dist/memory/layers.js +102 -2
- package/dist/memory/pyyaml.d.ts +6 -4
- package/dist/memory/pyyaml.js +6 -4
- package/dist/memory/store.d.ts +82 -7
- package/dist/memory/store.js +277 -29
- package/dist/npminstall.d.ts +18 -0
- package/dist/npminstall.js +26 -5
- package/dist/pyjsonschema.d.ts +85 -4
- package/dist/pyjsonschema.js +1812 -181
- package/dist/selfupdate.d.ts +112 -32
- package/dist/selfupdate.js +221 -79
- package/dist/shiftwork.d.ts +46 -7
- package/dist/shiftwork.js +246 -42
- package/dist/workplan.d.ts +8 -3
- package/dist/workplan.js +62 -3
- package/package.json +1 -1
package/dist/memory/layers.js
CHANGED
|
@@ -184,8 +184,23 @@ export function loadGrants(projectStore) {
|
|
|
184
184
|
throw new MemoryValidationError(`invalid memory config ${configPath}: not a file`);
|
|
185
185
|
}
|
|
186
186
|
let data;
|
|
187
|
+
let text;
|
|
187
188
|
try {
|
|
188
|
-
|
|
189
|
+
text = pyReadText(configPath);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
throw new MemoryValidationError(`invalid memory config ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
193
|
+
}
|
|
194
|
+
// job65/#158: a tab where a YAML token would start is refused BEFORE the reader runs, in
|
|
195
|
+
// the sentence `load_grants` gives for PyYAML's own refusal of the same byte. The reader
|
|
196
|
+
// below accepted `[ \t]*` as indentation, so a tab-indented grant list was layered here
|
|
197
|
+
// and refused at startup by the reference: one config, two verdicts.
|
|
198
|
+
const tab = firstTokenTab(text);
|
|
199
|
+
if (tab !== null) {
|
|
200
|
+
throw new MemoryValidationError(`invalid memory config ${configPath}: ${tabSentence(tab[0], tab[1])}`);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
data = parseConfigDocument(text);
|
|
189
204
|
}
|
|
190
205
|
catch (e) {
|
|
191
206
|
// `except (yaml.YAMLError, OSError, UnicodeDecodeError)`. `pyReadText` raises the last
|
|
@@ -206,12 +221,97 @@ export function loadGrants(projectStore) {
|
|
|
206
221
|
for (const entry of raw.items) {
|
|
207
222
|
const resolved = pyResolve(pyJoin(pyParent(configPath), entry.value));
|
|
208
223
|
if (!pyIsDir(resolved)) {
|
|
209
|
-
|
|
224
|
+
// job65/#184: a path that EXISTS but is not a directory is its own sentence; a regular
|
|
225
|
+
// file used to be called nonexistent. `pyExists` raises for EACCES exactly as `pyIsDir`.
|
|
226
|
+
const shape = pyExists(resolved) ? 'is not a directory' : 'does not exist';
|
|
227
|
+
throw new MemoryValidationError(`granted store ${shape}: ${resolved} (from ${configPath})`);
|
|
210
228
|
}
|
|
211
229
|
grants.push(resolved);
|
|
212
230
|
}
|
|
213
231
|
return grants;
|
|
214
232
|
}
|
|
233
|
+
// ------------------------------------------------------------------------- the config
|
|
234
|
+
/**
|
|
235
|
+
* job65/#158 — the sentence both runtimes give for a tab PyYAML will not scan. Python
|
|
236
|
+
* rewrites PyYAML's `found character '\t' that cannot start any token` into exactly this,
|
|
237
|
+
* from the error's own mark; this port finds the same byte with `firstTokenTab`.
|
|
238
|
+
*/
|
|
239
|
+
export function tabSentence(line, column) {
|
|
240
|
+
return `line ${line}, column ${column}: a tab cannot start a YAML token; indent with spaces`;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The first tab PyYAML's scanner would refuse, as 1-based `[line, column]`, or `null`.
|
|
244
|
+
*
|
|
245
|
+
* MEASURED, not read off the spec: PyYAML 6.0.3's `scan_to_next_token` skips SPACES only, and
|
|
246
|
+
* a plain scalar stops at a tab, so every tab outside a quoted scalar and outside a comment
|
|
247
|
+
* is `found character '\t' that cannot start any token` — leading indentation, after `:`,
|
|
248
|
+
* after `-`, inside `[ ]`, between a value and its comment, trailing, alone on a line, and
|
|
249
|
+
* after `---`. A tab inside `'...'`/`"..."` or after a ` #` is accepted. The probe battery
|
|
250
|
+
* is in `.shiftwork/notes-job65/J65-10.md`. A quote OPENS a scalar only where a token can
|
|
251
|
+
* start (line start, or after a space, `[`, `{` or `,`) — `it's` is a plain scalar — and an
|
|
252
|
+
* open quote carries across lines, as a flow scalar does.
|
|
253
|
+
*/
|
|
254
|
+
export function firstTokenTab(text) {
|
|
255
|
+
const lines = text.split('\n');
|
|
256
|
+
let quote = null;
|
|
257
|
+
let flow = 0;
|
|
258
|
+
let plain = false; // inside a plain scalar, where a quote, `[` or ` ` is content
|
|
259
|
+
let plainIndent = 0; // the indentation of the line that plain scalar started on
|
|
260
|
+
for (let n = 0; n < lines.length; n += 1) {
|
|
261
|
+
const line = lines[n];
|
|
262
|
+
// A plain scalar continues onto a MORE-indented next line (a multi-line plain scalar);
|
|
263
|
+
// anything else starts a token. An approximation of PyYAML's indent rule.
|
|
264
|
+
const indent = line.length - line.replace(/^ +/, '').length;
|
|
265
|
+
if (plain && indent <= plainIndent)
|
|
266
|
+
plain = false;
|
|
267
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
268
|
+
const ch = line[i];
|
|
269
|
+
if (quote !== null) {
|
|
270
|
+
if (quote === '"' && ch === '\\')
|
|
271
|
+
i += 1;
|
|
272
|
+
else if (quote === "'" && ch === "'" && line[i + 1] === "'")
|
|
273
|
+
i += 1;
|
|
274
|
+
else if (ch === quote)
|
|
275
|
+
quote = null;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (ch === '\t')
|
|
279
|
+
return [n + 1, i + 1];
|
|
280
|
+
const next = line[i + 1] ?? ' ';
|
|
281
|
+
if (ch === ' ') {
|
|
282
|
+
if (plain && next === '#')
|
|
283
|
+
plain = false;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (plain) {
|
|
287
|
+
if (ch === ':' && (next === ' ' || next === '\t' || (flow > 0 && ',[]{}'.includes(next))))
|
|
288
|
+
plain = false;
|
|
289
|
+
else if (flow > 0 && ',[]{}'.includes(ch))
|
|
290
|
+
plain = false;
|
|
291
|
+
else
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
// A token starts here.
|
|
295
|
+
if (ch === '#')
|
|
296
|
+
break;
|
|
297
|
+
if (ch === "'" || ch === '"')
|
|
298
|
+
quote = ch;
|
|
299
|
+
else if (ch === '[' || ch === '{')
|
|
300
|
+
flow += 1;
|
|
301
|
+
else if ((ch === ']' || ch === '}') && flow > 0)
|
|
302
|
+
flow -= 1;
|
|
303
|
+
else if (ch === ',' && flow > 0)
|
|
304
|
+
continue;
|
|
305
|
+
else if ((ch === '-' || ch === ':' || ch === '?') && (next === ' ' || next === '\t'))
|
|
306
|
+
continue;
|
|
307
|
+
else {
|
|
308
|
+
plain = true;
|
|
309
|
+
plainIndent = indent;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
215
315
|
class ConfigParseError extends Error {
|
|
216
316
|
}
|
|
217
317
|
function parseConfigDocument(text) {
|
package/dist/memory/pyyaml.d.ts
CHANGED
|
@@ -147,9 +147,11 @@ export type PyOrder = {
|
|
|
147
147
|
* carried as a must-differ case rather than claimed.
|
|
148
148
|
* - `yaml` (`!`, `&`, `*`) RAISES, and CPython does NOT agree for all three: `k: !` is a
|
|
149
149
|
* tag PROPERTY on an empty node in the real scanner and answers `None`, while `&` and
|
|
150
|
-
* `*` are an anchor and an alias and answer with a `ScannerError`.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
150
|
+
* `*` are an anchor and an alias and answer with a `ScannerError`. All three are
|
|
151
|
+
* must-differ cases; `!` is the one where CPython answers and this port refuses, and it
|
|
152
|
+
* is written down as that rather than filed with the other two. Since #120 the fact
|
|
153
|
+
* codec refuses a value STARTING with any of the three before it reaches this function
|
|
154
|
+
* (`REFUSED_INDICATORS` in `factfile.ts`), so the raise a store sees is a
|
|
155
|
+
* `FactParseError` naming the indicator; this arm still answers for any other caller.
|
|
154
156
|
*/
|
|
155
157
|
export declare function constructPlain(value: string): string | null | PyScalar;
|
package/dist/memory/pyyaml.js
CHANGED
|
@@ -832,10 +832,12 @@ function constructTimestamp(raw) {
|
|
|
832
832
|
* carried as a must-differ case rather than claimed.
|
|
833
833
|
* - `yaml` (`!`, `&`, `*`) RAISES, and CPython does NOT agree for all three: `k: !` is a
|
|
834
834
|
* tag PROPERTY on an empty node in the real scanner and answers `None`, while `&` and
|
|
835
|
-
* `*` are an anchor and an alias and answer with a `ScannerError`.
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
835
|
+
* `*` are an anchor and an alias and answer with a `ScannerError`. All three are
|
|
836
|
+
* must-differ cases; `!` is the one where CPython answers and this port refuses, and it
|
|
837
|
+
* is written down as that rather than filed with the other two. Since #120 the fact
|
|
838
|
+
* codec refuses a value STARTING with any of the three before it reaches this function
|
|
839
|
+
* (`REFUSED_INDICATORS` in `factfile.ts`), so the raise a store sees is a
|
|
840
|
+
* `FactParseError` naming the indicator; this arm still answers for any other caller.
|
|
839
841
|
*/
|
|
840
842
|
export function constructPlain(value) {
|
|
841
843
|
const tag = resolveImplicitTag(value);
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -90,6 +90,21 @@ export declare const VALID_TYPES: readonly ["feedback", "project", "reference",
|
|
|
90
90
|
* `store.DURABLE_TYPES`.
|
|
91
91
|
*/
|
|
92
92
|
export declare const DURABLE_TYPES: readonly ["feedback", "user"];
|
|
93
|
+
/**
|
|
94
|
+
* THE LONGEST NAME A SAVE ACCEPTS (job65/#156): `NAME_MAX_CHARS` in the reference, the same
|
|
95
|
+
* number. The fact lands as `<name>.md` through `<name>.md.<pid>.<8 hex>.tmp` (#161), so the
|
|
96
|
+
* name leaves room for that suffix under the 255-byte file-name limit. WAS: no cap, and a
|
|
97
|
+
* 301-character name came back as a raw `[Errno 63] File name too long: '<absolute path>'`.
|
|
98
|
+
* `NAME_RE` admits ASCII only, so characters and bytes are one count.
|
|
99
|
+
*/
|
|
100
|
+
export declare const NAME_MAX_CHARS = 128;
|
|
101
|
+
/** `U+XXXX` of the first lone surrogate in `text`, or `null` when it is all text. */
|
|
102
|
+
export declare function loneSurrogate(text: string): string | null;
|
|
103
|
+
/** One fact file left out of a tolerant read, and the store's own sentence for why (#119). */
|
|
104
|
+
export interface SkippedFact {
|
|
105
|
+
file: string;
|
|
106
|
+
reason: string;
|
|
107
|
+
}
|
|
93
108
|
export declare const DUPLICATE_JACCARD = 0.5;
|
|
94
109
|
export declare const RECALL_MIN_SCORE_RATIO = 0;
|
|
95
110
|
export declare const MIN_RATIO_RANGE = "recall min-score ratio must be between 0.0 and 1.0";
|
|
@@ -318,8 +333,19 @@ export declare class MemoryStore {
|
|
|
318
333
|
* the failing leaf, or that strerror, would be a divergence manufactured by mkdir's
|
|
319
334
|
* internals. `this.root` is the path the caller named and both sides agree on it, which is
|
|
320
335
|
* what lets one conformance case pin this line byte for byte.
|
|
336
|
+
*
|
|
337
|
+
* AMENDED job65/#141: THE SENTENCE ABOVE IS NOW THE "NOT THERE" ARM ONLY. A `chmod 000`
|
|
338
|
+
* store answered it too, about a directory that exists. The second arm is still an outcome:
|
|
339
|
+
* the mkdir failed with one of `NOT_WRITABLE_CODES` AND `this.root` is a directory that is
|
|
340
|
+
* there, and then the refusal is `memory store is not writable: <root>: <strerror>; the
|
|
341
|
+
* directory exists but cannot be written to, so nothing was written`. The cwd-`/` divergence
|
|
342
|
+
* measured above is about a root that is NOT there, so it never reaches this arm.
|
|
321
343
|
*/
|
|
322
344
|
private ensureDirs;
|
|
345
|
+
/** `_present`: is `this.root` a directory that is there? "Not allowed to look" is "there". */
|
|
346
|
+
private present;
|
|
347
|
+
/** `_unwritable`: one sentence for a store that exists and refused a write (#141). */
|
|
348
|
+
private unwritable;
|
|
323
349
|
/**
|
|
324
350
|
* Write a fact, or refuse it as a near-duplicate of one already here.
|
|
325
351
|
*
|
|
@@ -356,7 +382,7 @@ export declare class MemoryStore {
|
|
|
356
382
|
* reference's `not 0.0 <= min_ratio <= 1.0` refuses it. That is a real mutant (M7b) and
|
|
357
383
|
* it is killed on both sides.
|
|
358
384
|
*/
|
|
359
|
-
recall(query: string, k?: number | null, stamp?: boolean, minRatio?: number): Fact[];
|
|
385
|
+
recall(query: string, k?: number | null, stamp?: boolean, minRatio?: number, skipped?: SkippedFact[]): Fact[];
|
|
360
386
|
/**
|
|
361
387
|
* The fact whose `name` field is exactly `name`, or `null`; no scoring, no ranking.
|
|
362
388
|
*
|
|
@@ -373,7 +399,7 @@ export declare class MemoryStore {
|
|
|
373
399
|
* order, which is the order `recall` breaks a tie in. `stamp` dates the hit through
|
|
374
400
|
* `stamp()` exactly as `recall` dates its hits; a miss writes nothing.
|
|
375
401
|
*/
|
|
376
|
-
lookup(name: string, stamp?: boolean): Fact | null;
|
|
402
|
+
lookup(name: string, stamp?: boolean, skipped?: SkippedFact[]): Fact | null;
|
|
377
403
|
/**
|
|
378
404
|
* `len(store._facts())` at the operator CLI, which is the only caller.
|
|
379
405
|
*
|
|
@@ -610,6 +636,18 @@ export declare class MemoryStore {
|
|
|
610
636
|
* bytes `dream` MEASURES and the bytes the store WRITES are already one function. That is
|
|
611
637
|
* the code-shape difference the Python half had to manufacture, not a behaviour one.
|
|
612
638
|
*/
|
|
639
|
+
/**
|
|
640
|
+
* Every fact this store can read, and `{file, reason}` for each file it could not.
|
|
641
|
+
*
|
|
642
|
+
* THE DECLARED TOLERANT LISTING (job65/#119), `readable_facts` in the reference. One empty,
|
|
643
|
+
* truncated, non-UTF-8 or unreadable fact file costs that file, not the store: the hook's
|
|
644
|
+
* SessionStart index and native export read through this, as `Memory.recallOutcome` reads
|
|
645
|
+
* through `recall(..., skipped)`. A `facts/` that cannot be listed still throws.
|
|
646
|
+
*/
|
|
647
|
+
readableFacts(): {
|
|
648
|
+
facts: Fact[];
|
|
649
|
+
skipped: SkippedFact[];
|
|
650
|
+
};
|
|
613
651
|
internals(): StoreInternals;
|
|
614
652
|
private factPath;
|
|
615
653
|
private indexLine;
|
|
@@ -694,10 +732,11 @@ export declare class MemoryStore {
|
|
|
694
732
|
/**
|
|
695
733
|
* Every fact on disk, parsed.
|
|
696
734
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
735
|
+
* AMENDED job65/#159: the `read_text` is now INSIDE the reference's try, so a fact file that
|
|
736
|
+
* is not UTF-8 is `malformed fact file <name>: 'utf-8' codec can't decode …` — naming the
|
|
737
|
+
* file — instead of the bare decoder message. `pyReadText` raises `PyUnicodeDecodeError`
|
|
738
|
+
* with CPython's own text and it is wrapped here; a `PyOSError` from the read still escapes
|
|
739
|
+
* as itself, as in the reference (`OSError` is not in its `except`).
|
|
701
740
|
*
|
|
702
741
|
* RULING — THE MESSAGE TEXT FOR A MALFORMED FILE. `_facts` interpolates the exception into
|
|
703
742
|
* `malformed fact file {name}: {e}`, and that reaches the model. Three of the four shapes
|
|
@@ -712,7 +751,33 @@ export declare class MemoryStore {
|
|
|
712
751
|
* carried as a must-differ `ruling` case in the conformance suite so it is re-measured on
|
|
713
752
|
* every run instead of asserted once and forgotten.
|
|
714
753
|
*/
|
|
754
|
+
/**
|
|
755
|
+
* `_facts`: every fact on disk, in `factPaths` order, with two hand-edit shapes tolerated.
|
|
756
|
+
*
|
|
757
|
+
* AN UNQUOTED TIMESTAMP IS READ AS ITS `str()` (#121). `yaml.safe_load` hands the reference
|
|
758
|
+
* a `datetime.date` for `created: 2026-01-01` and a `datetime.datetime` for the longer
|
|
759
|
+
* forms, while every file the store writes carries the quoted string. `recall` and `save`
|
|
760
|
+
* never minded, but `compact` sorts `(last_recalled or created, name)` and the sort raised
|
|
761
|
+
* `'<' not supported between instances of 'datetime.date' and 'str'` the moment one
|
|
762
|
+
* hand-edited file sat beside a written one — the only remedy the budget refusal names,
|
|
763
|
+
* dead on the store that named it. The scalar's `text` IS `str(value)` on the reference
|
|
764
|
+
* (`isoformat()` for a date, `isoformat(' ')` for a datetime), so both runtimes read the
|
|
765
|
+
* same string and the next write quotes it. Only timestamps are touched: an `int` or `bool`
|
|
766
|
+
* in these fields is left exactly as the scalar cases in `suites/store.mjs` pin it.
|
|
767
|
+
*
|
|
768
|
+
* `findings`, when given, collects one sentence per shape `lint` should name — the coerced
|
|
769
|
+
* timestamp, and a file whose name is not the fact's (#122) — without throwing, because
|
|
770
|
+
* every other caller tolerates both. `lint` is the only caller that passes an array.
|
|
771
|
+
*/
|
|
772
|
+
/**
|
|
773
|
+
* `skipped`, when given, makes ONE BAD FILE COST THAT FILE (job65/#119): a file that is
|
|
774
|
+
* malformed or cannot be read is pushed as `{file, reason}` (the sentence this would have
|
|
775
|
+
* thrown) and every other file is still read. Only `recall`, `lookup` and `readableFacts`
|
|
776
|
+
* pass it; a save, a compaction or a lint keeps the strict read.
|
|
777
|
+
*/
|
|
715
778
|
private facts;
|
|
779
|
+
/** One fact file, parsed, or the `malformed fact file` sentence for it (see `facts`). */
|
|
780
|
+
private readFact;
|
|
716
781
|
/**
|
|
717
782
|
* Date the recall.
|
|
718
783
|
*
|
|
@@ -722,7 +787,17 @@ export declare class MemoryStore {
|
|
|
722
787
|
* listing inside `_facts` has already run before any write.
|
|
723
788
|
*/
|
|
724
789
|
private stamp;
|
|
725
|
-
/**
|
|
790
|
+
/**
|
|
791
|
+
* Write via a PER-WRITER temp and `os.replace`, so a reader never sees a half-written fact
|
|
792
|
+
* and two writers never share a temp (job65/#161).
|
|
793
|
+
*
|
|
794
|
+
* WAS `<name>.md.tmp`, one temp per FACT: two servers saving one name raced on it (the
|
|
795
|
+
* second `replace` found nothing — `[Errno 2] … 'shared-name.md.tmp'`), and a write that
|
|
796
|
+
* died half-way left the temp beside the facts. NOW the shape `clockOut` and the hook ledger
|
|
797
|
+
* use (J65-1, J65-4): `<name>.md.<pid>.<8 hex>.tmp`, created with `wx` (O_EXCL), renamed
|
|
798
|
+
* onto the fact, and unlinked on any failure before the error goes on up. `*.md` listings
|
|
799
|
+
* never see it.
|
|
800
|
+
*/
|
|
726
801
|
private writeFact;
|
|
727
802
|
/**
|
|
728
803
|
* Write `index.md` from the facts on disk. THE OP THE ORIGINAL DEFECT DESTROYED.
|