@saasontools/strauss-kb 0.1.9 → 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/dist/cli-main.cjs CHANGED
@@ -619,14 +619,162 @@ async function unpinBase(workspaceDir, bundlePath2) {
619
619
 
620
620
  // src/commands/model.ts
621
621
  var import_zod5 = require("zod");
622
+
623
+ // src/errors.ts
624
+ var BaseError = class extends Error {
625
+ code;
626
+ errorType;
627
+ fault;
628
+ retriable;
629
+ reportToUser;
630
+ details;
631
+ constructor(props) {
632
+ super(props.message);
633
+ this.name = props.name ?? this.constructor.name;
634
+ this.code = props.code ?? 500;
635
+ this.errorType = props.errorType;
636
+ this.fault = props.fault;
637
+ this.retriable = props.retriable ?? true;
638
+ this.reportToUser = props.reportToUser ?? false;
639
+ this.details = props.details;
640
+ }
641
+ };
642
+
643
+ // src/kb-errors.ts
644
+ var KbRecordAlreadyExistsError = class extends BaseError {
645
+ constructor(conceptId2) {
646
+ super({
647
+ message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
648
+ errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
649
+ code: 409,
650
+ fault: "User" /* User */,
651
+ retriable: false,
652
+ reportToUser: true,
653
+ details: { conceptId: conceptId2, action: "refused" }
654
+ });
655
+ this.conceptId = conceptId2;
656
+ }
657
+ conceptId;
658
+ };
659
+ var KbRecordNotFoundError = class extends BaseError {
660
+ constructor(conceptId2) {
661
+ super({
662
+ message: `kb: ${conceptId2} does not exist`,
663
+ errorType: "KbRecordNotFound" /* KbRecordNotFound */,
664
+ code: 404,
665
+ fault: "User" /* User */,
666
+ retriable: false,
667
+ reportToUser: true,
668
+ details: { conceptId: conceptId2 }
669
+ });
670
+ this.conceptId = conceptId2;
671
+ }
672
+ conceptId;
673
+ };
674
+ var KbWriteConflictError = class extends BaseError {
675
+ constructor(conceptId2) {
676
+ super({
677
+ message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
678
+ errorType: "KbWriteConflict" /* KbWriteConflict */,
679
+ code: 409,
680
+ fault: "System" /* System */,
681
+ retriable: true,
682
+ reportToUser: true,
683
+ details: { conceptId: conceptId2 }
684
+ });
685
+ this.conceptId = conceptId2;
686
+ }
687
+ conceptId;
688
+ };
689
+ var KbSelfVerificationError = class extends BaseError {
690
+ constructor(conceptId2, actor, generatedBy) {
691
+ super({
692
+ message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
693
+ errorType: "KbSelfVerification" /* KbSelfVerification */,
694
+ code: 400,
695
+ fault: "User" /* User */,
696
+ retriable: false,
697
+ reportToUser: true,
698
+ details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
699
+ });
700
+ this.conceptId = conceptId2;
701
+ this.actor = actor;
702
+ this.generatedBy = generatedBy;
703
+ }
704
+ conceptId;
705
+ actor;
706
+ generatedBy;
707
+ };
708
+ var KbPackBudgetExceededError = class extends BaseError {
709
+ constructor(recordCount, approxTokens2, budgetTokens, excluded) {
710
+ super({
711
+ message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
712
+ errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
713
+ code: 400,
714
+ fault: "User" /* User */,
715
+ retriable: false,
716
+ reportToUser: true,
717
+ details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
718
+ });
719
+ this.recordCount = recordCount;
720
+ this.approxTokens = approxTokens2;
721
+ this.budgetTokens = budgetTokens;
722
+ this.excluded = excluded;
723
+ }
724
+ recordCount;
725
+ approxTokens;
726
+ budgetTokens;
727
+ excluded;
728
+ };
729
+ var KbMissingFlagValueError = class extends BaseError {
730
+ constructor(flag) {
731
+ super({
732
+ message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
733
+ errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
734
+ code: 400,
735
+ fault: "User" /* User */,
736
+ retriable: false,
737
+ reportToUser: true,
738
+ details: { flag }
739
+ });
740
+ this.flag = flag;
741
+ }
742
+ flag;
743
+ };
744
+ var KbInvalidConceptIdError = class extends BaseError {
745
+ constructor(message, details) {
746
+ super({
747
+ message: `kb: ${message}`,
748
+ errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
749
+ code: 400,
750
+ fault: "User" /* User */,
751
+ retriable: false,
752
+ reportToUser: true,
753
+ details
754
+ });
755
+ }
756
+ };
757
+
758
+ // src/commands/model.ts
622
759
  var bundlePath = import_zod5.z.string().min(1).describe("Absolute path to the knowledge base directory.");
623
760
  var conceptId = import_zod5.z.string().min(1).describe("e.g. decision.cursor-v2");
624
761
  function define(command) {
625
762
  return command;
626
763
  }
627
764
  function argvFlag(argv, name) {
765
+ const joined = argv.find((arg) => arg.startsWith(`${name}=`));
766
+ if (joined !== void 0) {
767
+ const value2 = joined.slice(name.length + 1);
768
+ if (!value2) throw new KbMissingFlagValueError(name);
769
+ return value2;
770
+ }
628
771
  const at = argv.indexOf(name);
629
- return at !== -1 ? argv[at + 1] : void 0;
772
+ if (at === -1) return void 0;
773
+ const value = argv[at + 1];
774
+ if (value === void 0 || value.startsWith("--")) {
775
+ throw new KbMissingFlagValueError(name);
776
+ }
777
+ return value;
630
778
  }
631
779
 
632
780
  // src/commands/answer.ts
@@ -648,12 +796,9 @@ var answerCommand = define({
648
796
  }
649
797
  });
