@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/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({
@@ -833,6 +852,52 @@ function typeRank(record) {
833
852
  return index === -1 ? TYPE_PRIORITY.length : index;
834
853
  }
835
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
+
836
901
  // src/kb-gitattributes.ts
837
902
  var GITATTRIBUTES_FILE = ".gitattributes";
838
903
  var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
@@ -1113,9 +1178,16 @@ ${answer}
1113
1178
  * is indistinguishable from a complete one, so a caller would answer "that
1114
1179
  * was never decided" from a slice it did not know was a slice.
1115
1180
  *
1116
- * That refusal is the default guardrail. `all` bypasses it outright and
1117
- * always hands back the whole bundle: an explicit, never-accidental escape
1118
- * hatch for an operator who has the budget to spend, not a wider default.
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.
1119
1191
  */
1120
1192
  async load(bundlePath2, options = {}) {
1121
1193
  const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
@@ -1130,7 +1202,12 @@ ${answer}
1130
1202
  loaded: false,
1131
1203
  recordCount: wanted.length,
1132
1204
  approxTokens: approxTokens2,
1133
- budgetTokens
1205
+ budgetTokens,
1206
+ message: refusalMessage({
1207
+ approxTokens: approxTokens2,
1208
+ budgetTokens,
1209
+ type: options.type
1210
+ })
1134
1211
  };
1135
1212
  }
1136
1213
  return {
@@ -1146,6 +1223,10 @@ ${answer}
1146
1223
  async trace(bundlePath2, seedId, options = {}) {
1147
1224
  return trace(seedId, await this.list(bundlePath2), options);
1148
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
+ }
1149
1230
  /** A bounded neighbourhood around one record. See `pack.ts`. */
1150
1231
  async pack(bundlePath2, rootId, options = {}) {
1151
1232
  return pack(await this.list(bundlePath2), rootId, options);
@@ -1412,6 +1493,14 @@ function estimateTokens(record) {
1412
1493
  function estimateStubTokens(entry) {
1413
1494
  return Math.ceil(JSON.stringify(entry).length / 4);
1414
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
+ }
1415
1504
  function stub(hit) {
1416
1505
  return {
1417
1506
  conceptId: hit.record.conceptId,
@@ -2561,8 +2650,19 @@ function define(command) {
2561
2650
  return command;
2562
2651
  }
2563
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
+ }
2564
2659
  const at = argv.indexOf(name);
2565
- return at !== -1 ? argv[at + 1] : void 0;
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;
2566
2666
  }
2567
2667
 
2568
2668
  // src/commands/answer.ts
@@ -2584,27 +2684,90 @@ var answerCommand = define({
2584
2684
  }
2585
2685
  });
2586
2686
 
2587
- // src/commands/context.ts
2687
+ // src/commands/catalog.ts
2588
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");
2589
2752
  var contextCommand = define({
2590
2753
  name: "context",
2591
2754
  tool: "kb_context",
2592
2755
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
2593
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.",
2594
- input: import_zod9.z.object({
2595
- budgetTokens: import_zod9.z.number().int().positive().optional().describe(
2757
+ input: import_zod10.z.object({
2758
+ budgetTokens: import_zod10.z.number().int().positive().optional().describe(
2596
2759
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
2597
2760
  ),
2598
- fullUnderTokens: import_zod9.z.number().int().positive().optional().describe(
2761
+ fullUnderTokens: import_zod10.z.number().int().positive().optional().describe(
2599
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."
2600
2763
  ),
2601
- profile: import_zod9.z.string().optional().describe(
2764
+ profile: import_zod10.z.string().optional().describe(
2602
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."
2603
2766
  ),
2604
- format: import_zod9.z.enum(["markdown", "json"]).optional().describe(
2767
+ format: import_zod10.z.enum(["markdown", "json"]).optional().describe(
2605
2768
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
2606
2769
  ),
2607
- event: import_zod9.z.string().optional().describe(
2770
+ event: import_zod10.z.string().optional().describe(
2608
2771
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
2609
2772
  )
2610
2773
  }),
@@ -2640,14 +2803,14 @@ var contextCommand = define({
2640
2803
  });
2641
2804
 
2642
2805
  // src/commands/doctor.ts
2643
- var import_zod10 = require("zod");
2644
- var days = (what, fallback) => import_zod10.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
2806
+ var import_zod11 = require("zod");
2807
+ var days = (what, fallback) => import_zod11.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
2645
2808
  var doctorCommand = define({
2646
2809
  name: "doctor",
2647
2810
  tool: "kb_doctor",
2648
2811
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
2649
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.",
2650
- input: import_zod10.z.object({
2813
+ input: import_zod11.z.object({
2651
2814
  bundlePath,
2652
2815
  expiringDays: days(
2653
2816
  "How far ahead `expiring` looks, in days.",
@@ -2661,7 +2824,7 @@ var doctorCommand = define({
2661
2824
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
2662
2825
  DEFAULT_AGING_DAYS
2663
2826
  ),
2664
- strict: import_zod10.z.boolean().optional().describe(
2827
+ strict: import_zod11.z.boolean().optional().describe(
2665
2828
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
2666
2829
  )
2667
2830
  }),
@@ -2691,14 +2854,14 @@ var doctorCommand = define({
2691
2854
  });
2692
2855
  return { bundlePath: path, checkedAt, ...report };
2693
2856
  },
2694
- render: (result) => render(result),
2857
+ render: (result) => render2(result),
2695
2858
  // Only expiry, and only under --strict. The other six checks report debt a
2696
2859
  // reader decides about; an expired record is the base asserting something it
2697
2860
  // already said it would stop standing behind, which is the one finding a
2698
2861
  // pipeline can act on without a judgment call.
2699
2862
  failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
2700
2863
  });
2701
- function render(result) {
2864
+ function render2(result) {
2702
2865
  const { thresholds } = result;
2703
2866
  const lines = [
2704
2867
  `# KB Doctor \u2014 ${result.bundlePath}`,
@@ -2730,13 +2893,13 @@ function render(result) {
2730
2893
  }
2731
2894
 
2732
2895
  // src/commands/list.ts
2733
- var import_zod11 = require("zod");
2896
+ var import_zod12 = require("zod");
2734
2897
  var listCommand = define({
2735
2898
  name: "list",
2736
2899
  tool: "kb_list",
2737
2900
  usage: "list [type]",
2738
2901
  description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
2739
- input: import_zod11.z.object({ bundlePath, type: import_zod11.z.enum(KB_RECORD_TYPES).optional() }),
2902
+ input: import_zod12.z.object({ bundlePath, type: import_zod12.z.enum(KB_RECORD_TYPES).optional() }),
2740
2903
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
2741
2904
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
2742
2905
  conceptId: record.conceptId,
@@ -2748,21 +2911,21 @@ var listCommand = define({
2748
2911
  });
2749
2912
 
2750
2913
  // src/commands/load.ts
2751
- var import_zod12 = require("zod");
2914
+ var import_zod13 = require("zod");
2752
2915
  var loadCommand = define({
2753
2916
  name: "load",
2754
2917
  tool: "kb_load",
2755
- usage: "load [type] [--budget N | --all]",
2756
- 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.",
2757
- input: import_zod12.z.object({
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({
2758
2921
  bundlePath,
2759
- type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
2760
- budgetTokens: import_zod12.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
2761
- all: import_zod12.z.boolean().optional().describe(
2762
- "Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
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."
2763
2926
  )
2764
2927
  }).refine((value) => !(value.all && value.budgetTokens !== void 0), {
2765
- message: "all and budgetTokens are mutually exclusive: pass a ceiling or none, not both."
2928
+ message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
2766
2929
  }),
2767
2930
  fromArgv: (argv, path) => {
2768
2931
  const budget = argvFlag(argv, "--budget");
@@ -2796,25 +2959,25 @@ var loadCommand = define({
2796
2959
  });
2797
2960
 
2798
2961
  // src/commands/log.ts
2799
- var import_zod13 = require("zod");
2962
+ var import_zod14 = require("zod");
2800
2963
  var logCommand = define({
2801
2964
  name: "log",
2802
2965
  tool: "kb_log",
2803
2966
  usage: "log",
2804
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.",
2805
- input: import_zod13.z.object({ bundlePath }),
2968
+ input: import_zod14.z.object({ bundlePath }),
2806
2969
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2807
2970
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
2808
2971
  });
2809
2972
 
2810
2973
  // src/commands/no-decision.ts
2811
- var import_zod14 = require("zod");
2974
+ var import_zod15 = require("zod");
2812
2975
  var noDecisionCommand = define({
2813
2976
  name: "no-decision",
2814
2977
  tool: "kb_no_decision",
2815
2978
  usage: "no-decision <reason...>",
2816
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.',
2817
- input: import_zod14.z.object({ bundlePath, reason: import_zod14.z.string().min(1) }),
2980
+ input: import_zod15.z.object({ bundlePath, reason: import_zod15.z.string().min(1) }),
2818
2981
  fromArgv: (argv, path) => ({
2819
2982
  bundlePath: path,
2820
2983
  reason: argv.slice(1).join(" ").trim()
@@ -2831,20 +2994,20 @@ var noDecisionCommand = define({
2831
2994
  });
2832
2995
 
2833
2996
  // src/commands/pack.ts
2834
- var import_zod15 = require("zod");
2997
+ var import_zod16 = require("zod");
2835
2998
  var packCommand = define({
2836
2999
  name: "pack",
2837
3000
  tool: "kb_pack",
2838
3001
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
2839
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.",
2840
- input: import_zod15.z.object({
3003
+ input: import_zod16.z.object({
2841
3004
  bundlePath,
2842
3005
  conceptId,
2843
- hops: import_zod15.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
2844
- maxNodes: import_zod15.z.number().int().positive().optional().describe(
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(
2845
3008
  "How many records the pack may hold, root included. Defaults to 20."
2846
3009
  ),
2847
- budgetTokens: import_zod15.z.number().int().positive().optional().describe(
3010
+ budgetTokens: import_zod16.z.number().int().positive().optional().describe(
2848
3011
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
2849
3012
  )
2850
3013
  }),
@@ -2866,10 +3029,10 @@ var packCommand = define({
2866
3029
  ...maxNodes !== void 0 ? { maxNodes } : {},
2867
3030
  ...budgetTokens !== void 0 ? { budgetTokens } : {}
2868
3031
  });
2869
- return render2(result, path, now());
3032
+ return render3(result, path, now());
2870
3033
  }
2871
3034
  });
2872
- function render2(result, bundle, at) {
3035
+ function render3(result, bundle, at) {
2873
3036
  const lines = [
2874
3037
  `# KB Pack \u2014 ${result.root}`,
2875
3038
  `bundle: ${bundle}`,
@@ -2931,22 +3094,22 @@ function warningLabel(warning) {
2931
3094
  }
2932
3095
 
2933
3096
  // src/commands/pin.ts
2934
- var import_zod16 = require("zod");
3097
+ var import_zod17 = require("zod");
2935
3098
  var pinCommand = define({
2936
3099
  name: "pin",
2937
3100
  tool: "kb_pin",
2938
3101
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
2939
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.",
2940
- input: import_zod16.z.object({
3103
+ input: import_zod17.z.object({
2941
3104
  bundlePath,
2942
- mode: import_zod16.z.enum(["full", "index"]).optional().describe(
3105
+ mode: import_zod17.z.enum(["full", "index"]).optional().describe(
2943
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."
2944
3107
  ),
2945
- profiles: import_zod16.z.array(import_zod16.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
2946
- layer: import_zod16.z.enum(["project", "local", "user"]).optional().describe(
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(
2947
3110
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
2948
3111
  ),
2949
- frozen: import_zod16.z.boolean().optional().describe(
3112
+ frozen: import_zod17.z.boolean().optional().describe(
2950
3113
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
2951
3114
  )
2952
3115
  }),
@@ -2975,29 +3138,29 @@ var pinCommand = define({
2975
3138
  });
2976
3139
 
2977
3140
  // src/commands/pins.ts
2978
- var import_zod17 = require("zod");
3141
+ var import_zod18 = require("zod");
2979
3142
  var pinsCommand = define({
2980
3143
  name: "pins",
2981
3144
  tool: "kb_pins",
2982
3145
  usage: "pins",
2983
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.",
2984
- input: import_zod17.z.object({}),
3147
+ input: import_zod18.z.object({}),
2985
3148
  fromArgv: () => ({}),
2986
3149
  run: ({ store }) => listPins(store, process.cwd())
2987
3150
  });
2988
3151
 
2989
3152
  // src/commands/query.ts
2990
- var import_zod18 = require("zod");
3153
+ var import_zod19 = require("zod");
2991
3154
  var queryCommand = define({
2992
3155
  name: "query",
2993
3156
  tool: "kb_query",
2994
3157
  usage: "query <text...>",
2995
- 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.",
2996
- input: import_zod18.z.object({
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({
2997
3160
  bundlePath,
2998
- text: import_zod18.z.string().optional(),
2999
- type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
3000
- includeNonCurrent: import_zod18.z.boolean().optional()
3161
+ text: import_zod19.z.string().optional(),
3162
+ type: import_zod19.z.enum(KB_RECORD_TYPES).optional(),
3163
+ includeNonCurrent: import_zod19.z.boolean().optional()
3001
3164
  }),
3002
3165
  fromArgv: (argv, path) => ({
3003
3166
  bundlePath: path,
@@ -3019,40 +3182,40 @@ var queryCommand = define({
3019
3182
  });
3020
3183
 
3021
3184
  // src/commands/read-index.ts
3022
- var import_zod19 = require("zod");
3185
+ var import_zod20 = require("zod");
3023
3186
  var readIndexCommand = define({
3024
3187
  name: "index",
3025
3188
  tool: "kb_index",
3026
3189
  usage: "index",
3027
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.",
3028
- input: import_zod19.z.object({ bundlePath }),
3191
+ input: import_zod20.z.object({ bundlePath }),
3029
3192
  fromArgv: (_argv, path) => ({ bundlePath: path }),
3030
3193
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
3031
3194
  });
3032
3195
 
3033
3196
  // src/commands/schema.ts
3034
- var import_zod20 = require("zod");
3197
+ var import_zod21 = require("zod");
3035
3198
  var schemaCommand = define({
3036
3199
  name: "schema",
3037
3200
  tool: "kb_schema",
3038
3201
  usage: "schema",
3039
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.",
3040
- input: import_zod20.z.object({}),
3203
+ input: import_zod21.z.object({}),
3041
3204
  fromArgv: () => ({}),
3042
3205
  run: () => Promise.resolve(kbJsonSchemas())
3043
3206
  });
3044
3207
 
3045
3208
  // src/commands/status.ts
3046
- var import_zod21 = require("zod");
3209
+ var import_zod22 = require("zod");
3047
3210
  var statusCommand = define({
3048
3211
  name: "status",
3049
3212
  tool: "kb_status",
3050
3213
  usage: "status <concept-id> <status>",
3051
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.",
3052
- input: import_zod21.z.object({
3215
+ input: import_zod22.z.object({
3053
3216
  bundlePath,
3054
3217
  conceptId,
3055
- status: import_zod21.z.enum(KB_RECORD_STATUSES)
3218
+ status: import_zod22.z.enum(KB_RECORD_STATUSES)
3056
3219
  }),
3057
3220
  fromArgv: (argv, path) => ({
3058
3221
  bundlePath: path,
@@ -3067,13 +3230,13 @@ var statusCommand = define({
3067
3230
  });
3068
3231
 
3069
3232
  // src/commands/supersede.ts
3070
- var import_zod22 = require("zod");
3233
+ var import_zod23 = require("zod");
3071
3234
  var supersedeCommand = define({
3072
3235
  name: "supersede",
3073
3236
  tool: "kb_supersede",
3074
3237
  usage: "supersede <concept-id> <replacement-id>",
3075
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.",
3076
- input: import_zod22.z.object({ bundlePath, conceptId, replacementId: conceptId }),
3239
+ input: import_zod23.z.object({ bundlePath, conceptId, replacementId: conceptId }),
3077
3240
  fromArgv: (argv, path) => ({
3078
3241
  bundlePath: path,
3079
3242
  conceptId: argv[1],
@@ -3087,16 +3250,16 @@ var supersedeCommand = define({
3087
3250
  });
3088
3251
 
3089
3252
  // src/commands/sync-instructions.ts
3090
- var import_zod23 = require("zod");
3253
+ var import_zod24 = require("zod");
3091
3254
  var syncInstructionsCommand = define({
3092
3255
  name: "sync-instructions",
3093
3256
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
3094
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.",
3095
- input: import_zod23.z.object({
3096
- file: import_zod23.z.string().min(1).describe("The instruction file to edit in place."),
3097
- budgetTokens: import_zod23.z.number().int().positive().optional(),
3098
- fullUnderTokens: import_zod23.z.number().int().positive().optional(),
3099
- profile: import_zod23.z.string().optional()
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()
3100
3263
  }),
3101
3264
  fromArgv: (argv) => {
3102
3265
  const budget = argvFlag(argv, "--budget");
@@ -3122,17 +3285,17 @@ var syncInstructionsCommand = define({
3122
3285
  });
3123
3286
 
3124
3287
  // src/commands/trace.ts
3125
- var import_zod24 = require("zod");
3288
+ var import_zod25 = require("zod");
3126
3289
  var traceCommand = define({
3127
3290
  name: "trace",
3128
3291
  tool: "kb_trace",
3129
3292
  usage: "trace <concept-id> [edges...]",
3130
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.',
3131
- input: import_zod24.z.object({
3294
+ input: import_zod25.z.object({
3132
3295
  bundlePath,
3133
3296
  conceptId,
3134
- edges: import_zod24.z.array(import_zod24.z.enum(TRACE_EDGES)).optional(),
3135
- depth: import_zod24.z.number().int().positive().optional()
3297
+ edges: import_zod25.z.array(import_zod25.z.enum(TRACE_EDGES)).optional(),
3298
+ depth: import_zod25.z.number().int().positive().optional()
3136
3299
  }),
3137
3300
  fromArgv: (argv, path) => ({
3138
3301
  bundlePath: path,
@@ -3154,53 +3317,53 @@ var traceCommand = define({
3154
3317
  });
3155
3318
 
3156
3319
  // src/commands/types.ts
3157
- var import_zod25 = require("zod");
3320
+ var import_zod26 = require("zod");
3158
3321
  var typesCommand = define({
3159
3322
  name: "types",
3160
3323
  tool: "kb_types",
3161
3324
  usage: "types",
3162
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.",
3163
- input: import_zod25.z.object({}),
3326
+ input: import_zod26.z.object({}),
3164
3327
  fromArgv: () => ({}),
3165
3328
  run: () => Promise.resolve(RECORD_TYPES)
3166
3329
  });
3167
3330
 
3168
3331
  // src/commands/unpin.ts
3169
- var import_zod26 = require("zod");
3332
+ var import_zod27 = require("zod");
3170
3333
  var unpinCommand = define({
3171
3334
  name: "unpin",
3172
3335
  tool: "kb_unpin",
3173
3336
  usage: "unpin [bundle-path]",
3174
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.",
3175
- input: import_zod26.z.object({ bundlePath }),
3338
+ input: import_zod27.z.object({ bundlePath }),
3176
3339
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
3177
3340
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
3178
3341
  });
3179
3342
 
3180
3343
  // src/commands/validate.ts
3181
- var import_zod27 = require("zod");
3344
+ var import_zod28 = require("zod");
3182
3345
  var validateCommand = define({
3183
3346
  name: "validate",
3184
3347
  tool: "kb_validate",
3185
3348
  usage: "validate",
3186
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.",
3187
- input: import_zod27.z.object({ bundlePath }),
3350
+ input: import_zod28.z.object({ bundlePath }),
3188
3351
  fromArgv: (_argv, path) => ({ bundlePath: path }),
3189
3352
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
3190
3353
  failsWhen: (result) => Array.isArray(result) && result.length > 0
3191
3354
  });
3192
3355
 
3193
3356
  // src/commands/verify.ts
3194
- var import_zod28 = require("zod");
3357
+ var import_zod29 = require("zod");
3195
3358
  var verifyCommand = define({
3196
3359
  name: "verify",
3197
3360
  tool: "kb_verify",
3198
3361
  usage: "verify <concept-id> --note <text>",
3199
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.",
3200
- input: import_zod28.z.object({
3363
+ input: import_zod29.z.object({
3201
3364
  bundlePath,
3202
3365
  conceptId,
3203
- note: import_zod28.z.string().refine((s) => s.trim().length > 0, {
3366
+ note: import_zod29.z.string().refine((s) => s.trim().length > 0, {
3204
3367
  message: "note must say what the check found"
3205
3368
  })
3206
3369
  }),
@@ -3220,7 +3383,7 @@ var verifyCommand = define({
3220
3383
  });
3221
3384
 
3222
3385
  // src/commands/write.ts
3223
- var import_zod29 = require("zod");
3386
+ var import_zod30 = require("zod");
3224
3387
  var writeCommand = define({
3225
3388
  name: "write",
3226
3389
  tool: "kb_write",
@@ -3234,9 +3397,9 @@ var writeCommand = define({
3234
3397
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
3235
3398
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
3236
3399
  ].join("\n"),
3237
- input: import_zod29.z.object({
3400
+ input: import_zod30.z.object({
3238
3401
  bundlePath,
3239
- type: import_zod29.z.enum(KB_RECORD_TYPES),
3402
+ type: import_zod30.z.enum(KB_RECORD_TYPES),
3240
3403
  input: composeInputSchema
3241
3404
  }),
3242
3405
  fromArgv: async (argv, path, stdin) => ({
@@ -3260,7 +3423,7 @@ var writeCommand = define({
3260
3423
  });
3261
3424
 
3262
3425
  // src/commands/write-decision.ts
3263
- var import_zod30 = require("zod");
3426
+ var import_zod31 = require("zod");
3264
3427
  var writeDecisionCommand = define({
3265
3428
  name: "write-decision",
3266
3429
  tool: "kb_write_decision",
@@ -3273,7 +3436,7 @@ var writeDecisionCommand = define({
3273
3436
  "- `alternative` is what you turned down and why, not a list of everything considered.",
3274
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`."
3275
3438
  ].join("\n"),
3276
- input: import_zod30.z.object({ bundlePath, input: decisionInputSchema }),
3439
+ input: import_zod31.z.object({ bundlePath, input: decisionInputSchema }),
3277
3440
  fromArgv: async (_argv, path, stdin) => ({
3278
3441
  bundlePath: path,
3279
3442
  input: JSON.parse(await stdin())
@@ -3303,6 +3466,7 @@ var KB_COMMANDS = [
3303
3466
  answerCommand,
3304
3467
  verifyCommand,
3305
3468
  loadCommand,
3469
+ catalogCommand,
3306
3470
  packCommand,
3307
3471
  queryCommand,
3308
3472
  traceCommand,
@@ -3328,7 +3492,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
3328
3492
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
3329
3493
 
3330
3494
  // src/version.ts
3331
- var VERSION = true ? "0.1.9" : "0.0.0-dev";
3495
+ var VERSION = true ? "0.1.10" : "0.0.0-dev";
3332
3496
 
3333
3497
  // src/mcp.ts
3334
3498
  function createKbMcpServer() {
@@ -3497,6 +3661,7 @@ function usage() {
3497
3661
  KB_SLUG_PATTERN,
3498
3662
  KbBaseFrozenError,
3499
3663
  KbInvalidConceptIdError,
3664
+ KbMissingFlagValueError,
3500
3665
  KbPackBudgetExceededError,
3501
3666
  KbPinsMalformedError,
3502
3667
  KbRecordAlreadyExistsError,
@@ -3515,6 +3680,7 @@ function usage() {
3515
3680
  adjudicate,
3516
3681
  assertBaseNotFrozen,
3517
3682
  buildContext,
3683
+ catalog,
3518
3684
  composeDecisionRecord,
3519
3685
  composeInputSchema,
3520
3686
  composeNoDecisionRecord,
@@ -3546,6 +3712,7 @@ function usage() {
3546
3712
  pinBase,
3547
3713
  readMergedPins,
3548
3714
  readPinsLayer,
3715
+ renderCatalogLine,
3549
3716
  renderIndex,
3550
3717
  renderIndexLine,
3551
3718
  renderLogEntry,