@saasontools/strauss-kb 0.1.15 → 0.1.16

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/mcp-main.cjs CHANGED
@@ -1440,6 +1440,36 @@ var KbInvalidConceptIdError = class extends BaseError {
1440
1440
  });
1441
1441
  }
1442
1442
  };
1443
+ var KbStampBaselineError = class extends BaseError {
1444
+ constructor(since) {
1445
+ super({
1446
+ message: `kb: --since ${since} is neither a 64-character digest nor a readable stamp file`,
1447
+ errorType: "KbStampBaselineUnreadable" /* KbStampBaselineUnreadable */,
1448
+ code: 400,
1449
+ fault: "User" /* User */,
1450
+ retriable: false,
1451
+ reportToUser: true,
1452
+ details: { since }
1453
+ });
1454
+ this.since = since;
1455
+ }
1456
+ since;
1457
+ };
1458
+ var KbStampDigestBaselineError = class extends BaseError {
1459
+ constructor(since) {
1460
+ super({
1461
+ message: `kb: --since ${since} is a digest, which needs --bundle (one base) \u2014 a file baseline works for many`,
1462
+ errorType: "KbStampDigestBaselineAmbiguous" /* KbStampDigestBaselineAmbiguous */,
1463
+ code: 400,
1464
+ fault: "User" /* User */,
1465
+ retriable: false,
1466
+ reportToUser: true,
1467
+ details: { since }
1468
+ });
1469
+ this.since = since;
1470
+ }
1471
+ since;
1472
+ };
1443
1473
 
1444
1474
  // src/kb-pins/budgets.ts