650
798
 
651
- // src/commands/context.ts
799
+ // src/commands/catalog.ts
652
800
  var import_zod7 = require("zod");
653
801
 
654
- // src/kb-context.ts
655
- var import_promises2 = require("fs/promises");
656
-
657
802
  // src/adjudicate.ts
658
803
  var STANDING = {
659
804
  accepted: "current",
@@ -746,6 +891,120 @@ function successors(record, byId) {
746
891
  return { records, missing };
747
892
  }
748
893
 
894
+ // src/catalog.ts
895
+ var EMPTY_STANDINGS = {
896
+ current: 0,
897
+ superseded: 0,
898
+ rejected: 0,
899
+ unsettled: 0,
900
+ open: 0
901
+ };
902
+ function catalog(bundle, options = {}) {
903
+ const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
904
+ const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
905
+ conceptId: hit.record.conceptId,
906
+ type: hit.record.frontmatter.type,
907
+ title: hit.record.frontmatter.title ?? null,
908
+ standing: hit.standing,
909
+ supersededBy: hit.heads.map((head) => head.conceptId),
910
+ stale: hit.warnings.some((warning) => warning.kind === "stale")
911
+ })).sort(byTypeThenTitle);
912
+ const standings = { ...EMPTY_STANDINGS };
913
+ for (const entry of entries) standings[entry.standing] += 1;
914
+ return {
915
+ entries,
916
+ recordCount: entries.length,
917
+ standings,
918
+ currentCount: standings.current,
919
+ supersededCount: standings.superseded,
920
+ staleCount: entries.filter((entry) => entry.stale).length
921
+ };
922
+ }
923
+ function byTypeThenTitle(left, right) {
924
+ return byCodeUnit(left.type, right.type) || byCodeUnit(left.title ?? "", right.title ?? "") || byCodeUnit(left.conceptId, right.conceptId);
925
+ }
926
+ function byCodeUnit(left, right) {
927
+ return left < right ? -1 : left > right ? 1 : 0;
928
+ }
929
+ function renderCatalogLine(entry) {
930
+ const parts = [
931
+ entry.conceptId,
932
+ entry.type,
933
+ entry.title ?? "(untitled)",
934
+ entry.standing === "superseded" ? `superseded \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}` : entry.standing
935
+ ];
936
+ if (entry.stale) parts.push("stale");
937
+ return `- ${parts.join(" \xB7 ")}`;
938
+ }
939
+
940
+ // src/commands/catalog.ts
941
+ var catalogCommand = define({
942
+ name: "catalog",
943
+ tool: "kb_catalog",
944
+ usage: "catalog [type]",
945
+ 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.",
946
+ input: import_zod7.z.object({
947
+ bundlePath,
948
+ type: import_zod7.z.enum(KB_RECORD_TYPES).optional()
949
+ }),
950
+ fromArgv: (argv, path) => ({
951
+ bundlePath: path,
952
+ ...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
953
+ }),
954
+ run: async ({ store }, { bundlePath: path, type }) => render(
955
+ await store.catalog(path, { ...type ? { type } : {} }),
956
+ path,
957
+ type
958
+ )
959
+ });
960
+ function render(result, bundle, type) {
961
+ const lines = [
962
+ `# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
963
+ `bundle: ${bundle}`,
964
+ `${count(result.recordCount, "record")}: ${standingCounts(result)}`
965
+ ];
966
+ if (result.staleCount) {
967
+ lines.push(
968
+ `${result.staleCount} stale \u2014 a flag over the standings above, not one of them`
969
+ );
970
+ }
971
+ lines.push("");
972
+ if (!result.entries.length) {
973
+ lines.push(
974
+ type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
975
+ );
976
+ } else {
977
+ for (const entry of result.entries) lines.push(renderCatalogLine(entry));
978
+ }
979
+ lines.push(
980
+ "",
981
+ "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."
982
+ );
983
+ return lines.join("\n");
984
+ }
985
+ function standingCounts(result) {
986
+ const ORDER = [
987
+ "current",
988
+ "open",
989
+ "unsettled",
990
+ "rejected",
991
+ "superseded"
992
+ ];
993
+ const parts = ORDER.filter((standing) => result.standings[standing]).map(
994
+ (standing) => `${result.standings[standing]} ${standing}`
995
+ );
996
+ return parts.length ? parts.join(" \xB7 ") : "none";
997
+ }
998
+ function count(value, noun) {
999
+ return `${value} ${value === 1 ? noun : `${noun}s`}`;
1000
+ }
1001
+
1002
+ // src/commands/context.ts
1003
+ var import_zod8 = require("zod");
1004
+
1005
+ // src/kb-context.ts
1006
+ var import_promises2 = require("fs/promises");
1007
+
749
1008
  // src/kb-index.ts
750
1009
  var INDEX_FILE = "INDEX.md";
751
1010
  var HEADING = "# KB Index";
@@ -1007,20 +1266,20 @@ var contextCommand = define({
1007
1266
  tool: "kb_context",
1008
1267
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
1009
1268
  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.",
1010
- input: import_zod7.z.object({
1011
- budgetTokens: import_zod7.z.number().int().positive().optional().describe(
1269
+ input: import_zod8.z.object({
1270
+ budgetTokens: import_zod8.z.number().int().positive().optional().describe(
1012
1271
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
1013
1272
  ),
1014
- fullUnderTokens: import_zod7.z.number().int().positive().optional().describe(
1273
+ fullUnderTokens: import_zod8.z.number().int().positive().optional().describe(
1015
1274
  "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."
1016
1275
  ),
1017
- profile: import_zod7.z.string().optional().describe(
1276
+ profile: import_zod8.z.string().optional().describe(
1018
1277
  "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."
1019
1278
  ),
1020
- format: import_zod7.z.enum(["markdown", "json"]).optional().describe(
1279
+ format: import_zod8.z.enum(["markdown", "json"]).optional().describe(
1021
1280
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
1022
1281
  ),
1023
- event: import_zod7.z.string().optional().describe(
1282
+ event: import_zod8.z.string().optional().describe(
1024
1283
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
1025
1284
  )
1026
1285
  }),
@@ -1056,7 +1315,7 @@ var contextCommand = define({
1056
1315
  });
1057
1316
 
1058
1317
  // src/commands/doctor.ts
1059
- var import_zod8 = require("zod");
1318
+ var import_zod9 = require("zod");
1060
1319
 
1061
1320
  // src/kb-edges.ts
1062
1321
  var KB_EDGE_KINDS = [
@@ -1413,13 +1672,13 @@ function ageInDays(record, now) {
1413
1672
  }
1414
1673
 
1415
1674
  // src/commands/doctor.ts
1416
- var days = (what, fallback) => import_zod8.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
1675
+ var days = (what, fallback) => import_zod9.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
1417
1676
  var doctorCommand = define({
1418
1677
  name: "doctor",
1419
1678
  tool: "kb_doctor",
1420
1679
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
1421
1680
  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.",
1422
- input: import_zod8.z.object({
1681
+ input: import_zod9.z.object({
1423
1682
  bundlePath,
1424
1683
  expiringDays: days(
1425
1684
  "How far ahead `expiring` looks, in days.",
@@ -1433,7 +1692,7 @@ var doctorCommand = define({
1433
1692
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
1434
1693
  DEFAULT_AGING_DAYS
1435
1694
  ),
1436
- strict: import_zod8.z.boolean().optional().describe(
1695
+ strict: import_zod9.z.boolean().optional().describe(
1437
1696
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
1438
1697
  )
1439
1698
  }),
@@ -1463,14 +1722,14 @@ var doctorCommand = define({
1463
1722
  });
1464
1723
  return { bundlePath: path, checkedAt, ...report };
1465
1724
  },
1466
- render: (result) => render(result),
1725
+ render: (result) => render2(result),
1467
1726
  // Only expiry, and only under --strict. The other six checks report debt a
1468
1727
  // reader decides about; an expired record is the base asserting something it
1469
1728
  // already said it would stop standing behind, which is the one finding a
1470
1729
  // pipeline can act on without a judgment call.
1471
1730
  failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
1472
1731
  });
1473
- function render(result) {
1732
+ function render2(result) {
1474
1733
  const { thresholds } = result;
1475
1734
  const lines = [
1476
1735
  `# KB Doctor \u2014 ${result.bundlePath}`,
@@ -1502,13 +1761,13 @@ function render(result) {
1502
1761
  }
1503
1762
 
1504
1763
  // src/commands/list.ts
1505
- var import_zod9 = require("zod");
1764
+ var import_zod10 = require("zod");
1506
1765
  var listCommand = define({
1507
1766
  name: "list",
1508
1767
  tool: "kb_list",
1509
1768
  usage: "list [type]",
1510
1769
  description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
1511
- input: import_zod9.z.object({ bundlePath, type: import_zod9.z.enum(KB_RECORD_TYPES).optional() }),
1770
+ input: import_zod10.z.object({ bundlePath, type: import_zod10.z.enum(KB_RECORD_TYPES).optional() }),
1512
1771
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
1513
1772
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
1514
1773
  conceptId: record.conceptId,
@@ -1520,21 +1779,21 @@ var listCommand = define({
1520
1779
  });
1521
1780
 
1522
1781
  // src/commands/load.ts
1523
- var import_zod10 = require("zod");
1782
+ var import_zod11 = require("zod");
1524
1783
  var loadCommand = define({
1525
1784
  name: "load",
1526
1785
  tool: "kb_load",
1527
- usage: "load [type] [--budget N | --all]",
1528
- description: "Load the whole knowledge base at once, each record with its standing. Prefer this over searching: these bases run to a few thousand tokens, and a reader holding all of it has perfect recall and knows why it is asking, which no ranker does. Superseded records arrive under `superseded` as name, replacement and date only \u2014 their bodies no longer hold, and reading one later in a long session is the mistake this prevents; pass the id to kb_trace when you need the history. Rejected and unresolved records arrive whole: what was turned down, and what is still open, is the part a diff cannot show you. Refuses with a count rather than truncating when the base is too large \u2014 a truncated base is indistinguishable from a complete one, and would have you conclude something was never decided from a slice you did not know was a slice. Call at the point of use, not once per session: a base loaded early is summarised away by compaction, so if the visible context holds no records from this base and the question at hand is one it might govern, load before answering \u2014 never conclude nothing was decided from a context with no KB content in it. This tool (with 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.\n\nThat refusal is the default guardrail, meant for an agent that would otherwise burn its whole context on one call. `all` bypasses it and loads everything regardless of size: a deliberate operator with the budget to spend, not something to reach for automatically. It is mutually exclusive with `budgetTokens`. When the reader does not need everything, kb_query or a narrower `type` filter is the better fit than either.",
1529
- input: import_zod10.z.object({
1786
+ usage: "load [type] [--budget N] [--all]",
1787
+ 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.",
1788
+ input: import_zod11.z.object({
1530
1789
  bundlePath,
1531
- type: import_zod10.z.enum(KB_RECORD_TYPES).optional(),
1532
- budgetTokens: import_zod10.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
1533
- all: import_zod10.z.boolean().optional().describe(
1534
- "Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
1790
+ type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
1791
+ budgetTokens: import_zod11.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
1792
+ all: import_zod11.z.boolean().optional().describe(
1793
+ "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
1535
1794
  )
1536
1795
  }).refine((value) => !(value.all && value.budgetTokens !== void 0), {
1537
- message: "all and budgetTokens are mutually exclusive: pass a ceiling or none, not both."
1796
+ message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
1538
1797
  }),
1539
1798
  fromArgv: (argv, path) => {
1540
1799
  const budget = argvFlag(argv, "--budget");
@@ -1568,25 +1827,25 @@ var loadCommand = define({
1568
1827
  });
1569
1828
 
1570
1829
  // src/commands/log.ts
1571
- var import_zod11 = require("zod");
1830
+ var import_zod12 = require("zod");
1572
1831
  var logCommand = define({
1573
1832
  name: "log",
1574
1833
  tool: "kb_log",
1575
1834
  usage: "log",
1576
1835
  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.",
1577
- input: import_zod11.z.object({ bundlePath }),
1836
+ input: import_zod12.z.object({ bundlePath }),
1578
1837
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1579
1838
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
1580
1839
  });
1581
1840
 
1582
1841
  // src/commands/no-decision.ts
1583
- var import_zod12 = require("zod");
1842
+ var import_zod13 = require("zod");
1584
1843
  var noDecisionCommand = define({
1585
1844
  name: "no-decision",
1586
1845
  tool: "kb_no_decision",
1587
1846
  usage: "no-decision <reason...>",
1588
1847
  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.',
1589
- input: import_zod12.z.object({ bundlePath, reason: import_zod12.z.string().min(1) }),
1848
+ input: import_zod13.z.object({ bundlePath, reason: import_zod13.z.string().min(1) }),
1590
1849
  fromArgv: (argv, path) => ({
1591
1850
  bundlePath: path,
1592
1851
  reason: argv.slice(1).join(" ").trim()
@@ -1603,20 +1862,20 @@ var noDecisionCommand = define({
1603
1862
  });
1604
1863
 
1605
1864
  // src/commands/pack.ts
1606
- var import_zod13 = require("zod");
1865
+ var import_zod14 = require("zod");
1607
1866
  var packCommand = define({
1608
1867
  name: "pack",
1609
1868
  tool: "kb_pack",
1610
1869
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
1611
1870
  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.",
1612
- input: import_zod13.z.object({
1871
+ input: import_zod14.z.object({
1613
1872
  bundlePath,
1614
1873
  conceptId,
1615
- hops: import_zod13.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
1616
- maxNodes: import_zod13.z.number().int().positive().optional().describe(
1874
+ hops: import_zod14.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
1875
+ maxNodes: import_zod14.z.number().int().positive().optional().describe(
1617
1876
  "How many records the pack may hold, root included. Defaults to 20."
1618
1877
  ),
1619
- budgetTokens: import_zod13.z.number().int().positive().optional().describe(
1878
+ budgetTokens: import_zod14.z.number().int().positive().optional().describe(
1620
1879
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
1621
1880
  )
1622
1881
  }),
@@ -1638,10 +1897,10 @@ var packCommand = define({
1638
1897
  ...maxNodes !== void 0 ? { maxNodes } : {},
1639
1898
  ...budgetTokens !== void 0 ? { budgetTokens } : {}
1640
1899
  });
1641
- return render2(result, path, now());
1900
+ return render3(result, path, now());
1642
1901
  }
1643
1902
  });
1644
- function render2(result, bundle, at) {
1903
+ function render3(result, bundle, at) {
1645
1904
  const lines = [
1646
1905
  `# KB Pack \u2014 ${result.root}`,
1647
1906
  `bundle: ${bundle}`,
@@ -1703,22 +1962,22 @@ function warningLabel(warning) {
1703
1962
  }
1704
1963
 
1705
1964
  // src/commands/pin.ts
1706
- var import_zod14 = require("zod");
1965
+ var import_zod15 = require("zod");
1707
1966
  var pinCommand = define({
1708
1967
  name: "pin",
1709
1968
  tool: "kb_pin",
1710
1969
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
1711
1970
  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.",
1712
- input: import_zod14.z.object({
1971
+ input: import_zod15.z.object({
1713
1972
  bundlePath,
1714
- mode: import_zod14.z.enum(["full", "index"]).optional().describe(
1973
+ mode: import_zod15.z.enum(["full", "index"]).optional().describe(
1715
1974
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
1716
1975
  ),
1717
- profiles: import_zod14.z.array(import_zod14.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1718
- layer: import_zod14.z.enum(["project", "local", "user"]).optional().describe(
1976
+ profiles: import_zod15.z.array(import_zod15.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1977
+ layer: import_zod15.z.enum(["project", "local", "user"]).optional().describe(
1719
1978
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
1720
1979
  ),
1721
- frozen: import_zod14.z.boolean().optional().describe(
1980
+ frozen: import_zod15.z.boolean().optional().describe(
1722
1981
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
1723
1982
  )
1724
1983
  }),
@@ -1747,29 +2006,29 @@ var pinCommand = define({
1747
2006
  });
1748
2007
 
1749
2008
  // src/commands/pins.ts
1750
- var import_zod15 = require("zod");
2009
+ var import_zod16 = require("zod");
1751
2010
  var pinsCommand = define({
1752
2011
  name: "pins",
1753
2012
  tool: "kb_pins",
1754
2013
  usage: "pins",
1755
2014
  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.",
1756
- input: import_zod15.z.object({}),
2015
+ input: import_zod16.z.object({}),
1757
2016
  fromArgv: () => ({}),
1758
2017
  run: ({ store }) => listPins(store, process.cwd())
1759
2018
  });
1760
2019
 
1761
2020
  // src/commands/query.ts
1762
- var import_zod16 = require("zod");
2021
+ var import_zod17 = require("zod");
1763
2022
  var queryCommand = define({
1764
2023
  name: "query",
1765
2024
  tool: "kb_query",
1766
2025
  usage: "query <text...>",
1767
- 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. Prefer kb_load when the base fits its budget: 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. Never read record files directly \u2014 this tool (with kb_load 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.",
1768
- input: import_zod16.z.object({
2026
+ 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.",
2027
+ input: import_zod17.z.object({
1769
2028
  bundlePath,
1770
- text: import_zod16.z.string().optional(),
1771
- type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
1772
- includeNonCurrent: import_zod16.z.boolean().optional()
2029
+ text: import_zod17.z.string().optional(),
2030
+ type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
2031
+ includeNonCurrent: import_zod17.z.boolean().optional()
1773
2032
  }),
1774
2033
  fromArgv: (argv, path) => ({
1775
2034
  bundlePath: path,
@@ -1791,27 +2050,27 @@ var queryCommand = define({
1791
2050
  });
1792
2051
 
1793
2052
  // src/commands/read-index.ts
1794
- var import_zod17 = require("zod");
2053
+ var import_zod18 = require("zod");
1795
2054
  var readIndexCommand = define({
1796
2055
  name: "index",
1797
2056
  tool: "kb_index",
1798
2057
  usage: "index",
1799
2058
  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.",
1800
- input: import_zod17.z.object({ bundlePath }),
2059
+ input: import_zod18.z.object({ bundlePath }),
1801
2060
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1802
2061
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
1803
2062
  });
1804
2063
 
1805
2064
  // src/commands/schema.ts
1806
- var import_zod20 = require("zod");
2065
+ var import_zod21 = require("zod");
1807
2066
 
1808
2067
  // src/json-schema.ts
1809
- var import_zod19 = require("zod");
2068
+ var import_zod20 = require("zod");
1810
2069
 
1811
2070
  // src/kb-log.ts
1812
- var import_zod18 = require("zod");
2071
+ var import_zod19 = require("zod");
1813
2072
  var LOG_FILE = "log.jsonl";
1814
- var kbLogEntrySchema = import_zod18.z.object({
2073
+ var kbLogEntrySchema = import_zod19.z.object({
1815
2074
  // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
1816
2075
  // below), and a value that isn't actually chronological — a Unix
1817
2076
  // timestamp, a human-typed date, garbage — would sort wrong without
@@ -1820,12 +2079,12 @@ var kbLogEntrySchema = import_zod18.z.object({
1820
2079
  // and rejects everything else, including a non-`Z` offset — so a
1821
2080
  // malformed `at` is reported the same way a malformed line already is,
1822
2081
  // rather than silently sorting into the wrong place.
1823
- at: import_zod18.z.iso.datetime(),
1824
- by: import_zod18.z.string().min(1),
1825
- operation: import_zod18.z.string().min(1),
1826
- conceptId: import_zod18.z.string().min(1),
2082
+ at: import_zod19.z.iso.datetime(),
2083
+ by: import_zod19.z.string().min(1),
2084
+ operation: import_zod19.z.string().min(1),
2085
+ conceptId: import_zod19.z.string().min(1),
1827
2086
  /** Second concept id, where the operation relates two — supersession. */
1828
- target: import_zod18.z.string().min(1).optional()
2087
+ target: import_zod19.z.string().min(1).optional()
1829
2088
  }).strict();
1830
2089
  function renderLogEntry(entry) {
1831
2090
  return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
@@ -1863,11 +2122,11 @@ function parseLog(raw) {
1863
2122
  // src/json-schema.ts
1864
2123
  function kbJsonSchemas() {
1865
2124
  return {
1866
- recordFrontmatter: import_zod19.z.toJSONSchema(kbRecordFrontmatterSchema, {
2125
+ recordFrontmatter: import_zod20.z.toJSONSchema(kbRecordFrontmatterSchema, {
1867
2126
  io: "input"
1868
2127
  }),
1869
- composeInput: import_zod19.z.toJSONSchema(composeInputSchema, { io: "input" }),
1870
- logEntry: import_zod19.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
2128
+ composeInput: import_zod20.z.toJSONSchema(composeInputSchema, { io: "input" }),
2129
+ logEntry: import_zod20.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
1871
2130
  };
1872
2131
  }
1873
2132
 
@@ -1877,22 +2136,22 @@ var schemaCommand = define({
1877
2136
  tool: "kb_schema",
1878
2137
  usage: "schema",
1879
2138
  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.",
1880
- input: import_zod20.z.object({}),
2139
+ input: import_zod21.z.object({}),
1881
2140
  fromArgv: () => ({}),
1882
2141
  run: () => Promise.resolve(kbJsonSchemas())
1883
2142
  });
1884
2143
 
1885
2144
  // src/commands/status.ts
1886
- var import_zod21 = require("zod");
2145
+ var import_zod22 = require("zod");
1887
2146
  var statusCommand = define({
1888
2147
  name: "status",
1889
2148
  tool: "kb_status",
1890
2149
  usage: "status <concept-id> <status>",
1891
2150
  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.",
1892
- input: import_zod21.z.object({
2151
+ input: import_zod22.z.object({
1893
2152
  bundlePath,
1894
2153
  conceptId,
1895
- status: import_zod21.z.enum(KB_RECORD_STATUSES)
2154
+ status: import_zod22.z.enum(KB_RECORD_STATUSES)
1896
2155
  }),
1897
2156
  fromArgv: (argv, path) => ({
1898
2157
  bundlePath: path,
@@ -1907,13 +2166,13 @@ var statusCommand = define({
1907
2166
  });
1908
2167
 
1909
2168
  // src/commands/supersede.ts
1910
- var import_zod22 = require("zod");
2169
+ var import_zod23 = require("zod");
1911
2170
  var supersedeCommand = define({
1912
2171
  name: "supersede",
1913
2172
  tool: "kb_supersede",
1914
2173
  usage: "supersede <concept-id> <replacement-id>",
1915
2174
  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.",
1916
- input: import_zod22.z.object({ bundlePath, conceptId, replacementId: conceptId }),
2175
+ input: import_zod23.z.object({ bundlePath, conceptId, replacementId: conceptId }),
1917
2176
  fromArgv: (argv, path) => ({
1918
2177
  bundlePath: path,
1919
2178
  conceptId: argv[1],
@@ -1927,16 +2186,16 @@ var supersedeCommand = define({
1927
2186
  });
1928
2187
 
1929
2188
  // src/commands/sync-instructions.ts
1930
- var import_zod23 = require("zod");
2189
+ var import_zod24 = require("zod");
1931
2190
  var syncInstructionsCommand = define({
1932
2191
  name: "sync-instructions",
1933
2192
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
1934
2193
  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.",
1935
- input: import_zod23.z.object({
1936
- file: import_zod23.z.string().min(1).describe("The instruction file to edit in place."),
1937
- budgetTokens: import_zod23.z.number().int().positive().optional(),
1938
- fullUnderTokens: import_zod23.z.number().int().positive().optional(),
1939
- profile: import_zod23.z.string().optional()
2194
+ input: import_zod24.z.object({
2195
+ file: import_zod24.z.string().min(1).describe("The instruction file to edit in place."),
2196
+ budgetTokens: import_zod24.z.number().int().positive().optional(),
2197
+ fullUnderTokens: import_zod24.z.number().int().positive().optional(),
2198
+ profile: import_zod24.z.string().optional()
1940
2199
  }),
1941
2200
  fromArgv: (argv) => {
1942
2201
  const budget = argvFlag(argv, "--budget");
@@ -1962,7 +2221,7 @@ var syncInstructionsCommand = define({
1962
2221
  });
1963
2222
 
1964
2223
  // src/commands/trace.ts
1965
- var import_zod24 = require("zod");
2224
+ var import_zod25 = require("zod");
1966
2225
 
1967
2226
  // src/trace.ts
1968
2227
  var TRACE_EDGES = ["supersession", "anchor", "source"];
@@ -2008,11 +2267,11 @@ var traceCommand = define({
2008
2267
  tool: "kb_trace",
2009
2268
  usage: "trace <concept-id> [edges...]",
2010
2269
  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.',
2011
- input: import_zod24.z.object({
2270
+ input: import_zod25.z.object({
2012
2271
  bundlePath,
2013
2272
  conceptId,
2014
- edges: import_zod24.z.array(import_zod24.z.enum(TRACE_EDGES)).optional(),
2015
- depth: import_zod24.z.number().int().positive().optional()
2273
+ edges: import_zod25.z.array(import_zod25.z.enum(TRACE_EDGES)).optional(),
2274
+ depth: import_zod25.z.number().int().positive().optional()
2016
2275
  }),
2017
2276
  fromArgv: (argv, path) => ({
2018
2277
  bundlePath: path,
@@ -2034,53 +2293,53 @@ var traceCommand = define({
2034
2293
  });
2035
2294
 
2036
2295
  // src/commands/types.ts
2037
- var import_zod25 = require("zod");
2296
+ var import_zod26 = require("zod");
2038
2297
  var typesCommand = define({
2039
2298
  name: "types",
2040
2299
  tool: "kb_types",
2041
2300
  usage: "types",
2042
2301
  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.",
2043
- input: import_zod25.z.object({}),
2302
+ input: import_zod26.z.object({}),
2044
2303
  fromArgv: () => ({}),
2045
2304
  run: () => Promise.resolve(RECORD_TYPES)
2046
2305
  });
2047
2306
 
2048
2307
  // src/commands/unpin.ts
2049
- var import_zod26 = require("zod");
2308
+ var import_zod27 = require("zod");
2050
2309
  var unpinCommand = define({
2051
2310
  name: "unpin",
2052
2311
  tool: "kb_unpin",
2053
2312
  usage: "unpin [bundle-path]",
2054
2313
  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.",
2055
- input: import_zod26.z.object({ bundlePath }),
2314
+ input: import_zod27.z.object({ bundlePath }),
2056
2315
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
2057
2316
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
2058
2317
  });
2059
2318
 
2060
2319
  // src/commands/validate.ts
2061
- var import_zod27 = require("zod");
2320
+ var import_zod28 = require("zod");
2062
2321
  var validateCommand = define({
2063
2322
  name: "validate",
2064
2323
  tool: "kb_validate",
2065
2324
  usage: "validate",
2066
2325
  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.",
2067
- input: import_zod27.z.object({ bundlePath }),
2326
+ input: import_zod28.z.object({ bundlePath }),
2068
2327
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2069
2328
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
2070
2329
  failsWhen: (result) => Array.isArray(result) && result.length > 0
2071
2330
  });
2072
2331
 
2073
2332
  // src/commands/verify.ts
2074
- var import_zod28 = require("zod");
2333
+ var import_zod29 = require("zod");
2075
2334
  var verifyCommand = define({
2076
2335
  name: "verify",
2077
2336
  tool: "kb_verify",
2078
2337
  usage: "verify <concept-id> --note <text>",
2079
2338
  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.",
2080
- input: import_zod28.z.object({
2339
+ input: import_zod29.z.object({
2081
2340
  bundlePath,
2082
2341
  conceptId,
2083
- note: import_zod28.z.string().refine((s) => s.trim().length > 0, {
2342
+ note: import_zod29.z.string().refine((s) => s.trim().length > 0, {
2084
2343
  message: "note must say what the check found"
2085
2344
  })
2086
2345
  }),
@@ -2100,7 +2359,7 @@ var verifyCommand = define({
2100
2359
  });
2101
2360
 
2102
2361
  // src/commands/write.ts
2103
- var import_zod29 = require("zod");
2362
+ var import_zod30 = require("zod");
2104
2363
  var writeCommand = define({
2105
2364
  name: "write",
2106
2365
  tool: "kb_write",
@@ -2114,9 +2373,9 @@ var writeCommand = define({
2114
2373
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
2115
2374
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
2116
2375
  ].join("\n"),
2117
- input: import_zod29.z.object({
2376
+ input: import_zod30.z.object({
2118
2377
  bundlePath,
2119
- type: import_zod29.z.enum(KB_RECORD_TYPES),
2378
+ type: import_zod30.z.enum(KB_RECORD_TYPES),
2120
2379
  input: composeInputSchema
2121
2380
  }),
2122
2381
  fromArgv: async (argv, path, stdin) => ({
@@ -2140,7 +2399,7 @@ var writeCommand = define({
2140
2399
  });
2141
2400
 
2142
2401
  // src/commands/write-decision.ts
2143
- var import_zod30 = require("zod");
2402
+ var import_zod31 = require("zod");
2144
2403
  var writeDecisionCommand = define({
2145
2404
  name: "write-decision",
2146
2405
  tool: "kb_write_decision",
@@ -2153,7 +2412,7 @@ var writeDecisionCommand = define({
2153
2412
  "- `alternative` is what you turned down and why, not a list of everything considered.",
2154
2413
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
2155
2414
  ].join("\n"),
2156
- input: import_zod30.z.object({ bundlePath, input: decisionInputSchema }),
2415
+ input: import_zod31.z.object({ bundlePath, input: decisionInputSchema }),
2157
2416
  fromArgv: async (_argv, path, stdin) => ({
2158
2417
  bundlePath: path,
2159
2418
  input: JSON.parse(await stdin())
@@ -2183,6 +2442,7 @@ var KB_COMMANDS = [
2183
2442
  answerCommand,
2184
2443
  verifyCommand,
2185
2444
  loadCommand,
2445
+ catalogCommand,
2186
2446
  packCommand,
2187
2447
  queryCommand,
2188
2448
  traceCommand,
@@ -2233,126 +2493,6 @@ function parseMarkdownWithFrontmatter(text, schema) {
2233
2493
  };
2234
2494
  }
2235
2495
 
2236
- // src/errors.ts
2237
- var BaseError = class extends Error {
2238
- code;
2239
- errorType;
2240
- fault;
2241
- retriable;
2242
- reportToUser;
2243
- details;
2244
- constructor(props) {
2245
- super(props.message);
2246
- this.name = props.name ?? this.constructor.name;
2247
- this.code = props.code ?? 500;
2248
- this.errorType = props.errorType;
2249
- this.fault = props.fault;
2250
- this.retriable = props.retriable ?? true;
2251
- this.reportToUser = props.reportToUser ?? false;
2252
- this.details = props.details;
2253
- }
2254
- };
2255
-
2256
- // src/kb-errors.ts
2257
- var KbRecordAlreadyExistsError = class extends BaseError {
2258
- constructor(conceptId2) {
2259
- super({
2260
- message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
2261
- errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
2262
- code: 409,
2263
- fault: "User" /* User */,
2264
- retriable: false,
2265
- reportToUser: true,
2266
- details: { conceptId: conceptId2, action: "refused" }
2267
- });
2268
- this.conceptId = conceptId2;
2269
- }
2270
- conceptId;
2271
- };
2272
- var KbRecordNotFoundError = class extends BaseError {
2273
- constructor(conceptId2) {
2274
- super({
2275
- message: `kb: ${conceptId2} does not exist`,
2276
- errorType: "KbRecordNotFound" /* KbRecordNotFound */,
2277
- code: 404,
2278
- fault: "User" /* User */,
2279
- retriable: false,
2280
- reportToUser: true,
2281
- details: { conceptId: conceptId2 }
2282
- });
2283
- this.conceptId = conceptId2;
2284
- }
2285
- conceptId;
2286
- };
2287
- var KbWriteConflictError = class extends BaseError {
2288
- constructor(conceptId2) {
2289
- super({
2290
- message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
2291
- errorType: "KbWriteConflict" /* KbWriteConflict */,
2292
- code: 409,
2293
- fault: "System" /* System */,
2294
- retriable: true,
2295
- reportToUser: true,
2296
- details: { conceptId: conceptId2 }
2297
- });
2298
- this.conceptId = conceptId2;
2299
- }
2300
- conceptId;
2301
- };
2302
- var KbSelfVerificationError = class extends BaseError {
2303
- constructor(conceptId2, actor, generatedBy) {
2304
- super({
2305
- message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
2306
- errorType: "KbSelfVerification" /* KbSelfVerification */,
2307
- code: 400,
2308
- fault: "User" /* User */,
2309
- retriable: false,
2310
- reportToUser: true,
2311
- details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
2312
- });
2313
- this.conceptId = conceptId2;
2314
- this.actor = actor;
2315
- this.generatedBy = generatedBy;
2316
- }
2317
- conceptId;
2318
- actor;
2319
- generatedBy;
2320
- };
2321
- var KbPackBudgetExceededError = class extends BaseError {
2322
- constructor(recordCount, approxTokens2, budgetTokens, excluded) {
2323
- super({
2324
- message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
2325
- errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
2326
- code: 400,
2327
- fault: "User" /* User */,
2328
- retriable: false,
2329
- reportToUser: true,
2330
- details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
2331
- });
2332
- this.recordCount = recordCount;
2333
- this.approxTokens = approxTokens2;
2334
- this.budgetTokens = budgetTokens;
2335
- this.excluded = excluded;
2336
- }
2337
- recordCount;
2338
- approxTokens;
2339
- budgetTokens;
2340
- excluded;
2341
- };
2342
- var KbInvalidConceptIdError = class extends BaseError {
2343
- constructor(message, details) {
2344
- super({
2345
- message: `kb: ${message}`,
2346
- errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
2347
- code: 400,
2348
- fault: "User" /* User */,
2349
- retriable: false,
2350
- reportToUser: true,
2351
- details
2352
- });
2353
- }
2354
- };
2355
-
2356
2496
  // src/search-index.ts
2357
2497
  var import_promises3 = require("fs/promises");
2358
2498
  var import_node_path5 = require("path");
@@ -2803,9 +2943,16 @@ ${answer}
2803
2943
  * is indistinguishable from a complete one, so a caller would answer "that
2804
2944
  * was never decided" from a slice it did not know was a slice.
2805
2945
  *
2806
- * That refusal is the default guardrail. `all` bypasses it outright and
2807
- * always hands back the whole bundle: an explicit, never-accidental escape
2808
- * hatch for an operator who has the budget to spend, not a wider default.
2946
+ * A token budget decides that, measured over what is actually handed back.
2947
+ * The refusal names the estimate and the budget, because a caller told only
2948
+ * "too big" cannot tell whether to narrow the type filter, raise the budget,
2949
+ * or stop loading the base whole altogether. Past the budget the answer is
2950
+ * the catalog and then a pack, which is what the refusal says.
2951
+ *
2952
+ * That refusal is the default guardrail. `all` bypasses the budget outright
2953
+ * and always hands back the whole bundle: an explicit, never-accidental
2954
+ * escape hatch for an operator who has the budget to spend, not a wider
2955
+ * default.
2809
2956
  */
2810
2957
  async load(bundlePath2, options = {}) {
2811
2958
  const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
@@ -2820,7 +2967,12 @@ ${answer}
2820
2967
  loaded: false,
2821
2968
  recordCount: wanted.length,
2822
2969
  approxTokens: approxTokens2,
2823
- budgetTokens
2970
+ budgetTokens,
2971
+ message: refusalMessage({
2972
+ approxTokens: approxTokens2,
2973
+ budgetTokens,
2974
+ type: options.type
2975
+ })
2824
2976
  };
2825
2977
  }
2826
2978
  return {
@@ -2836,6 +2988,10 @@ ${answer}
2836
2988
  async trace(bundlePath2, seedId, options = {}) {
2837
2989
  return trace(seedId, await this.list(bundlePath2), options);
2838
2990
  }
2991
+ /** Every record named in one line each. See `catalog.ts`. */
2992
+ async catalog(bundlePath2, options = {}) {
2993
+ return catalog(await this.list(bundlePath2), options);
2994
+ }
2839
2995
  /** A bounded neighbourhood around one record. See `pack.ts`. */
2840
2996
  async pack(bundlePath2, rootId, options = {}) {
2841
2997
  return pack(await this.list(bundlePath2), rootId, options);
@@ -3102,6 +3258,14 @@ function estimateTokens(record) {
3102
3258
  function estimateStubTokens(entry) {
3103
3259
  return Math.ceil(JSON.stringify(entry).length / 4);
3104
3260
  }
3261
+ function refusalMessage(refusal) {
3262
+ const scope = refusal.type ? ` of type ${refusal.type}` : "";
3263
+ return [
3264
+ `Refusing to load this base whole: ~${refusal.approxTokens} tokens is past the ${refusal.budgetTokens}-token budget.`,
3265
+ `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.`,
3266
+ `To load anyway: raise budgetTokens (currently ${refusal.budgetTokens}), or all=true to bypass the budget.`
3267
+ ].join(" ");
3268
+ }
3105
3269
  function stub(hit) {
3106
3270
  return {
3107
3271
  conceptId: hit.record.conceptId,
@@ -3126,7 +3290,7 @@ function digest(contents) {
3126
3290
  }
3127
3291
 
3128
3292
  // src/version.ts
3129
- var VERSION = true ? "0.1.9" : "0.0.0-dev";
3293
+ var VERSION = true ? "0.1.10" : "0.0.0-dev";
3130
3294
 
3131
3295
  // src/cli.ts
3132
3296
  async function runKbCli(argv) {