@saasontools/strauss-kb 0.1.8 → 0.1.10
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/ARCHITECTURE.md +89 -0
- package/README.md +112 -39
- package/dist/{chunk-YJK7KGHN.js → chunk-EJQPZWN5.js} +518 -234
- package/dist/chunk-EJQPZWN5.js.map +1 -0
- package/dist/{chunk-YYX6CX6V.js → chunk-NMTP7V7E.js} +2 -2
- package/dist/{chunk-MGPYUZOM.js → chunk-RGK3K6LN.js} +2 -2
- package/dist/cli-main.cjs +504 -222
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +374 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -5
- package/dist/index.d.ts +146 -5
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +504 -222
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-YJK7KGHN.js.map +0 -1
- /package/dist/{chunk-YYX6CX6V.js.map → chunk-NMTP7V7E.js.map} +0 -0
- /package/dist/{chunk-MGPYUZOM.js.map → chunk-RGK3K6LN.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,7 @@ __export(index_exports, {
|
|
|
57
57
|
KB_SLUG_PATTERN: () => KB_SLUG_PATTERN,
|
|
58
58
|
KbBaseFrozenError: () => KbBaseFrozenError,
|
|
59
59
|
KbInvalidConceptIdError: () => KbInvalidConceptIdError,
|
|
60
|
+
KbMissingFlagValueError: () => KbMissingFlagValueError,
|
|
60
61
|
KbPackBudgetExceededError: () => KbPackBudgetExceededError,
|
|
61
62
|
KbPinsMalformedError: () => KbPinsMalformedError,
|
|
62
63
|
KbRecordAlreadyExistsError: () => KbRecordAlreadyExistsError,
|
|
@@ -75,6 +76,7 @@ __export(index_exports, {
|
|
|
75
76
|
adjudicate: () => adjudicate,
|
|
76
77
|
assertBaseNotFrozen: () => assertBaseNotFrozen,
|
|
77
78
|
buildContext: () => buildContext,
|
|
79
|
+
catalog: () => catalog,
|
|
78
80
|
composeDecisionRecord: () => composeDecisionRecord,
|
|
79
81
|
composeInputSchema: () => composeInputSchema,
|
|
80
82
|
composeNoDecisionRecord: () => composeNoDecisionRecord,
|
|
@@ -106,6 +108,7 @@ __export(index_exports, {
|
|
|
106
108
|
pinBase: () => pinBase,
|
|
107
109
|
readMergedPins: () => readMergedPins,
|
|
108
110
|
readPinsLayer: () => readPinsLayer,
|
|
111
|
+
renderCatalogLine: () => renderCatalogLine,
|
|
109
112
|
renderIndex: () => renderIndex,
|
|
110
113
|
renderIndexLine: () => renderIndexLine,
|
|
111
114
|
renderLogEntry: () => renderLogEntry,
|
|
@@ -257,6 +260,7 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
|
257
260
|
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
258
261
|
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
259
262
|
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
263
|
+
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
260
264
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
261
265
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
262
266
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
@@ -368,6 +372,21 @@ var KbPackBudgetExceededError = class extends BaseError {
|
|
|
368
372
|
budgetTokens;
|
|
369
373
|
excluded;
|
|
370
374
|
};
|
|
375
|
+
var KbMissingFlagValueError = class extends BaseError {
|
|
376
|
+
constructor(flag) {
|
|
377
|
+
super({
|
|
378
|
+
message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
|
|
379
|
+
errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
|
|
380
|
+
code: 400,
|
|
381
|
+
fault: "User" /* User */,
|
|
382
|
+
retriable: false,
|
|
383
|
+
reportToUser: true,
|
|
384
|
+
details: { flag }
|
|
385
|
+
});
|
|
386
|
+
this.flag = flag;
|
|
387
|
+
}
|
|
388
|
+
flag;
|
|
389
|
+
};
|
|
371
390
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
372
391
|
constructor(message, details) {
|
|
373
392
|
super({
|
|
@@ -503,7 +522,15 @@ var import_node_path = require("path");
|
|
|
503
522
|
var import_zod2 = require("zod");
|
|
504
523
|
var LOG_FILE = "log.jsonl";
|
|
505
524
|
var kbLogEntrySchema = import_zod2.z.object({
|
|
506
|
-
|
|
525
|
+
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
526
|
+
// below), and a value that isn't actually chronological — a Unix
|
|
527
|
+
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
528
|
+
// ever failing to parse. `z.iso.datetime()` accepts exactly what
|
|
529
|
+
// `record()` writes (`Date#toISOString()`: full precision, `Z` offset)
|
|
530
|
+
// and rejects everything else, including a non-`Z` offset — so a
|
|
531
|
+
// malformed `at` is reported the same way a malformed line already is,
|
|
532
|
+
// rather than silently sorting into the wrong place.
|
|
533
|
+
at: import_zod2.z.iso.datetime(),
|
|
507
534
|
by: import_zod2.z.string().min(1),
|
|
508
535
|
operation: import_zod2.z.string().min(1),
|
|
509
536
|
conceptId: import_zod2.z.string().min(1),
|
|
@@ -517,6 +544,7 @@ function renderLogEntry(entry) {
|
|
|
517
544
|
function parseLog(raw) {
|
|
518
545
|
const entries = [];
|
|
519
546
|
const malformed = [];
|
|
547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
520
548
|
raw.split("\n").forEach((text, index) => {
|
|
521
549
|
if (!text.trim()) return;
|
|
522
550
|
let value;
|
|
@@ -531,8 +559,14 @@ function parseLog(raw) {
|
|
|
531
559
|
malformed.push({ line: index + 1, text });
|
|
532
560
|
return;
|
|
533
561
|
}
|
|
562
|
+
const key2 = JSON.stringify(parsed.data);
|
|
563
|
+
if (seen.has(key2)) return;
|
|
564
|
+
seen.add(key2);
|
|
534
565
|
entries.push(parsed.data);
|
|
535
566
|
});
|
|
567
|
+
entries.sort(
|
|
568
|
+
(left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
|
|
569
|
+
);
|
|
536
570
|
return { entries, malformed };
|
|
537
571
|
}
|
|
538
572
|
|
|
@@ -818,6 +852,76 @@ function typeRank(record) {
|
|
|
818
852
|
return index === -1 ? TYPE_PRIORITY.length : index;
|
|
819
853
|
}
|
|
820
854
|
|
|
855
|
+
// src/catalog.ts
|
|
856
|
+
var EMPTY_STANDINGS = {
|
|
857
|
+
current: 0,
|
|
858
|
+
superseded: 0,
|
|
859
|
+
rejected: 0,
|
|
860
|
+
unsettled: 0,
|
|
861
|
+
open: 0
|
|
862
|
+
};
|
|
863
|
+
function catalog(bundle, options = {}) {
|
|
864
|
+
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
865
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
866
|
+
conceptId: hit.record.conceptId,
|
|
867
|
+
type: hit.record.frontmatter.type,
|
|
868
|
+
title: hit.record.frontmatter.title ?? null,
|
|
869
|
+
standing: hit.standing,
|
|
870
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
871
|
+
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
872
|
+
})).sort(byTypeThenTitle);
|
|
873
|
+
const standings = { ...EMPTY_STANDINGS };
|
|
874
|
+
for (const entry of entries) standings[entry.standing] += 1;
|
|
875
|
+
return {
|
|
876
|
+
entries,
|
|
877
|
+
recordCount: entries.length,
|
|
878
|
+
standings,
|
|
879
|
+
currentCount: standings.current,
|
|
880
|
+
supersededCount: standings.superseded,
|
|
881
|
+
staleCount: entries.filter((entry) => entry.stale).length
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function byTypeThenTitle(left, right) {
|
|
885
|
+
return byCodeUnit(left.type, right.type) || byCodeUnit(left.title ?? "", right.title ?? "") || byCodeUnit(left.conceptId, right.conceptId);
|
|
886
|
+
}
|
|
887
|
+
function byCodeUnit(left, right) {
|
|
888
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
889
|
+
}
|
|
890
|
+
function renderCatalogLine(entry) {
|
|
891
|
+
const parts = [
|
|
892
|
+
entry.conceptId,
|
|
893
|
+
entry.type,
|
|
894
|
+
entry.title ?? "(untitled)",
|
|
895
|
+
entry.standing === "superseded" ? `superseded \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}` : entry.standing
|
|
896
|
+
];
|
|
897
|
+
if (entry.stale) parts.push("stale");
|
|
898
|
+
return `- ${parts.join(" \xB7 ")}`;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/kb-gitattributes.ts
|
|
902
|
+
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
903
|
+
var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
|
|
904
|
+
function parseLine(line) {
|
|
905
|
+
const trimmed = line.trim();
|
|
906
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
907
|
+
const [pattern, ...attrs] = trimmed.split(/\s+/);
|
|
908
|
+
return pattern === void 0 ? null : { pattern, attrs };
|
|
909
|
+
}
|
|
910
|
+
function hasMergeDeclaration(contents) {
|
|
911
|
+
return contents.split("\n").some((line) => {
|
|
912
|
+
const parsed = parseLine(line);
|
|
913
|
+
if (!parsed || parsed.pattern !== LOG_FILE) return false;
|
|
914
|
+
return parsed.attrs.some(
|
|
915
|
+
(attr) => attr === "merge" || attr === "-merge" || attr.startsWith("merge=")
|
|
916
|
+
);
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
function appendUnionMergeLine(contents) {
|
|
920
|
+
const separator = contents.length === 0 || contents.endsWith("\n") ? "" : "\n";
|
|
921
|
+
return `${separator}${UNION_MERGE_LINE}
|
|
922
|
+
`;
|
|
923
|
+
}
|
|
924
|
+
|
|
821
925
|
// src/kb-store.ts
|
|
822
926
|
var KB_DIR = (0, import_node_path2.join)(".strauss", "kb");
|
|
823
927
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
@@ -1074,9 +1178,16 @@ ${answer}
|
|
|
1074
1178
|
* is indistinguishable from a complete one, so a caller would answer "that
|
|
1075
1179
|
* was never decided" from a slice it did not know was a slice.
|
|
1076
1180
|
*
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1181
|
+
* A token budget decides that, measured over what is actually handed back.
|
|
1182
|
+
* The refusal names the estimate and the budget, because a caller told only
|
|
1183
|
+
* "too big" cannot tell whether to narrow the type filter, raise the budget,
|
|
1184
|
+
* or stop loading the base whole altogether. Past the budget the answer is
|
|
1185
|
+
* the catalog and then a pack, which is what the refusal says.
|
|
1186
|
+
*
|
|
1187
|
+
* That refusal is the default guardrail. `all` bypasses the budget outright
|
|
1188
|
+
* and always hands back the whole bundle: an explicit, never-accidental
|
|
1189
|
+
* escape hatch for an operator who has the budget to spend, not a wider
|
|
1190
|
+
* default.
|
|
1080
1191
|
*/
|
|
1081
1192
|
async load(bundlePath2, options = {}) {
|
|
1082
1193
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
@@ -1091,7 +1202,12 @@ ${answer}
|
|
|
1091
1202
|
loaded: false,
|
|
1092
1203
|
recordCount: wanted.length,
|
|
1093
1204
|
approxTokens: approxTokens2,
|
|
1094
|
-
budgetTokens
|
|
1205
|
+
budgetTokens,
|
|
1206
|
+
message: refusalMessage({
|
|
1207
|
+
approxTokens: approxTokens2,
|
|
1208
|
+
budgetTokens,
|
|
1209
|
+
type: options.type
|
|
1210
|
+
})
|
|
1095
1211
|
};
|
|
1096
1212
|
}
|
|
1097
1213
|
return {
|
|
@@ -1107,6 +1223,10 @@ ${answer}
|
|
|
1107
1223
|
async trace(bundlePath2, seedId, options = {}) {
|
|
1108
1224
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
1109
1225
|
}
|
|
1226
|
+
/** Every record named in one line each. See `catalog.ts`. */
|
|
1227
|
+
async catalog(bundlePath2, options = {}) {
|
|
1228
|
+
return catalog(await this.list(bundlePath2), options);
|
|
1229
|
+
}
|
|
1110
1230
|
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
1111
1231
|
async pack(bundlePath2, rootId, options = {}) {
|
|
1112
1232
|
return pack(await this.list(bundlePath2), rootId, options);
|
|
@@ -1243,8 +1363,87 @@ ${answer}
|
|
|
1243
1363
|
await (0, import_promises2.unlink)(staging).catch(() => void 0);
|
|
1244
1364
|
}
|
|
1245
1365
|
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Declares union merge for the log, so two worktrees writing the same
|
|
1368
|
+
* bundle interleave their `log.jsonl` lines on merge rather than one
|
|
1369
|
+
* side's appends silently losing to git's ordinary line-level merge.
|
|
1370
|
+
*
|
|
1371
|
+
* Called from `record` — every path that appends a log line, not just
|
|
1372
|
+
* `write` — so a bundle only ever mutated through `setStatus`/`verify`/
|
|
1373
|
+
* `supersede` still gets it. There is no cheaper reliable signal for
|
|
1374
|
+
* "first write" than checking the file itself, and after the first call
|
|
1375
|
+
* the check is a no-op `readFile`.
|
|
1376
|
+
*
|
|
1377
|
+
* A missing `.gitattributes` is created outright, with `wx` (exclusive
|
|
1378
|
+
* create) rather than a plain write: if another process's `write()` won a
|
|
1379
|
+
* race and created the file between the `readFile` below and this call,
|
|
1380
|
+
* `wx` fails instead of truncating what that writer just wrote, and the
|
|
1381
|
+
* failure is swallowed by the catch below same as any other best-effort
|
|
1382
|
+
* miss. A file that exists but declares no merge strategy for the log
|
|
1383
|
+
* gets the line appended, never a wholesale rewrite; one that already
|
|
1384
|
+
* declares any merge strategy — this one or a user's own — is left alone
|
|
1385
|
+
* entirely (see `hasMergeDeclaration`).
|
|
1386
|
+
*
|
|
1387
|
+
* `readFile` failing is `existing === null` only for `ENOENT` — genuinely
|
|
1388
|
+
* missing. Any other error (a permission problem, a transient `EMFILE`,
|
|
1389
|
+
* the path being a directory) is *not* "missing" and must not fall into
|
|
1390
|
+
* the create branch, which would truncate whatever is actually there with
|
|
1391
|
+
* just the union-merge line: that is the file-destroying bug this
|
|
1392
|
+
* function exists to avoid, not commit. An unreadable existing file is
|
|
1393
|
+
* therefore left untouched and reported as a failure like any other.
|
|
1394
|
+
*
|
|
1395
|
+
* Two processes racing the append branch — both read a file without the
|
|
1396
|
+
* line, both append it — is possible and left unguarded: `appendFile` is
|
|
1397
|
+
* `O_APPEND`, so the result is two copies of the same line rather than a
|
|
1398
|
+
* torn write, and `hasMergeDeclaration` sees a duplicate declaration as
|
|
1399
|
+
* "already declared" on the next call. A cheap-to-detect, harmless-to-
|
|
1400
|
+
* leave residue, not a reason to add a cross-process lock (see
|
|
1401
|
+
* `ARCHITECTURE.md`'s rejection of one for the same trade on records).
|
|
1402
|
+
*
|
|
1403
|
+
* Best-effort, like the log append it precedes: failing to write this
|
|
1404
|
+
* file must not fail the mutation it guards.
|
|
1405
|
+
*/
|
|
1406
|
+
async ensureGitattributes(root) {
|
|
1407
|
+
const target = (0, import_node_path2.join)(root, GITATTRIBUTES_FILE);
|
|
1408
|
+
try {
|
|
1409
|
+
let existing;
|
|
1410
|
+
try {
|
|
1411
|
+
existing = await (0, import_promises2.readFile)(target, "utf8");
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
if (error.code !== "ENOENT") throw error;
|
|
1414
|
+
existing = null;
|
|
1415
|
+
}
|
|
1416
|
+
if (existing === null) {
|
|
1417
|
+
await (0, import_promises2.writeFile)(target, appendUnionMergeLine(""), {
|
|
1418
|
+
encoding: "utf8",
|
|
1419
|
+
flag: "wx"
|
|
1420
|
+
});
|
|
1421
|
+
this.logger.info?.({
|
|
1422
|
+
operation: "kb.gitattributes.ensure",
|
|
1423
|
+
bundlePath: root,
|
|
1424
|
+
outcome: "created"
|
|
1425
|
+
});
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (!hasMergeDeclaration(existing)) {
|
|
1429
|
+
await (0, import_promises2.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
1430
|
+
this.logger.info?.({
|
|
1431
|
+
operation: "kb.gitattributes.ensure",
|
|
1432
|
+
bundlePath: root,
|
|
1433
|
+
outcome: "appended"
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
this.logger.warn?.({
|
|
1438
|
+
operation: "kb.gitattributes.ensure",
|
|
1439
|
+
outcome: "failed",
|
|
1440
|
+
error: error instanceof Error ? error.message : "unknown"
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1246
1444
|
/** Appends one log line. Failing to log must not fail the mutation. */
|
|
1247
1445
|
async record(root, entry) {
|
|
1446
|
+
await this.ensureGitattributes(root);
|
|
1248
1447
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
1249
1448
|
await (0, import_promises2.appendFile)((0, import_node_path2.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
1250
1449
|
this.logger.warn?.({
|
|
@@ -1294,6 +1493,14 @@ function estimateTokens(record) {
|
|
|
1294
1493
|
function estimateStubTokens(entry) {
|
|
1295
1494
|
return Math.ceil(JSON.stringify(entry).length / 4);
|
|
1296
1495
|
}
|
|
1496
|
+
function refusalMessage(refusal) {
|
|
1497
|
+
const scope = refusal.type ? ` of type ${refusal.type}` : "";
|
|
1498
|
+
return [
|
|
1499
|
+
`Refusing to load this base whole: ~${refusal.approxTokens} tokens is past the ${refusal.budgetTokens}-token budget.`,
|
|
1500
|
+
`Call kb_catalog for one line per record${scope} (id, type, title, standing), then kb_pack on the record that matters; kb_query works for a lookup by wording.`,
|
|
1501
|
+
`To load anyway: raise budgetTokens (currently ${refusal.budgetTokens}), or all=true to bypass the budget.`
|
|
1502
|
+
].join(" ");
|
|
1503
|
+
}
|
|
1297
1504
|
function stub(hit) {
|
|
1298
1505
|
return {
|
|
1299
1506
|
conceptId: hit.record.conceptId,
|
|
@@ -2443,8 +2650,19 @@ function define(command) {
|
|
|
2443
2650
|
return command;
|
|
2444
2651
|
}
|
|
2445
2652
|
function argvFlag(argv, name) {
|
|
2653
|
+
const joined = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
2654
|
+
if (joined !== void 0) {
|
|
2655
|
+
const value2 = joined.slice(name.length + 1);
|
|
2656
|
+
if (!value2) throw new KbMissingFlagValueError(name);
|
|
2657
|
+
return value2;
|
|
2658
|
+
}
|
|
2446
2659
|
const at = argv.indexOf(name);
|
|
2447
|
-
|
|
2660
|
+
if (at === -1) return void 0;
|
|
2661
|
+
const value = argv[at + 1];
|
|
2662
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
2663
|
+
throw new KbMissingFlagValueError(name);
|
|
2664
|
+
}
|
|
2665
|
+
return value;
|
|
2448
2666
|
}
|
|
2449
2667
|
|
|
2450
2668
|
// src/commands/answer.ts
|
|
@@ -2466,27 +2684,90 @@ var answerCommand = define({
|
|
|
2466
2684
|
}
|
|
2467
2685
|
});
|
|
2468
2686
|
|
|
2469
|
-
// src/commands/
|
|
2687
|
+
// src/commands/catalog.ts
|
|
2470
2688
|
var import_zod9 = require("zod");
|
|
2689
|
+
var catalogCommand = define({
|
|
2690
|
+
name: "catalog",
|
|
2691
|
+
tool: "kb_catalog",
|
|
2692
|
+
usage: "catalog [type]",
|
|
2693
|
+
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
2694
|
+
input: import_zod9.z.object({
|
|
2695
|
+
bundlePath,
|
|
2696
|
+
type: import_zod9.z.enum(KB_RECORD_TYPES).optional()
|
|
2697
|
+
}),
|
|
2698
|
+
fromArgv: (argv, path) => ({
|
|
2699
|
+
bundlePath: path,
|
|
2700
|
+
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
|
|
2701
|
+
}),
|
|
2702
|
+
run: async ({ store }, { bundlePath: path, type }) => render(
|
|
2703
|
+
await store.catalog(path, { ...type ? { type } : {} }),
|
|
2704
|
+
path,
|
|
2705
|
+
type
|
|
2706
|
+
)
|
|
2707
|
+
});
|
|
2708
|
+
function render(result, bundle, type) {
|
|
2709
|
+
const lines = [
|
|
2710
|
+
`# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
|
|
2711
|
+
`bundle: ${bundle}`,
|
|
2712
|
+
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
2713
|
+
];
|
|
2714
|
+
if (result.staleCount) {
|
|
2715
|
+
lines.push(
|
|
2716
|
+
`${result.staleCount} stale \u2014 a flag over the standings above, not one of them`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
lines.push("");
|
|
2720
|
+
if (!result.entries.length) {
|
|
2721
|
+
lines.push(
|
|
2722
|
+
type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
|
|
2723
|
+
);
|
|
2724
|
+
} else {
|
|
2725
|
+
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
2726
|
+
}
|
|
2727
|
+
lines.push(
|
|
2728
|
+
"",
|
|
2729
|
+
"Bodies are not here: kb_pack <conceptId> for the neighbourhood around one record, kb_load for the whole base when it fits the budget, kb_query for a lookup by wording, kb_trace <conceptId> for how a position was arrived at."
|
|
2730
|
+
);
|
|
2731
|
+
return lines.join("\n");
|
|
2732
|
+
}
|
|
2733
|
+
function standingCounts(result) {
|
|
2734
|
+
const ORDER = [
|
|
2735
|
+
"current",
|
|
2736
|
+
"open",
|
|
2737
|
+
"unsettled",
|
|
2738
|
+
"rejected",
|
|
2739
|
+
"superseded"
|
|
2740
|
+
];
|
|
2741
|
+
const parts = ORDER.filter((standing) => result.standings[standing]).map(
|
|
2742
|
+
(standing) => `${result.standings[standing]} ${standing}`
|
|
2743
|
+
);
|
|
2744
|
+
return parts.length ? parts.join(" \xB7 ") : "none";
|
|
2745
|
+
}
|
|
2746
|
+
function count(value, noun) {
|
|
2747
|
+
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// src/commands/context.ts
|
|
2751
|
+
var import_zod10 = require("zod");
|
|
2471
2752
|
var contextCommand = define({
|
|
2472
2753
|
name: "context",
|
|
2473
2754
|
tool: "kb_context",
|
|
2474
2755
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
2475
2756
|
description: "The pinned-base index block, for injection at every context birth \u2014 startup, clear, resume, and after compaction. An index, not the content: concept ids, titles and standing, with the bodies left behind kb_load at the point of use. Emits nothing when nothing is pinned. Refuses with the list of bases and their sizes rather than truncating past its budget. Budgets resolve most-specific-first: explicit flags, then the workspace manifests' `context` tables (per profile, over their `default`), then the built-in profile (session-start, compact, turn), then package defaults \u2014 so a repo tunes its own numbers in .strauss/kb-pins.json without touching hook commands. Like kb_schema and kb_types this takes no bundlePath \u2014 it reads the workspace pin manifests, because which bases a session should see is workspace state, not a property of one base.",
|
|
2476
|
-
input:
|
|
2477
|
-
budgetTokens:
|
|
2757
|
+
input: import_zod10.z.object({
|
|
2758
|
+
budgetTokens: import_zod10.z.number().int().positive().optional().describe(
|
|
2478
2759
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
2479
2760
|
),
|
|
2480
|
-
fullUnderTokens:
|
|
2761
|
+
fullUnderTokens: import_zod10.z.number().int().positive().optional().describe(
|
|
2481
2762
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
2482
2763
|
),
|
|
2483
|
-
profile:
|
|
2764
|
+
profile: import_zod10.z.string().optional().describe(
|
|
2484
2765
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
2485
2766
|
),
|
|
2486
|
-
format:
|
|
2767
|
+
format: import_zod10.z.enum(["markdown", "json"]).optional().describe(
|
|
2487
2768
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
2488
2769
|
),
|
|
2489
|
-
event:
|
|
2770
|
+
event: import_zod10.z.string().optional().describe(
|
|
2490
2771
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
2491
2772
|
)
|
|
2492
2773
|
}),
|
|
@@ -2522,14 +2803,14 @@ var contextCommand = define({
|
|
|
2522
2803
|
});
|
|
2523
2804
|
|
|
2524
2805
|
// src/commands/doctor.ts
|
|
2525
|
-
var
|
|
2526
|
-
var days = (what, fallback) =>
|
|
2806
|
+
var import_zod11 = require("zod");
|
|
2807
|
+
var days = (what, fallback) => import_zod11.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
2527
2808
|
var doctorCommand = define({
|
|
2528
2809
|
name: "doctor",
|
|
2529
2810
|
tool: "kb_doctor",
|
|
2530
2811
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
2531
2812
|
description: "A health sweep over a whole base: what the calendar has already retired, what nobody ever confirmed, what has been open or proposed long enough that the status is now the answer, and what the graph has dropped on the floor. Read-only \u2014 it never writes, never supersedes, and never re-dates anything; every finding names a record for a person to repair. Seven checks, grouped and counted: expired (past `stale_after`), expiring (inside the window), unverified (an empty `verified[]` on a record old enough to matter), aging (still `open` or `proposed`), orphaned (no other record links to it), broken supersession (a chain that does not resolve), and superseded-but-cited (a live record whose body links to a record that no longer holds). Every group is reported even when empty, because a check that found nothing and a check that never ran look identical in a report that only lists findings.\n\nThis is the question no reader thinks to ask, which is why it needs a command: decay is invisible from inside a single record \u2014 a stale one reads exactly like a live one, and a question nobody answered reads exactly like one nobody asked. Reach for it when picking up a base someone else kept, before trusting a base you have not touched in months, or on a schedule; kb_validate is the narrower neighbour, checking only whether pointers between records agree.",
|
|
2532
|
-
input:
|
|
2813
|
+
input: import_zod11.z.object({
|
|
2533
2814
|
bundlePath,
|
|
2534
2815
|
expiringDays: days(
|
|
2535
2816
|
"How far ahead `expiring` looks, in days.",
|
|
@@ -2543,7 +2824,7 @@ var doctorCommand = define({
|
|
|
2543
2824
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2544
2825
|
DEFAULT_AGING_DAYS
|
|
2545
2826
|
),
|
|
2546
|
-
strict:
|
|
2827
|
+
strict: import_zod11.z.boolean().optional().describe(
|
|
2547
2828
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2548
2829
|
)
|
|
2549
2830
|
}),
|
|
@@ -2573,14 +2854,14 @@ var doctorCommand = define({
|
|
|
2573
2854
|
});
|
|
2574
2855
|
return { bundlePath: path, checkedAt, ...report };
|
|
2575
2856
|
},
|
|
2576
|
-
render: (result) =>
|
|
2857
|
+
render: (result) => render2(result),
|
|
2577
2858
|
// Only expiry, and only under --strict. The other six checks report debt a
|
|
2578
2859
|
// reader decides about; an expired record is the base asserting something it
|
|
2579
2860
|
// already said it would stop standing behind, which is the one finding a
|
|
2580
2861
|
// pipeline can act on without a judgment call.
|
|
2581
2862
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
2582
2863
|
});
|
|
2583
|
-
function
|
|
2864
|
+
function render2(result) {
|
|
2584
2865
|
const { thresholds } = result;
|
|
2585
2866
|
const lines = [
|
|
2586
2867
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -2612,13 +2893,13 @@ function render(result) {
|
|
|
2612
2893
|
}
|
|
2613
2894
|
|
|
2614
2895
|
// src/commands/list.ts
|
|
2615
|
-
var
|
|
2896
|
+
var import_zod12 = require("zod");
|
|
2616
2897
|
var listCommand = define({
|
|
2617
2898
|
name: "list",
|
|
2618
2899
|
tool: "kb_list",
|
|
2619
2900
|
usage: "list [type]",
|
|
2620
2901
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
2621
|
-
input:
|
|
2902
|
+
input: import_zod12.z.object({ bundlePath, type: import_zod12.z.enum(KB_RECORD_TYPES).optional() }),
|
|
2622
2903
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
2623
2904
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
2624
2905
|
conceptId: record.conceptId,
|
|
@@ -2630,21 +2911,21 @@ var listCommand = define({
|
|
|
2630
2911
|
});
|
|
2631
2912
|
|
|
2632
2913
|
// src/commands/load.ts
|
|
2633
|
-
var
|
|
2914
|
+
var import_zod13 = require("zod");
|
|
2634
2915
|
var loadCommand = define({
|
|
2635
2916
|
name: "load",
|
|
2636
2917
|
tool: "kb_load",
|
|
2637
|
-
usage: "load [type] [--budget N
|
|
2638
|
-
description: "
|
|
2639
|
-
input:
|
|
2918
|
+
usage: "load [type] [--budget N] [--all]",
|
|
2919
|
+
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs (name, replacement, date); rejected and open records arrive whole. Refuses past the token budget rather than truncating \u2014 call kb_catalog, then kb_pack on the record that matters, or narrow with `type`; kb_query for a lookup by wording. `all` bypasses the budget.",
|
|
2920
|
+
input: import_zod13.z.object({
|
|
2640
2921
|
bundlePath,
|
|
2641
|
-
type:
|
|
2642
|
-
budgetTokens:
|
|
2643
|
-
all:
|
|
2644
|
-
"
|
|
2922
|
+
type: import_zod13.z.enum(KB_RECORD_TYPES).optional(),
|
|
2923
|
+
budgetTokens: import_zod13.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2924
|
+
all: import_zod13.z.boolean().optional().describe(
|
|
2925
|
+
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
2645
2926
|
)
|
|
2646
2927
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
2647
|
-
message: "all
|
|
2928
|
+
message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
|
|
2648
2929
|
}),
|
|
2649
2930
|
fromArgv: (argv, path) => {
|
|
2650
2931
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -2678,25 +2959,25 @@ var loadCommand = define({
|
|
|
2678
2959
|
});
|
|
2679
2960
|
|
|
2680
2961
|
// src/commands/log.ts
|
|
2681
|
-
var
|
|
2962
|
+
var import_zod14 = require("zod");
|
|
2682
2963
|
var logCommand = define({
|
|
2683
2964
|
name: "log",
|
|
2684
2965
|
tool: "kb_log",
|
|
2685
2966
|
usage: "log",
|
|
2686
2967
|
description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
|
|
2687
|
-
input:
|
|
2968
|
+
input: import_zod14.z.object({ bundlePath }),
|
|
2688
2969
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2689
2970
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
2690
2971
|
});
|
|
2691
2972
|
|
|
2692
2973
|
// src/commands/no-decision.ts
|
|
2693
|
-
var
|
|
2974
|
+
var import_zod15 = require("zod");
|
|
2694
2975
|
var noDecisionCommand = define({
|
|
2695
2976
|
name: "no-decision",
|
|
2696
2977
|
tool: "kb_no_decision",
|
|
2697
2978
|
usage: "no-decision <reason...>",
|
|
2698
2979
|
description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
|
|
2699
|
-
input:
|
|
2980
|
+
input: import_zod15.z.object({ bundlePath, reason: import_zod15.z.string().min(1) }),
|
|
2700
2981
|
fromArgv: (argv, path) => ({
|
|
2701
2982
|
bundlePath: path,
|
|
2702
2983
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -2713,20 +2994,20 @@ var noDecisionCommand = define({
|
|
|
2713
2994
|
});
|
|
2714
2995
|
|
|
2715
2996
|
// src/commands/pack.ts
|
|
2716
|
-
var
|
|
2997
|
+
var import_zod16 = require("zod");
|
|
2717
2998
|
var packCommand = define({
|
|
2718
2999
|
name: "pack",
|
|
2719
3000
|
tool: "kb_pack",
|
|
2720
3001
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
2721
3002
|
description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
|
|
2722
|
-
input:
|
|
3003
|
+
input: import_zod16.z.object({
|
|
2723
3004
|
bundlePath,
|
|
2724
3005
|
conceptId,
|
|
2725
|
-
hops:
|
|
2726
|
-
maxNodes:
|
|
3006
|
+
hops: import_zod16.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
3007
|
+
maxNodes: import_zod16.z.number().int().positive().optional().describe(
|
|
2727
3008
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
2728
3009
|
),
|
|
2729
|
-
budgetTokens:
|
|
3010
|
+
budgetTokens: import_zod16.z.number().int().positive().optional().describe(
|
|
2730
3011
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
2731
3012
|
)
|
|
2732
3013
|
}),
|
|
@@ -2748,10 +3029,10 @@ var packCommand = define({
|
|
|
2748
3029
|
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
2749
3030
|
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
2750
3031
|
});
|
|
2751
|
-
return
|
|
3032
|
+
return render3(result, path, now());
|
|
2752
3033
|
}
|
|
2753
3034
|
});
|
|
2754
|
-
function
|
|
3035
|
+
function render3(result, bundle, at) {
|
|
2755
3036
|
const lines = [
|
|
2756
3037
|
`# KB Pack \u2014 ${result.root}`,
|
|
2757
3038
|
`bundle: ${bundle}`,
|
|
@@ -2813,22 +3094,22 @@ function warningLabel(warning) {
|
|
|
2813
3094
|
}
|
|
2814
3095
|
|
|
2815
3096
|
// src/commands/pin.ts
|
|
2816
|
-
var
|
|
3097
|
+
var import_zod17 = require("zod");
|
|
2817
3098
|
var pinCommand = define({
|
|
2818
3099
|
name: "pin",
|
|
2819
3100
|
tool: "kb_pin",
|
|
2820
3101
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
2821
3102
|
description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
|
|
2822
|
-
input:
|
|
3103
|
+
input: import_zod17.z.object({
|
|
2823
3104
|
bundlePath,
|
|
2824
|
-
mode:
|
|
3105
|
+
mode: import_zod17.z.enum(["full", "index"]).optional().describe(
|
|
2825
3106
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
2826
3107
|
),
|
|
2827
|
-
profiles:
|
|
2828
|
-
layer:
|
|
3108
|
+
profiles: import_zod17.z.array(import_zod17.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
3109
|
+
layer: import_zod17.z.enum(["project", "local", "user"]).optional().describe(
|
|
2829
3110
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
2830
3111
|
),
|
|
2831
|
-
frozen:
|
|
3112
|
+
frozen: import_zod17.z.boolean().optional().describe(
|
|
2832
3113
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
2833
3114
|
)
|
|
2834
3115
|
}),
|
|
@@ -2857,29 +3138,29 @@ var pinCommand = define({
|
|
|
2857
3138
|
});
|
|
2858
3139
|
|
|
2859
3140
|
// src/commands/pins.ts
|
|
2860
|
-
var
|
|
3141
|
+
var import_zod18 = require("zod");
|
|
2861
3142
|
var pinsCommand = define({
|
|
2862
3143
|
name: "pins",
|
|
2863
3144
|
tool: "kb_pins",
|
|
2864
3145
|
usage: "pins",
|
|
2865
3146
|
description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
|
|
2866
|
-
input:
|
|
3147
|
+
input: import_zod18.z.object({}),
|
|
2867
3148
|
fromArgv: () => ({}),
|
|
2868
3149
|
run: ({ store }) => listPins(store, process.cwd())
|
|
2869
3150
|
});
|
|
2870
3151
|
|
|
2871
3152
|
// src/commands/query.ts
|
|
2872
|
-
var
|
|
3153
|
+
var import_zod19 = require("zod");
|
|
2873
3154
|
var queryCommand = define({
|
|
2874
3155
|
name: "query",
|
|
2875
3156
|
tool: "kb_query",
|
|
2876
3157
|
usage: "query <text...>",
|
|
2877
|
-
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted.
|
|
2878
|
-
input:
|
|
3158
|
+
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted. This is the lookup-by-wording rung, and the narrowest of the three: use it when you know roughly what the record says. The decision rule around it \u2014 while the base fits kb_load's token budget, kb_load it whole, because on this package's measurements a reader holding the whole base answered eight of nine questions whose wording appears in no record where embedding search answered four; once kb_load refuses, kb_catalog for one line per record and then kb_pack on the record the work centres on; and kb_query when the question is a point lookup rather than a neighbourhood. A query cannot tell you that nothing was decided \u2014 it returns its nearest hit whatever the distance \u2014 so reach for kb_catalog when the question is what exists. Never read record files directly: this tool (with kb_load, kb_catalog, kb_pack and kb_trace) is the only supported way to read a base; a file read bypasses supersession resolution and returns replaced records as if current.",
|
|
3159
|
+
input: import_zod19.z.object({
|
|
2879
3160
|
bundlePath,
|
|
2880
|
-
text:
|
|
2881
|
-
type:
|
|
2882
|
-
includeNonCurrent:
|
|
3161
|
+
text: import_zod19.z.string().optional(),
|
|
3162
|
+
type: import_zod19.z.enum(KB_RECORD_TYPES).optional(),
|
|
3163
|
+
includeNonCurrent: import_zod19.z.boolean().optional()
|
|
2883
3164
|
}),
|
|
2884
3165
|
fromArgv: (argv, path) => ({
|
|
2885
3166
|
bundlePath: path,
|
|
@@ -2901,40 +3182,40 @@ var queryCommand = define({
|
|
|
2901
3182
|
});
|
|
2902
3183
|
|
|
2903
3184
|
// src/commands/read-index.ts
|
|
2904
|
-
var
|
|
3185
|
+
var import_zod20 = require("zod");
|
|
2905
3186
|
var readIndexCommand = define({
|
|
2906
3187
|
name: "index",
|
|
2907
3188
|
tool: "kb_index",
|
|
2908
3189
|
usage: "index",
|
|
2909
3190
|
description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
|
|
2910
|
-
input:
|
|
3191
|
+
input: import_zod20.z.object({ bundlePath }),
|
|
2911
3192
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2912
3193
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
2913
3194
|
});
|
|
2914
3195
|
|
|
2915
3196
|
// src/commands/schema.ts
|
|
2916
|
-
var
|
|
3197
|
+
var import_zod21 = require("zod");
|
|
2917
3198
|
var schemaCommand = define({
|
|
2918
3199
|
name: "schema",
|
|
2919
3200
|
tool: "kb_schema",
|
|
2920
3201
|
usage: "schema",
|
|
2921
3202
|
description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
|
|
2922
|
-
input:
|
|
3203
|
+
input: import_zod21.z.object({}),
|
|
2923
3204
|
fromArgv: () => ({}),
|
|
2924
3205
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
2925
3206
|
});
|
|
2926
3207
|
|
|
2927
3208
|
// src/commands/status.ts
|
|
2928
|
-
var
|
|
3209
|
+
var import_zod22 = require("zod");
|
|
2929
3210
|
var statusCommand = define({
|
|
2930
3211
|
name: "status",
|
|
2931
3212
|
tool: "kb_status",
|
|
2932
3213
|
usage: "status <concept-id> <status>",
|
|
2933
3214
|
description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
|
|
2934
|
-
input:
|
|
3215
|
+
input: import_zod22.z.object({
|
|
2935
3216
|
bundlePath,
|
|
2936
3217
|
conceptId,
|
|
2937
|
-
status:
|
|
3218
|
+
status: import_zod22.z.enum(KB_RECORD_STATUSES)
|
|
2938
3219
|
}),
|
|
2939
3220
|
fromArgv: (argv, path) => ({
|
|
2940
3221
|
bundlePath: path,
|
|
@@ -2949,13 +3230,13 @@ var statusCommand = define({
|
|
|
2949
3230
|
});
|
|
2950
3231
|
|
|
2951
3232
|
// src/commands/supersede.ts
|
|
2952
|
-
var
|
|
3233
|
+
var import_zod23 = require("zod");
|
|
2953
3234
|
var supersedeCommand = define({
|
|
2954
3235
|
name: "supersede",
|
|
2955
3236
|
tool: "kb_supersede",
|
|
2956
3237
|
usage: "supersede <concept-id> <replacement-id>",
|
|
2957
3238
|
description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
|
|
2958
|
-
input:
|
|
3239
|
+
input: import_zod23.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
2959
3240
|
fromArgv: (argv, path) => ({
|
|
2960
3241
|
bundlePath: path,
|
|
2961
3242
|
conceptId: argv[1],
|
|
@@ -2969,16 +3250,16 @@ var supersedeCommand = define({
|
|
|
2969
3250
|
});
|
|
2970
3251
|
|
|
2971
3252
|
// src/commands/sync-instructions.ts
|
|
2972
|
-
var
|
|
3253
|
+
var import_zod24 = require("zod");
|
|
2973
3254
|
var syncInstructionsCommand = define({
|
|
2974
3255
|
name: "sync-instructions",
|
|
2975
3256
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
2976
3257
|
description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
|
|
2977
|
-
input:
|
|
2978
|
-
file:
|
|
2979
|
-
budgetTokens:
|
|
2980
|
-
fullUnderTokens:
|
|
2981
|
-
profile:
|
|
3258
|
+
input: import_zod24.z.object({
|
|
3259
|
+
file: import_zod24.z.string().min(1).describe("The instruction file to edit in place."),
|
|
3260
|
+
budgetTokens: import_zod24.z.number().int().positive().optional(),
|
|
3261
|
+
fullUnderTokens: import_zod24.z.number().int().positive().optional(),
|
|
3262
|
+
profile: import_zod24.z.string().optional()
|
|
2982
3263
|
}),
|
|
2983
3264
|
fromArgv: (argv) => {
|
|
2984
3265
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3004,17 +3285,17 @@ var syncInstructionsCommand = define({
|
|
|
3004
3285
|
});
|
|
3005
3286
|
|
|
3006
3287
|
// src/commands/trace.ts
|
|
3007
|
-
var
|
|
3288
|
+
var import_zod25 = require("zod");
|
|
3008
3289
|
var traceCommand = define({
|
|
3009
3290
|
name: "trace",
|
|
3010
3291
|
tool: "kb_trace",
|
|
3011
3292
|
usage: "trace <concept-id> [edges...]",
|
|
3012
3293
|
description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
|
|
3013
|
-
input:
|
|
3294
|
+
input: import_zod25.z.object({
|
|
3014
3295
|
bundlePath,
|
|
3015
3296
|
conceptId,
|
|
3016
|
-
edges:
|
|
3017
|
-
depth:
|
|
3297
|
+
edges: import_zod25.z.array(import_zod25.z.enum(TRACE_EDGES)).optional(),
|
|
3298
|
+
depth: import_zod25.z.number().int().positive().optional()
|
|
3018
3299
|
}),
|
|
3019
3300
|
fromArgv: (argv, path) => ({
|
|
3020
3301
|
bundlePath: path,
|
|
@@ -3036,53 +3317,53 @@ var traceCommand = define({
|
|
|
3036
3317
|
});
|
|
3037
3318
|
|
|
3038
3319
|
// src/commands/types.ts
|
|
3039
|
-
var
|
|
3320
|
+
var import_zod26 = require("zod");
|
|
3040
3321
|
var typesCommand = define({
|
|
3041
3322
|
name: "types",
|
|
3042
3323
|
tool: "kb_types",
|
|
3043
3324
|
usage: "types",
|
|
3044
3325
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
3045
|
-
input:
|
|
3326
|
+
input: import_zod26.z.object({}),
|
|
3046
3327
|
fromArgv: () => ({}),
|
|
3047
3328
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3048
3329
|
});
|
|
3049
3330
|
|
|
3050
3331
|
// src/commands/unpin.ts
|
|
3051
|
-
var
|
|
3332
|
+
var import_zod27 = require("zod");
|
|
3052
3333
|
var unpinCommand = define({
|
|
3053
3334
|
name: "unpin",
|
|
3054
3335
|
tool: "kb_unpin",
|
|
3055
3336
|
usage: "unpin [bundle-path]",
|
|
3056
3337
|
description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
|
|
3057
|
-
input:
|
|
3338
|
+
input: import_zod27.z.object({ bundlePath }),
|
|
3058
3339
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3059
3340
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3060
3341
|
});
|
|
3061
3342
|
|
|
3062
3343
|
// src/commands/validate.ts
|
|
3063
|
-
var
|
|
3344
|
+
var import_zod28 = require("zod");
|
|
3064
3345
|
var validateCommand = define({
|
|
3065
3346
|
name: "validate",
|
|
3066
3347
|
tool: "kb_validate",
|
|
3067
3348
|
usage: "validate",
|
|
3068
3349
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
|
|
3069
|
-
input:
|
|
3350
|
+
input: import_zod28.z.object({ bundlePath }),
|
|
3070
3351
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3071
3352
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3072
3353
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
3073
3354
|
});
|
|
3074
3355
|
|
|
3075
3356
|
// src/commands/verify.ts
|
|
3076
|
-
var
|
|
3357
|
+
var import_zod29 = require("zod");
|
|
3077
3358
|
var verifyCommand = define({
|
|
3078
3359
|
name: "verify",
|
|
3079
3360
|
tool: "kb_verify",
|
|
3080
3361
|
usage: "verify <concept-id> --note <text>",
|
|
3081
3362
|
description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
|
|
3082
|
-
input:
|
|
3363
|
+
input: import_zod29.z.object({
|
|
3083
3364
|
bundlePath,
|
|
3084
3365
|
conceptId,
|
|
3085
|
-
note:
|
|
3366
|
+
note: import_zod29.z.string().refine((s) => s.trim().length > 0, {
|
|
3086
3367
|
message: "note must say what the check found"
|
|
3087
3368
|
})
|
|
3088
3369
|
}),
|
|
@@ -3102,7 +3383,7 @@ var verifyCommand = define({
|
|
|
3102
3383
|
});
|
|
3103
3384
|
|
|
3104
3385
|
// src/commands/write.ts
|
|
3105
|
-
var
|
|
3386
|
+
var import_zod30 = require("zod");
|
|
3106
3387
|
var writeCommand = define({
|
|
3107
3388
|
name: "write",
|
|
3108
3389
|
tool: "kb_write",
|
|
@@ -3116,9 +3397,9 @@ var writeCommand = define({
|
|
|
3116
3397
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
3117
3398
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
3118
3399
|
].join("\n"),
|
|
3119
|
-
input:
|
|
3400
|
+
input: import_zod30.z.object({
|
|
3120
3401
|
bundlePath,
|
|
3121
|
-
type:
|
|
3402
|
+
type: import_zod30.z.enum(KB_RECORD_TYPES),
|
|
3122
3403
|
input: composeInputSchema
|
|
3123
3404
|
}),
|
|
3124
3405
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3142,7 +3423,7 @@ var writeCommand = define({
|
|
|
3142
3423
|
});
|
|
3143
3424
|
|
|
3144
3425
|
// src/commands/write-decision.ts
|
|
3145
|
-
var
|
|
3426
|
+
var import_zod31 = require("zod");
|
|
3146
3427
|
var writeDecisionCommand = define({
|
|
3147
3428
|
name: "write-decision",
|
|
3148
3429
|
tool: "kb_write_decision",
|
|
@@ -3155,7 +3436,7 @@ var writeDecisionCommand = define({
|
|
|
3155
3436
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
3156
3437
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
3157
3438
|
].join("\n"),
|
|
3158
|
-
input:
|
|
3439
|
+
input: import_zod31.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3159
3440
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3160
3441
|
bundlePath: path,
|
|
3161
3442
|
input: JSON.parse(await stdin())
|
|
@@ -3185,6 +3466,7 @@ var KB_COMMANDS = [
|
|
|
3185
3466
|
answerCommand,
|
|
3186
3467
|
verifyCommand,
|
|
3187
3468
|
loadCommand,
|
|
3469
|
+
catalogCommand,
|
|
3188
3470
|
packCommand,
|
|
3189
3471
|
queryCommand,
|
|
3190
3472
|
traceCommand,
|
|
@@ -3210,7 +3492,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
3210
3492
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3211
3493
|
|
|
3212
3494
|
// src/version.ts
|
|
3213
|
-
var VERSION = true ? "0.1.
|
|
3495
|
+
var VERSION = true ? "0.1.10" : "0.0.0-dev";
|
|
3214
3496
|
|
|
3215
3497
|
// src/mcp.ts
|
|
3216
3498
|
function createKbMcpServer() {
|
|
@@ -3379,6 +3661,7 @@ function usage() {
|
|
|
3379
3661
|
KB_SLUG_PATTERN,
|
|
3380
3662
|
KbBaseFrozenError,
|
|
3381
3663
|
KbInvalidConceptIdError,
|
|
3664
|
+
KbMissingFlagValueError,
|
|
3382
3665
|
KbPackBudgetExceededError,
|
|
3383
3666
|
KbPinsMalformedError,
|
|
3384
3667
|
KbRecordAlreadyExistsError,
|
|
@@ -3397,6 +3680,7 @@ function usage() {
|
|
|
3397
3680
|
adjudicate,
|
|
3398
3681
|
assertBaseNotFrozen,
|
|
3399
3682
|
buildContext,
|
|
3683
|
+
catalog,
|
|
3400
3684
|
composeDecisionRecord,
|
|
3401
3685
|
composeInputSchema,
|
|
3402
3686
|
composeNoDecisionRecord,
|
|
@@ -3428,6 +3712,7 @@ function usage() {
|
|
|
3428
3712
|
pinBase,
|
|
3429
3713
|
readMergedPins,
|
|
3430
3714
|
readPinsLayer,
|
|
3715
|
+
renderCatalogLine,
|
|
3431
3716
|
renderIndex,
|
|
3432
3717
|
renderIndexLine,
|
|
3433
3718
|
renderLogEntry,
|