1445
1475
  function asBudgets(value) {
@@ -3578,17 +3608,115 @@ var schemaCommand = define({
3578
3608
  run: () => Promise.resolve(kbJsonSchemas())
3579
3609
  });
3580
3610
 
3581
- // src/commands/status.ts
3611
+ // src/commands/stamp.ts
3612
+ var import_promises5 = require("fs/promises");
3582
3613
  var import_zod25 = require("zod");
3614
+ var DIGEST = /^[0-9a-f]{64}$/;
3615
+ var stampCommand = define({
3616
+ name: "stamp",
3617
+ tool: "kb_stamp",
3618
+ usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
3619
+ description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids when the baseline is a prior stamp; silent when nothing changed. Reads, never writes.",
3620
+ input: import_zod25.z.object({
3621
+ bundlePath: import_zod25.z.string().min(1).optional().describe(
3622
+ "Absolute path to one knowledge base. Omit to stamp every pinned base."
3623
+ ),
3624
+ since: import_zod25.z.string().min(1).optional().describe(
3625
+ "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
3626
+ )
3627
+ }),
3628
+ fromArgv: (argv, path, _stdin, bundleExplicit) => {
3629
+ const since = argvFlag(argv, "--since");
3630
+ return {
3631
+ ...bundleExplicit ? { bundlePath: path } : {},
3632
+ ...since !== void 0 ? { since } : {}
3633
+ };
3634
+ },
3635
+ run: async ({ store }, { bundlePath: bundlePath2, since }) => {
3636
+ const targets = bundlePath2 ? [bundlePath2] : (await readMergedPins(process.cwd())).pins.map(
3637
+ (pin) => pin.absolutePath
3638
+ );
3639
+ if (since !== void 0 && DIGEST.test(since) && targets.length > 1) {
3640
+ throw new KbStampDigestBaselineError(since);
3641
+ }
3642
+ const stamps = await Promise.all(
3643
+ targets.map((target) => store.stamp(target))
3644
+ );
3645
+ if (since === void 0) {
3646
+ return stamps.map((stamp) => ({ ...stamp, changed: null }));
3647
+ }
3648
+ const baseline = await readBaseline(since);
3649
+ const reports = [];
3650
+ for (const stamp of stamps) {
3651
+ const before = baseline.byPath.get(stamp.path);
3652
+ if (baseline.digest !== null) {
3653
+ if (baseline.digest === stamp.digest) continue;
3654
+ reports.push({ ...stamp, changed: null });
3655
+ continue;
3656
+ }
3657
+ if (before && before.digest === stamp.digest) continue;
3658
+ reports.push({ ...stamp, changed: changedIds(before?.records, stamp) });
3659
+ }
3660
+ return reports;
3661
+ },
3662
+ render: (result) => result.map((report) => {
3663
+ const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
3664
+ const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
3665
+ return report.changed?.length ? `${head}
3666
+ changed: ${report.changed.join(", ")}` : head;
3667
+ }).join("\n")
3668
+ });
3669
+ function changedIds(before, stamp) {
3670
+ const now = new Map(
3671
+ stamp.records.map((record) => [record.conceptId, record.digest])
3672
+ );
3673
+ const ids = /* @__PURE__ */ new Set();
3674
+ for (const [conceptId2, digest] of now) {
3675
+ if (before?.get(conceptId2) !== digest) ids.add(conceptId2);
3676
+ }
3677
+ for (const conceptId2 of before?.keys() ?? []) {
3678
+ if (!now.has(conceptId2)) ids.add(conceptId2);
3679
+ }
3680
+ return [...ids].sort();
3681
+ }
3682
+ async function readBaseline(since) {
3683
+ if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
3684
+ let parsed;
3685
+ try {
3686
+ parsed = JSON.parse(await (0, import_promises5.readFile)(since, "utf8"));
3687
+ } catch {
3688
+ throw new KbStampBaselineError(since);
3689
+ }
3690
+ const entries = Array.isArray(parsed) ? parsed : parsed?.stamps ?? [];
3691
+ const byPath = /* @__PURE__ */ new Map();
3692
+ for (const entry of entries) {
3693
+ if (typeof entry?.path !== "string" || typeof entry?.digest !== "string") {
3694
+ continue;
3695
+ }
3696
+ byPath.set(entry.path, {
3697
+ digest: entry.digest,
3698
+ records: new Map(
3699
+ (entry.records ?? []).map((record) => [
3700
+ record.conceptId,
3701
+ record.digest
3702
+ ])
3703
+ )
3704
+ });
3705
+ }
3706
+ return { digest: null, byPath };
3707
+ }
3708
+
3709
+ // src/commands/status.ts
3710
+ var import_zod26 = require("zod");
3583
3711
  var statusCommand = define({
3584
3712
  name: "status",
3585
3713
  tool: "kb_status",
3586
3714
  usage: "status <concept-id> <status>",
3587
3715
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
3588
- input: import_zod25.z.object({
3716
+ input: import_zod26.z.object({
3589
3717
  bundlePath,
3590
3718
  conceptId,
3591
- status: import_zod25.z.enum(KB_RECORD_STATUSES)
3719
+ status: import_zod26.z.enum(KB_RECORD_STATUSES)
3592
3720
  }),
3593
3721
  fromArgv: (argv, path) => ({
3594
3722
  bundlePath: path,
@@ -3603,13 +3731,13 @@ var statusCommand = define({
3603
3731
  });
3604
3732
 
3605
3733
  // src/commands/supersede.ts
3606
- var import_zod26 = require("zod");
3734
+ var import_zod27 = require("zod");
3607
3735
  var supersedeCommand = define({
3608
3736
  name: "supersede",
3609
3737
  tool: "kb_supersede",
3610
3738
  usage: "supersede <concept-id> <replacement-id>",
3611
3739
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
3612
- input: import_zod26.z.object({ bundlePath, conceptId, replacementId: conceptId }),
3740
+ input: import_zod27.z.object({ bundlePath, conceptId, replacementId: conceptId }),
3613
3741
  fromArgv: (argv, path) => ({
3614
3742
  bundlePath: path,
3615
3743
  conceptId: argv[1],
@@ -3623,16 +3751,16 @@ var supersedeCommand = define({
3623
3751
  });
3624
3752
 
3625
3753
  // src/commands/sync-instructions.ts
3626
- var import_zod27 = require("zod");
3754
+ var import_zod28 = require("zod");
3627
3755
  var syncInstructionsCommand = define({
3628
3756
  name: "sync-instructions",
3629
3757
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
3630
3758
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
3631
- input: import_zod27.z.object({
3632
- file: import_zod27.z.string().min(1).describe("The instruction file to edit in place."),
3633
- budgetTokens: import_zod27.z.number().int().positive().optional(),
3634
- fullUnderTokens: import_zod27.z.number().int().positive().optional(),
3635
- profile: import_zod27.z.string().optional()
3759
+ input: import_zod28.z.object({
3760
+ file: import_zod28.z.string().min(1).describe("The instruction file to edit in place."),
3761
+ budgetTokens: import_zod28.z.number().int().positive().optional(),
3762
+ fullUnderTokens: import_zod28.z.number().int().positive().optional(),
3763
+ profile: import_zod28.z.string().optional()
3636
3764
  }),
3637
3765
  fromArgv: (argv) => {
3638
3766
  const budget = argvFlag(argv, "--budget");
@@ -3658,7 +3786,7 @@ var syncInstructionsCommand = define({
3658
3786
  });
3659
3787
 
3660
3788
  // src/commands/trace.ts
3661
- var import_zod28 = require("zod");
3789
+ var import_zod29 = require("zod");
3662
3790
 
3663
3791
  // src/trace.ts
3664
3792
  var TRACE_EDGES = [
@@ -3714,11 +3842,11 @@ var traceCommand = define({
3714
3842
  tool: "kb_trace",
3715
3843
  usage: "trace <concept-id> [edges...]",
3716
3844
  description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
3717
- input: import_zod28.z.object({
3845
+ input: import_zod29.z.object({
3718
3846
  bundlePath,
3719
3847
  conceptId,
3720
- edges: import_zod28.z.array(import_zod28.z.enum(TRACE_EDGES)).optional(),
3721
- depth: import_zod28.z.number().int().positive().optional()
3848
+ edges: import_zod29.z.array(import_zod29.z.enum(TRACE_EDGES)).optional(),
3849
+ depth: import_zod29.z.number().int().positive().optional()
3722
3850
  }),
3723
3851
  fromArgv: (argv, path) => ({
3724
3852
  bundlePath: path,
@@ -3740,37 +3868,37 @@ var traceCommand = define({
3740
3868
  });
3741
3869
 
3742
3870
  // src/commands/types.ts
3743
- var import_zod29 = require("zod");
3871
+ var import_zod30 = require("zod");
3744
3872
  var typesCommand = define({
3745
3873
  name: "types",
3746
3874
  tool: "kb_types",
3747
3875
  usage: "types",
3748
3876
  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.",
3749
- input: import_zod29.z.object({}),
3877
+ input: import_zod30.z.object({}),
3750
3878
  fromArgv: () => ({}),
3751
3879
  run: () => Promise.resolve(RECORD_TYPES)
3752
3880
  });
3753
3881
 
3754
3882
  // src/commands/unpin.ts
3755
- var import_zod30 = require("zod");
3883
+ var import_zod31 = require("zod");
3756
3884
  var unpinCommand = define({
3757
3885
  name: "unpin",
3758
3886
  tool: "kb_unpin",
3759
3887
  usage: "unpin [bundle-path]",
3760
3888
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
3761
- input: import_zod30.z.object({ bundlePath }),
3889
+ input: import_zod31.z.object({ bundlePath }),
3762
3890
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
3763
3891
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
3764
3892
  });
3765
3893
 
3766
3894
  // src/commands/validate.ts
3767
- var import_zod31 = require("zod");
3895
+ var import_zod32 = require("zod");
3768
3896
  var validateCommand = define({
3769
3897
  name: "validate",
3770
3898
  tool: "kb_validate",
3771
3899
  usage: "validate",
3772
3900
  description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
3773
- input: import_zod31.z.object({ bundlePath }),
3901
+ input: import_zod32.z.object({ bundlePath }),
3774
3902
  fromArgv: (_argv, path) => ({ bundlePath: path }),
3775
3903
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
3776
3904
  // Warnings never fail the exit code; every other severity does.
@@ -3780,16 +3908,16 @@ var validateCommand = define({
3780
3908
  });
3781
3909
 
3782
3910
  // src/commands/verify.ts
3783
- var import_zod32 = require("zod");
3911
+ var import_zod33 = require("zod");
3784
3912
  var verifyCommand = define({
3785
3913
  name: "verify",
3786
3914
  tool: "kb_verify",
3787
3915
  usage: "verify <concept-id> --note <text>",
3788
3916
  description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
3789
- input: import_zod32.z.object({
3917
+ input: import_zod33.z.object({
3790
3918
  bundlePath,
3791
3919
  conceptId,
3792
- note: import_zod32.z.string().refine((s) => s.trim().length > 0, {
3920
+ note: import_zod33.z.string().refine((s) => s.trim().length > 0, {
3793
3921
  message: "note must say what the check found"
3794
3922
  })
3795
3923
  }),
@@ -3809,15 +3937,15 @@ var verifyCommand = define({
3809
3937
  });
3810
3938
 
3811
3939
  // src/commands/write.ts
3812
- var import_zod33 = require("zod");
3940
+ var import_zod34 = require("zod");
3813
3941
  var writeCommand = define({
3814
3942
  name: "write",
3815
3943
  tool: "kb_write",
3816
3944
  usage: "write <type> < record.json",
3817
3945
  description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
3818
- input: import_zod33.z.object({
3946
+ input: import_zod34.z.object({
3819
3947
  bundlePath,
3820
- type: import_zod33.z.enum(KB_RECORD_TYPES),
3948
+ type: import_zod34.z.enum(KB_RECORD_TYPES),
3821
3949
  input: composeInputSchema
3822
3950
  }),
3823
3951
  fromArgv: async (argv, path, stdin) => ({
@@ -3841,13 +3969,13 @@ var writeCommand = define({
3841
3969
  });
3842
3970
 
3843
3971
  // src/commands/write-decision.ts
3844
- var import_zod34 = require("zod");
3972
+ var import_zod35 = require("zod");
3845
3973
  var writeDecisionCommand = define({
3846
3974
  name: "write-decision",
3847
3975
  tool: "kb_write_decision",
3848
3976
  usage: "write-decision < decision.json",
3849
3977
  description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
3850
- input: import_zod34.z.object({ bundlePath, input: decisionInputSchema }),
3978
+ input: import_zod35.z.object({ bundlePath, input: decisionInputSchema }),
3851
3979
  fromArgv: async (_argv, path, stdin) => ({
3852
3980
  bundlePath: path,
3853
3981
  input: JSON.parse(await stdin())
@@ -3887,6 +4015,7 @@ var KB_COMMANDS = [
3887
4015
  listCommand,
3888
4016
  readIndexCommand,
3889
4017
  logCommand,
4018
+ stampCommand,
3890
4019
  validateCommand,
3891
4020
  doctorCommand,
3892
4021
  schemaCommand,
@@ -3902,8 +4031,7 @@ var KB_COMMANDS_BY_NAME = new Map(
3902
4031
  );
3903
4032
 
3904
4033
  // src/kb-store.ts
3905
- var import_node_crypto2 = require("crypto");
3906
- var import_promises6 = require("fs/promises");
4034
+ var import_promises7 = require("fs/promises");
3907
4035
  var import_node_path8 = require("path");
3908
4036
 
3909
4037
  // src/markdown.ts
@@ -3931,8 +4059,40 @@ function parseMarkdownWithFrontmatter(text, schema) {
3931
4059
  };
3932
4060
  }
3933
4061
 
4062
+ // src/kb-stamp.ts
4063
+ var import_node_crypto2 = require("crypto");
4064
+ function sha256(contents) {
4065
+ return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
4066
+ }
4067
+ function bundleStamp(records, superseded) {
4068
+ const entries = [
4069
+ ...records.map((hit) => ({
4070
+ conceptId: hit.record.conceptId,
4071
+ digest: `current:${sha256(
4072
+ stringifyMarkdownWithFrontmatter(
4073
+ hit.record.body,
4074
+ hit.record.frontmatter
4075
+ )
4076
+ )}`
4077
+ })),
4078
+ ...superseded.map((entry) => ({
4079
+ conceptId: entry.conceptId,
4080
+ digest: `superseded:${sha256(JSON.stringify(entry))}`
4081
+ }))
4082
+ ].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
4083
+ return {
4084
+ digest: sha256(
4085
+ entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
4086
+ ),
4087
+ records: entries
4088
+ };
4089
+ }
4090
+ function bundleDigest(records, superseded) {
4091
+ return bundleStamp(records, superseded).digest;
4092
+ }
4093
+
3934
4094
  // src/search-index.ts
3935
- var import_promises5 = require("fs/promises");
4095
+ var import_promises6 = require("fs/promises");
3936
4096
  var import_node_path7 = require("path");
3937
4097
  var SEARCH_INDEX_FILE = ".index.sqlite";
3938
4098
  var COLLECTION = "kb";
@@ -3977,7 +4137,7 @@ async function searchBase(bundlePath2, query, options = {}) {
3977
4137
  }
3978
4138
  }
3979
4139
  async function isStale(bundlePath2) {
3980
- const indexAt = await (0, import_promises5.stat)((0, import_node_path7.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
4140
+ const indexAt = await (0, import_promises6.stat)((0, import_node_path7.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
3981
4141
  if (!indexAt) return true;
3982
4142
  const { readdir: readdir2 } = await import("fs/promises");
3983
4143
  const names = (await readdir2(bundlePath2).catch(() => [])).filter(
@@ -3986,7 +4146,7 @@ async function isStale(bundlePath2) {
3986
4146
  let stale = false;
3987
4147
  await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
3988
4148
  if (stale) return;
3989
- const at = await (0, import_promises5.stat)((0, import_node_path7.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
4149
+ const at = await (0, import_promises6.stat)((0, import_node_path7.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
3990
4150
  if (at > indexAt) stale = true;
3991
4151
  });
3992
4152
  return stale;
@@ -4295,7 +4455,7 @@ var KbStore = class {
4295
4455
  const conceptId2 = `${input.type}.${input.slug}`;
4296
4456
  const root = this.root(bundlePath2);
4297
4457
  const target = this.recordPath(bundlePath2, conceptId2);
4298
- await (0, import_promises6.mkdir)(root, { recursive: true });
4458
+ await (0, import_promises7.mkdir)(root, { recursive: true });
4299
4459
  await this.publish(
4300
4460
  target,
4301
4461
  stringifyMarkdownWithFrontmatter(input.body, frontmatter),
@@ -4334,7 +4494,7 @@ var KbStore = class {
4334
4494
  const target = this.recordPath(bundlePath2, conceptId2);
4335
4495
  let raw;
4336
4496
  try {
4337
- raw = await (0, import_promises6.readFile)(target, "utf8");
4497
+ raw = await (0, import_promises7.readFile)(target, "utf8");
4338
4498
  } catch {
4339
4499
  return null;
4340
4500
  }
@@ -4351,7 +4511,7 @@ var KbStore = class {
4351
4511
  const root = this.root(bundlePath2);
4352
4512
  let names;
4353
4513
  try {
4354
- names = await (0, import_promises6.readdir)(root);
4514
+ names = await (0, import_promises7.readdir)(root);
4355
4515
  } catch {
4356
4516
  return [];
4357
4517
  }
@@ -4359,7 +4519,7 @@ var KbStore = class {
4359
4519
  const records = await mapLimit(
4360
4520
  wanted,
4361
4521
  DEFAULT_IO_CONCURRENCY,
4362
- async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path8.join)(root, name), "utf8"))
4522
+ async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises7.readFile)((0, import_node_path8.join)(root, name), "utf8"))
4363
4523
  );
4364
4524
  return records.filter((record) => record !== null);
4365
4525
  }
@@ -4634,6 +4794,28 @@ ${answer}
4634
4794
  digest: bundleDigestValue
4635
4795
  };
4636
4796
  }
4797
+ /**
4798
+ * `load`'s digest without `load`'s bodies — the same records, adjudicated
4799
+ * the same way, handed back as a stamp. Skips the anchor drift pass, which
4800
+ * reads source files and only ever adds warnings: no warning reaches the
4801
+ * digest, so the value is identical to the one `load` returns.
4802
+ */
4803
+ async stamp(bundlePath2) {
4804
+ const bundle = await this.list(bundlePath2);
4805
+ const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
4806
+ const current = adjudicated.filter((hit) => hit.standing !== "superseded");
4807
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
4808
+ const stamped = bundleStamp(current, superseded);
4809
+ const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
4810
+ return {
4811
+ path: bundlePath2,
4812
+ digest: stamped.digest,
4813
+ recordCount: bundle.length,
4814
+ superseded: superseded.length,
4815
+ newestAt: dates.at(-1) ?? null,
4816
+ records: stamped.records
4817
+ };
4818
+ }
4637
4819
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
4638
4820
  async trace(bundlePath2, seedId, options = {}) {
4639
4821
  return trace(seedId, await this.list(bundlePath2), options);
@@ -4664,7 +4846,7 @@ ${answer}
4664
4846
  async readIndex(bundlePath2) {
4665
4847
  const root = this.root(bundlePath2);
4666
4848
  const expected = renderIndex(await this.list(bundlePath2));
4667
- const stored = await (0, import_promises6.readFile)((0, import_node_path8.join)(root, INDEX_FILE), "utf8").catch(
4849
+ const stored = await (0, import_promises7.readFile)((0, import_node_path8.join)(root, INDEX_FILE), "utf8").catch(
4668
4850
  () => null
4669
4851
  );
4670
4852
  if (indexIsStale(stored, expected)) {
@@ -4685,7 +4867,7 @@ ${answer}
4685
4867
  * knows which agent touched what. So a bad line is surfaced and left alone.
4686
4868
  */
4687
4869
  async readLog(bundlePath2) {
4688
- const raw = await (0, import_promises6.readFile)(
4870
+ const raw = await (0, import_promises7.readFile)(
4689
4871
  (0, import_node_path8.join)(this.root(bundlePath2), LOG_FILE),
4690
4872
  "utf8"
4691
4873
  ).catch(() => "");
@@ -4737,15 +4919,15 @@ ${answer}
4737
4919
  }
4738
4920
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
4739
4921
  const target = this.recordPath(bundlePath2, conceptId2);
4740
- const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
4922
+ const before = await (0, import_promises7.readFile)(target, "utf8").catch(() => null);
4741
4923
  if (before === null) throw new KbRecordNotFoundError(conceptId2);
4742
4924
  const parsed = this.parse(conceptId2, before);
4743
4925
  if (!parsed) throw new KbRecordNotFoundError(conceptId2);
4744
4926
  const frontmatter = change(parsed.frontmatter);
4745
4927
  const body = changeBody(parsed.body);
4746
4928
  const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
4747
- const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
4748
- if (witness === null || digest(witness) !== digest(before)) {
4929
+ const witness = await (0, import_promises7.readFile)(target, "utf8").catch(() => null);
4930
+ if (witness === null || sha256(witness) !== sha256(before)) {
4749
4931
  throw new KbWriteConflictError(conceptId2);
4750
4932
  }
4751
4933
  await this.publish(target, contents, true, conceptId2);
@@ -4770,20 +4952,20 @@ ${answer}
4770
4952
  */
4771
4953
  async publish(target, contents, overwrite, conceptId2) {
4772
4954
  const staging = `${target}.${process.pid}.tmp`;
4773
- await (0, import_promises6.writeFile)(staging, contents, "utf8");
4955
+ await (0, import_promises7.writeFile)(staging, contents, "utf8");
4774
4956
  try {
4775
4957
  if (overwrite) {
4776
- await (0, import_promises6.rename)(staging, target);
4958
+ await (0, import_promises7.rename)(staging, target);
4777
4959
  return;
4778
4960
  }
4779
- await (0, import_promises6.link)(staging, target);
4961
+ await (0, import_promises7.link)(staging, target);
4780
4962
  } catch (error) {
4781
4963
  if (error.code === "EEXIST") {
4782
4964
  throw new KbRecordAlreadyExistsError(conceptId2);
4783
4965
  }
4784
4966
  throw error;
4785
4967
  } finally {
4786
- await (0, import_promises6.unlink)(staging).catch(() => void 0);
4968
+ await (0, import_promises7.unlink)(staging).catch(() => void 0);
4787
4969
  }
4788
4970
  }
4789
4971
  /**
@@ -4831,14 +5013,14 @@ ${answer}
4831
5013
  try {
4832
5014
  let existing;
4833
5015
  try {
4834
- existing = await (0, import_promises6.readFile)(target, "utf8");
5016
+ existing = await (0, import_promises7.readFile)(target, "utf8");
4835
5017
  } catch (error) {
4836
5018
  if (error.code !== "ENOENT") throw error;
4837
5019
  existing = null;
4838
5020
  }
4839
5021
  if (existing === null) {
4840
5022
  try {
4841
- await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
5023
+ await (0, import_promises7.writeFile)(target, appendUnionMergeLine(""), {
4842
5024
  encoding: "utf8",
4843
5025
  flag: "wx"
4844
5026
  });
@@ -4859,7 +5041,7 @@ ${answer}
4859
5041
  return;
4860
5042
  }
4861
5043
  if (!hasMergeDeclaration(existing)) {
4862
- await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
5044
+ await (0, import_promises7.appendFile)(target, appendUnionMergeLine(existing), "utf8");
4863
5045
  this.logger.info?.({
4864
5046
  operation: "kb.gitattributes.ensure",
4865
5047
  bundlePath: root,
@@ -4878,7 +5060,7 @@ ${answer}
4878
5060
  async record(root, entry) {
4879
5061
  await this.ensureGitattributes(root);
4880
5062
  const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
4881
- await (0, import_promises6.appendFile)((0, import_node_path8.join)(root, LOG_FILE), line, "utf8").catch((error) => {
5063
+ await (0, import_promises7.appendFile)((0, import_node_path8.join)(root, LOG_FILE), line, "utf8").catch((error) => {
4882
5064
  this.logger.warn?.({
4883
5065
  operation: "kb.log.append",
4884
5066
  outcome: "failed",
@@ -4953,28 +5135,9 @@ function normalizeActor(id) {
4953
5135
  if (colon === -1) return id.toLowerCase();
4954
5136
  return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
4955
5137
  }
4956
- function digest(contents) {
4957
- return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
4958
- }
4959
- function bundleDigest(records, superseded) {
4960
- const entries = [
4961
- ...records.map(
4962
- (hit) => `${hit.record.conceptId}:current:${digest(
4963
- stringifyMarkdownWithFrontmatter(
4964
- hit.record.body,
4965
- hit.record.frontmatter
4966
- )
4967
- )}`
4968
- ),
4969
- ...superseded.map(
4970
- (entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
4971
- )
4972
- ].sort();
4973
- return digest(entries.join("\n"));
4974
- }
4975
5138
 
4976
5139
  // src/version.ts
4977
- var VERSION = true ? "0.1.15" : "0.0.0-dev";
5140
+ var VERSION = true ? "0.1.16" : "0.0.0-dev";
4978
5141
 
4979
5142
  // src/mcp.ts
4980
5143
  function createKbMcpServer() {