@bartolli/kmd 0.3.0 → 0.5.0

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/kmd.mjs CHANGED
@@ -15,6 +15,10 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // ../db/src/database.ts
18
+ import { createHash } from "node:crypto";
19
+ import { realpathSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { basename, join, resolve } from "node:path";
18
22
  import { DatabaseSync } from "node:sqlite";
19
23
  function openDatabase(dbPath) {
20
24
  const db = new DatabaseSync(dbPath);
@@ -23,6 +27,34 @@ function openDatabase(dbPath) {
23
27
  db.exec(SCHEMA);
24
28
  return db;
25
29
  }
30
+ function indexRootDir() {
31
+ const home = process.env.KMD_HOME ?? join(homedir(), ".kmd");
32
+ return join(home, "db");
33
+ }
34
+ function canonicalVaultRoot(vaultRoot2) {
35
+ try {
36
+ return realpathSync(vaultRoot2);
37
+ } catch {
38
+ return resolve(vaultRoot2);
39
+ }
40
+ }
41
+ function vaultKey(vaultRoot2) {
42
+ const canonical = canonicalVaultRoot(vaultRoot2);
43
+ const hash = createHash("sha256").update(canonical).digest("hex").slice(0, 8);
44
+ return `${basename(canonical)}-${hash}`;
45
+ }
46
+ function resolveIndexPath(vaultRoot2) {
47
+ return join(indexRootDir(), vaultKey(vaultRoot2), "index.db");
48
+ }
49
+ function getMeta(db, key) {
50
+ const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
51
+ return row?.value ?? null;
52
+ }
53
+ function setMeta(db, key, value) {
54
+ db.prepare(
55
+ "INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
56
+ ).run(key, value);
57
+ }
26
58
  var SCHEMA;
27
59
  var init_database = __esm({
28
60
  "../db/src/database.ts"() {
@@ -73,17 +105,25 @@ CREATE TABLE IF NOT EXISTS events (
73
105
  );
74
106
 
75
107
  CREATE INDEX IF NOT EXISTS events_scope_ts ON events(scope, ts DESC);
108
+
109
+ CREATE TABLE IF NOT EXISTS meta (
110
+ key TEXT PRIMARY KEY,
111
+ value TEXT NOT NULL
112
+ );
76
113
  `;
77
114
  }
78
115
  });
79
116
 
80
117
  // ../cli/src/config.ts
81
118
  import { readFile } from "node:fs/promises";
82
- import { join } from "node:path";
119
+ import { join as join2 } from "node:path";
83
120
  import { parse } from "yaml";
84
121
  import { z } from "zod";
122
+ function kindName(entry) {
123
+ return typeof entry === "string" ? entry : entry.name;
124
+ }
85
125
  async function loadVaultConfig(vaultRoot2) {
86
- const path = join(vaultRoot2, "vault.yaml");
126
+ const path = join2(vaultRoot2, "vault.yaml");
87
127
  let raw;
88
128
  try {
89
129
  raw = await readFile(path, "utf8");
@@ -98,18 +138,26 @@ ${issues}`);
98
138
  }
99
139
  return parsed.data;
100
140
  }
101
- var ScopeSchema, VaultConfigSchema;
141
+ var ScopeSchema, KindEntrySchema, VaultConfigSchema, BUILT_IN_KINDS;
102
142
  var init_config = __esm({
103
143
  "../cli/src/config.ts"() {
104
144
  "use strict";
105
145
  ScopeSchema = z.object({
106
146
  repo: z.string().optional(),
107
- methodology: z.enum(["sdd", "tdd", "hybrid"]).optional(),
147
+ methodology: z.string().optional(),
108
148
  status: z.string()
109
149
  });
150
+ KindEntrySchema = z.union([
151
+ z.string(),
152
+ z.object({
153
+ name: z.string(),
154
+ signal: z.string(),
155
+ where: z.string()
156
+ })
157
+ ]);
110
158
  VaultConfigSchema = z.object({
111
159
  scopes: z.record(z.string(), ScopeSchema),
112
- kinds: z.array(z.string()),
160
+ kinds: z.array(KindEntrySchema),
113
161
  statuses: z.array(z.string()),
114
162
  methodologies: z.array(z.string()),
115
163
  tags: z.object({
@@ -117,8 +165,34 @@ var init_config = __esm({
117
165
  aliases: z.record(z.string(), z.string())
118
166
  }),
119
167
  authoring_rules: z.string().optional(),
120
- sync_protocol: z.string().optional()
168
+ authoring_rules_extra: z.string().optional(),
169
+ sync_protocol: z.string().optional(),
170
+ sync_protocol_extra: z.string().optional()
171
+ }).superRefine((config, ctx) => {
172
+ for (const [name, scope] of Object.entries(config.scopes)) {
173
+ if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
174
+ ctx.addIssue({
175
+ code: "custom",
176
+ path: ["scopes", name, "methodology"],
177
+ message: `"${scope.methodology}" is not in the methodologies list`
178
+ });
179
+ }
180
+ }
121
181
  });
182
+ BUILT_IN_KINDS = /* @__PURE__ */ new Set([
183
+ "project",
184
+ "spec",
185
+ "adr",
186
+ "plan",
187
+ "story",
188
+ "ops",
189
+ "topic",
190
+ "article",
191
+ "src",
192
+ "note",
193
+ "artifact",
194
+ "prompt"
195
+ ]);
122
196
  }
123
197
  });
124
198
 
@@ -147,11 +221,10 @@ var init_frontmatter = __esm({
147
221
  });
148
222
 
149
223
  // ../cli/src/sync.ts
150
- import { createHash } from "node:crypto";
224
+ import { createHash as createHash2 } from "node:crypto";
151
225
  import { mkdirSync } from "node:fs";
152
226
  import { readdir, readFile as readFile2 } from "node:fs/promises";
153
- import { homedir } from "node:os";
154
- import { join as join2, relative, sep } from "node:path";
227
+ import { dirname, join as join3, relative, sep } from "node:path";
155
228
  import { z as z2 } from "zod";
156
229
  function loadEnv() {
157
230
  const parsed = EnvSchema.safeParse({
@@ -172,20 +245,20 @@ async function walkMarkdown(root, domain) {
172
245
  for (const entry of entries) {
173
246
  if (entry.name.startsWith(".")) continue;
174
247
  if (entry.isDirectory()) {
175
- await recurse(join2(dir, entry.name));
248
+ await recurse(join3(dir, entry.name));
176
249
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
177
- out.push(join2(dir, entry.name));
250
+ out.push(join3(dir, entry.name));
178
251
  }
179
252
  }
180
253
  }
181
- await recurse(join2(root, domain));
254
+ await recurse(join3(root, domain));
182
255
  return out;
183
256
  }
184
257
  function toRelativePath(root, absolute) {
185
258
  return relative(root, absolute).split(sep).join("/");
186
259
  }
187
260
  function sha256(content) {
188
- return createHash("sha256").update(content).digest("hex");
261
+ return createHash2("sha256").update(content).digest("hex");
189
262
  }
190
263
  function extractWikilinks(body) {
191
264
  const links = [];
@@ -302,12 +375,11 @@ function syncPage(db, fields) {
302
375
  }
303
376
  async function runSync() {
304
377
  const env = loadEnv();
305
- const dbDir = join2(homedir(), ".kmd", "db");
306
- const dbPath = join2(dbDir, "index.db");
378
+ const dbPath = resolveIndexPath(env.WIKI_VAULT);
307
379
  console.log(`sync: ${env.WIKI_VAULT} \u2192 ${dbPath}`);
308
380
  const vaultConfig = await loadVaultConfig(env.WIKI_VAULT);
309
381
  const scopes = new Set(Object.keys(vaultConfig.scopes));
310
- mkdirSync(dbDir, { recursive: true });
382
+ mkdirSync(dirname(dbPath), { recursive: true });
311
383
  const db = openDatabase(dbPath);
312
384
  try {
313
385
  const files = [];
@@ -347,6 +419,8 @@ async function runSync() {
347
419
  console.warn("no indexable pages found; skipping orphan deletion (safety)");
348
420
  }
349
421
  db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
422
+ setMeta(db, "vault_root", canonicalVaultRoot(env.WIKI_VAULT));
423
+ setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
350
424
  console.log(
351
425
  `done: ${changed} changed, ${unchanged} unchanged, ${skipped} skipped, ${pagesDeleted} pages deleted, ${linksDeleted} link orphans cleared`
352
426
  );
@@ -380,9 +454,13 @@ var init_sync = __esm({
380
454
  });
381
455
 
382
456
  // ../cli/src/validate.ts
383
- import { readFile as readFile3 } from "node:fs/promises";
457
+ import { readFile as readFile3, stat } from "node:fs/promises";
458
+ import { join as join4 } from "node:path";
459
+ function hasIndexableTitle(data) {
460
+ return typeof data.title === "string" && data.title.trim() !== "";
461
+ }
384
462
  function isIndexed(relPath, data) {
385
- if (typeof data.title !== "string" || data.title.trim() === "") return false;
463
+ if (!hasIndexableTitle(data)) return false;
386
464
  if (typeof data.kind === "string" && data.kind !== "") return true;
387
465
  return relPath.startsWith("notes/");
388
466
  }
@@ -404,6 +482,45 @@ function checkRequiredFields(relPath, data) {
404
482
  });
405
483
  }
406
484
  }
485
+ if (Object.hasOwn(data, "title") && !hasIndexableTitle(data)) {
486
+ findings.push({
487
+ path: relPath,
488
+ rule: "required-fields",
489
+ severity: "error",
490
+ message: `"title" must be a non-empty string for kind "${kind}" \u2014 sync skips the page otherwise`
491
+ });
492
+ }
493
+ return findings;
494
+ }
495
+ function customKindNames(cfg) {
496
+ const names = /* @__PURE__ */ new Set();
497
+ for (const entry of cfg.kinds) {
498
+ if (typeof entry !== "string" && !BUILT_IN_KINDS.has(entry.name)) names.add(entry.name);
499
+ }
500
+ return names;
501
+ }
502
+ function checkCustomKindFloor(relPath, data, cfg) {
503
+ const kind = data.kind;
504
+ if (typeof kind !== "string" || !customKindNames(cfg).has(kind)) return [];
505
+ const findings = [];
506
+ for (const field of UNIVERSAL_FLOOR) {
507
+ if (!Object.hasOwn(data, field)) {
508
+ findings.push({
509
+ path: relPath,
510
+ rule: "custom-kind-floor",
511
+ severity: "warning",
512
+ message: `custom kind "${kind}": missing "${field}" \u2014 the universal floor (title, summary, updated) keeps the page retrievable`
513
+ });
514
+ }
515
+ }
516
+ if (Object.hasOwn(data, "title") && !hasIndexableTitle(data)) {
517
+ findings.push({
518
+ path: relPath,
519
+ rule: "custom-kind-floor",
520
+ severity: "warning",
521
+ message: `custom kind "${kind}": "title" is empty \u2014 sync skips title-less pages; this page will not be indexed`
522
+ });
523
+ }
407
524
  return findings;
408
525
  }
409
526
  function checkTagsRequired(relPath, data) {
@@ -435,7 +552,7 @@ function checkFolderSlug(relPath, data) {
435
552
  function checkVocabulary(relPath, data, cfg) {
436
553
  const findings = [];
437
554
  const kind = data.kind;
438
- if (typeof kind === "string" && !cfg.kinds.includes(kind)) {
555
+ if (typeof kind === "string" && !cfg.kinds.some((k) => kindName(k) === kind)) {
439
556
  findings.push({
440
557
  path: relPath,
441
558
  rule: "kind-vocabulary",
@@ -496,7 +613,7 @@ function checkScopePath(relPath, data) {
496
613
  function refTarget(value) {
497
614
  if (typeof value !== "string") return null;
498
615
  const trimmed = value.trim();
499
- return trimmed === "" ? null : basename(trimmed);
616
+ return trimmed === "" ? null : basename2(trimmed);
500
617
  }
501
618
  function refTargets(value) {
502
619
  if (Array.isArray(value)) {
@@ -512,7 +629,7 @@ function checkBodyLinks(relPath, body, refIndex) {
512
629
  const findings = [];
513
630
  for (const link of extractWikilinks(stripCode(body))) {
514
631
  if (!link.target.endsWith(".md")) continue;
515
- if (!refIndex.has(basename(link.target))) {
632
+ if (!refIndex.has(basename2(link.target))) {
516
633
  findings.push({
517
634
  path: relPath,
518
635
  rule: "dangling-link",
@@ -574,7 +691,6 @@ function checkSupersededLink(relPath, data) {
574
691
  function checkIndexedPage(relPath, data, body, cfg, refIndex) {
575
692
  if (!isIndexed(relPath, data)) return [];
576
693
  return [
577
- ...checkRequiredFields(relPath, data),
578
694
  ...checkTagsRequired(relPath, data),
579
695
  ...checkFolderSlug(relPath, data),
580
696
  ...checkVocabulary(relPath, data, cfg),
@@ -601,12 +717,16 @@ function validatePage(relPath, raw, cfg, refIndex) {
601
717
  if (isPrimer(relPath)) {
602
718
  return checkBodyLinks(relPath, parsed.content, refIndex);
603
719
  }
604
- return checkIndexedPage(relPath, parsed.data, parsed.content, cfg, refIndex);
720
+ return [
721
+ ...checkRequiredFields(relPath, parsed.data),
722
+ ...checkCustomKindFloor(relPath, parsed.data, cfg),
723
+ ...checkIndexedPage(relPath, parsed.data, parsed.content, cfg, refIndex)
724
+ ];
605
725
  }
606
726
  function hasErrors(findings) {
607
727
  return findings.some((f) => f.severity === "error");
608
728
  }
609
- function basename(relPath) {
729
+ function basename2(relPath) {
610
730
  const last = relPath.split("/").pop() ?? relPath;
611
731
  return last.replace(/\.md$/, "");
612
732
  }
@@ -614,7 +734,7 @@ function validateSupersession(pages) {
614
734
  const adrs = /* @__PURE__ */ new Map();
615
735
  for (const { path, data } of pages) {
616
736
  if (data.kind !== "adr") continue;
617
- adrs.set(basename(path), {
737
+ adrs.set(basename2(path), {
618
738
  path,
619
739
  supersedes: refTargets(data.supersedes),
620
740
  supersededBy: refTargets(data.superseded_by)
@@ -660,7 +780,7 @@ function validateAmbiguousLinks(pages, basenameToPaths) {
660
780
  const here = locationKey(path);
661
781
  for (const link of extractWikilinks(stripCode(body))) {
662
782
  if (!link.target.endsWith(".md") || link.target.includes("/")) continue;
663
- const base = basename(link.target);
783
+ const base = basename2(link.target);
664
784
  const owners = basenameToPaths.get(base);
665
785
  if (!owners || owners.length < 2) continue;
666
786
  if (here !== null && owners.some((p) => locationKey(p) === here)) continue;
@@ -682,7 +802,7 @@ async function validateVault(root) {
682
802
  }));
683
803
  const basenameToPaths = /* @__PURE__ */ new Map();
684
804
  for (const f of all) {
685
- const b = basename(f.relPath);
805
+ const b = basename2(f.relPath);
686
806
  const owners = basenameToPaths.get(b);
687
807
  if (owners) owners.push(f.relPath);
688
808
  else basenameToPaths.set(b, [f.relPath]);
@@ -704,9 +824,22 @@ async function validateVault(root) {
704
824
  }
705
825
  findings.push(...validateSupersession(pages));
706
826
  findings.push(...validateAmbiguousLinks(linkPages, basenameToPaths));
827
+ for (const name of customKindNames(cfg)) {
828
+ const file = `templates/${name}.md`;
829
+ try {
830
+ await stat(join4(root, file));
831
+ } catch {
832
+ findings.push({
833
+ path: file,
834
+ rule: "custom-kind-template",
835
+ severity: "warning",
836
+ message: `custom kind "${name}" declared in vault.yaml has no template file`
837
+ });
838
+ }
839
+ }
707
840
  return findings;
708
841
  }
709
- var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS;
842
+ var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS, UNIVERSAL_FLOOR;
710
843
  var init_validate = __esm({
711
844
  "../cli/src/validate.ts"() {
712
845
  "use strict";
@@ -757,6 +890,7 @@ var init_validate = __esm({
757
890
  topic: /^research\/[^/]+\/index\.md$/,
758
891
  src: /^research\/[^/]+\/src-[^/]+\.md$/
759
892
  };
893
+ UNIVERSAL_FLOOR = ["title", "summary", "updated"];
760
894
  }
761
895
  });
762
896
 
@@ -765,10 +899,14 @@ var cli_exports = {};
765
899
  __export(cli_exports, {
766
900
  main: () => main,
767
901
  resolveCli: () => resolveCli,
902
+ runConfig: () => runConfig,
903
+ runDbReset: () => runDbReset,
768
904
  runSyncCommand: () => runSyncCommand,
769
905
  runValidate: () => runValidate,
770
906
  vaultRoot: () => vaultRoot
771
907
  });
908
+ import { existsSync, readdirSync, rmSync } from "node:fs";
909
+ import { dirname as dirname2, join as join5 } from "node:path";
772
910
  import { parseArgs } from "node:util";
773
911
  function resolveCli(argv) {
774
912
  const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
@@ -813,6 +951,76 @@ async function runSyncCommand() {
813
951
  }
814
952
  await runSync();
815
953
  }
954
+ function describeVault(root) {
955
+ const vault = canonicalVaultRoot(root);
956
+ const index = resolveIndexPath(vault);
957
+ let synced = "never";
958
+ if (existsSync(index)) {
959
+ const db = openDatabase(index);
960
+ try {
961
+ synced = getMeta(db, "last_synced") ?? "never";
962
+ } finally {
963
+ db.close();
964
+ }
965
+ }
966
+ return { vault, index, synced };
967
+ }
968
+ function printVault(d) {
969
+ console.log(`vault: ${d.vault}`);
970
+ console.log(`index: ${d.index}`);
971
+ console.log(`synced: ${d.synced}`);
972
+ }
973
+ function knownVaults() {
974
+ const root = indexRootDir();
975
+ if (!existsSync(root)) return [];
976
+ const known = [];
977
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
978
+ if (!entry.isDirectory()) continue;
979
+ const index = join5(root, entry.name, "index.db");
980
+ if (!existsSync(index)) continue;
981
+ const db = openDatabase(index);
982
+ try {
983
+ const vault = getMeta(db, "vault_root");
984
+ if (vault === null) continue;
985
+ known.push({ vault, index, synced: getMeta(db, "last_synced") ?? "never" });
986
+ } finally {
987
+ db.close();
988
+ }
989
+ }
990
+ return known;
991
+ }
992
+ async function runConfig() {
993
+ const root = process.env.WIKI_VAULT;
994
+ if (root) {
995
+ printVault(describeVault(root));
996
+ return;
997
+ }
998
+ const known = knownVaults();
999
+ if (known.length === 0) {
1000
+ console.error(
1001
+ "no vault specified and none known \u2014 pass a vault root, set WIKI_VAULT, or run `kmd sync <vault-root>` once"
1002
+ );
1003
+ process.exit(1);
1004
+ }
1005
+ known.forEach((d, i) => {
1006
+ if (i > 0) console.log("");
1007
+ printVault(d);
1008
+ });
1009
+ }
1010
+ async function runDbReset() {
1011
+ const root = process.env.WIKI_VAULT;
1012
+ if (!root) {
1013
+ console.error("usage: kmd db reset [<vault-root>] (or set WIKI_VAULT)");
1014
+ process.exit(2);
1015
+ }
1016
+ const dir = dirname2(resolveIndexPath(root));
1017
+ if (!existsSync(dir)) {
1018
+ console.log(`${dir} does not exist \u2014 nothing to reset`);
1019
+ return;
1020
+ }
1021
+ rmSync(dir, { recursive: true, force: true });
1022
+ console.log(`deleted ${dir}`);
1023
+ }
816
1024
  async function main() {
817
1025
  const resolution = resolveCli(process.argv.slice(2));
818
1026
  if (resolution.kind === "error") {
@@ -828,6 +1036,7 @@ async function main() {
828
1036
  var init_cli = __esm({
829
1037
  "../cli/src/cli.ts"() {
830
1038
  "use strict";
1039
+ init_database();
831
1040
  init_sync();
832
1041
  init_validate();
833
1042
  }
@@ -864,13 +1073,13 @@ var init_config2 = __esm({
864
1073
 
865
1074
  // ../mcp/src/db.ts
866
1075
  import { mkdirSync as mkdirSync2 } from "node:fs";
867
- import { homedir as homedir2 } from "node:os";
868
- import { join as join3 } from "node:path";
869
- function createDatabase() {
870
- const dbDir = join3(homedir2(), ".kmd", "db");
871
- const dbPath = join3(dbDir, "index.db");
872
- mkdirSync2(dbDir, { recursive: true });
873
- return openDatabase(dbPath);
1076
+ import { dirname as dirname3 } from "node:path";
1077
+ function createDatabase(vaultRoot2) {
1078
+ const dbPath = resolveIndexPath(vaultRoot2);
1079
+ mkdirSync2(dirname3(dbPath), { recursive: true });
1080
+ const db = openDatabase(dbPath);
1081
+ setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
1082
+ return db;
874
1083
  }
875
1084
  var init_db = __esm({
876
1085
  "../mcp/src/db.ts"() {
@@ -881,8 +1090,8 @@ var init_db = __esm({
881
1090
 
882
1091
  // ../mcp/src/lib/diag.ts
883
1092
  import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
884
- import { homedir as homedir3 } from "node:os";
885
- import { join as join4 } from "node:path";
1093
+ import { homedir as homedir2 } from "node:os";
1094
+ import { join as join6 } from "node:path";
886
1095
  function diag(msg, data) {
887
1096
  try {
888
1097
  const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
@@ -896,8 +1105,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
896
1105
  var init_diag = __esm({
897
1106
  "../mcp/src/lib/diag.ts"() {
898
1107
  "use strict";
899
- DIAG_DIR = join4(homedir3(), ".local", "state", "wiki-mcp");
900
- DIAG_LOG_PATH = join4(DIAG_DIR, "server.log");
1108
+ DIAG_DIR = join6(homedir2(), ".local", "state", "wiki-mcp");
1109
+ DIAG_LOG_PATH = join6(DIAG_DIR, "server.log");
901
1110
  try {
902
1111
  mkdirSync3(DIAG_DIR, { recursive: true });
903
1112
  } catch {
@@ -924,23 +1133,111 @@ var init_logger = __esm({
924
1133
  }
925
1134
  });
926
1135
 
1136
+ // ../mcp/src/vault-config.ts
1137
+ import { readFile as readFile4 } from "node:fs/promises";
1138
+ import { join as join7 } from "node:path";
1139
+ import { parse as parse2 } from "yaml";
1140
+ import { z as z4 } from "zod";
1141
+ function kindName2(entry) {
1142
+ return typeof entry === "string" ? entry : entry.name;
1143
+ }
1144
+ async function loadVaultConfig2(vaultRoot2) {
1145
+ const path = join7(vaultRoot2, "vault.yaml");
1146
+ let raw;
1147
+ try {
1148
+ raw = await readFile4(path, "utf8");
1149
+ } catch (err) {
1150
+ throw new Error(`vault.yaml not found at ${path}`, { cause: err });
1151
+ }
1152
+ const parsed = VaultConfigSchema2.safeParse(parse2(raw));
1153
+ if (!parsed.success) {
1154
+ const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1155
+ throw new Error(`Invalid vault.yaml at ${path}:
1156
+ ${issues}`);
1157
+ }
1158
+ return parsed.data;
1159
+ }
1160
+ var ScopeSchema2, KindEntrySchema2, VaultConfigSchema2, BUILT_IN_KINDS2;
1161
+ var init_vault_config = __esm({
1162
+ "../mcp/src/vault-config.ts"() {
1163
+ "use strict";
1164
+ ScopeSchema2 = z4.object({
1165
+ repo: z4.string().optional(),
1166
+ methodology: z4.string().optional(),
1167
+ status: z4.string()
1168
+ });
1169
+ KindEntrySchema2 = z4.union([
1170
+ z4.string(),
1171
+ z4.object({
1172
+ name: z4.string(),
1173
+ signal: z4.string(),
1174
+ where: z4.string()
1175
+ })
1176
+ ]);
1177
+ VaultConfigSchema2 = z4.object({
1178
+ scopes: z4.record(z4.string(), ScopeSchema2),
1179
+ kinds: z4.array(KindEntrySchema2),
1180
+ statuses: z4.array(z4.string()),
1181
+ methodologies: z4.array(z4.string()),
1182
+ tags: z4.object({
1183
+ canonical: z4.array(z4.string()),
1184
+ aliases: z4.record(z4.string(), z4.string())
1185
+ }),
1186
+ authoring_rules: z4.string().optional(),
1187
+ authoring_rules_extra: z4.string().optional(),
1188
+ sync_protocol: z4.string().optional(),
1189
+ sync_protocol_extra: z4.string().optional()
1190
+ }).superRefine((config, ctx) => {
1191
+ for (const [name, scope] of Object.entries(config.scopes)) {
1192
+ if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
1193
+ ctx.addIssue({
1194
+ code: "custom",
1195
+ path: ["scopes", name, "methodology"],
1196
+ message: `"${scope.methodology}" is not in the methodologies list`
1197
+ });
1198
+ }
1199
+ }
1200
+ });
1201
+ BUILT_IN_KINDS2 = /* @__PURE__ */ new Set([
1202
+ "project",
1203
+ "spec",
1204
+ "adr",
1205
+ "plan",
1206
+ "story",
1207
+ "ops",
1208
+ "topic",
1209
+ "article",
1210
+ "src",
1211
+ "note",
1212
+ "artifact",
1213
+ "prompt"
1214
+ ]);
1215
+ }
1216
+ });
1217
+
927
1218
  // ../mcp/src/resources/authoring.ts
928
1219
  function buildAuthoringRules(config) {
929
- return ["## Authoring rules", "", config.authoring_rules ?? DEFAULT_AUTHORING_RULES].join("\n");
1220
+ const parts = [(config.authoring_rules ?? DEFAULT_AUTHORING_RULES).trim()];
1221
+ if (config.authoring_rules_extra) parts.push(config.authoring_rules_extra.trim());
1222
+ return ["## Authoring rules", "", parts.join("\n\n")].join("\n");
930
1223
  }
931
1224
  function buildSyncProtocol(config) {
932
- return ["## Resync protocol", "", config.sync_protocol ?? DEFAULT_SYNC_PROTOCOL].join("\n");
1225
+ const parts = [(config.sync_protocol ?? DEFAULT_SYNC_PROTOCOL).trim()];
1226
+ if (config.sync_protocol_extra) parts.push(config.sync_protocol_extra.trim());
1227
+ return ["## Resync protocol", "", parts.join("\n\n")].join("\n");
933
1228
  }
934
1229
  function buildKindSelector(kinds) {
935
1230
  const lines = ["## Kind selector", "", "| Signal | Kind | Where |", "|---|---|---|"];
936
- for (const kind of kinds) {
937
- const pedagogy = KIND_PEDAGOGY.get(kind);
1231
+ for (const entry of kinds) {
1232
+ const name = kindName2(entry);
1233
+ const pedagogy = typeof entry === "string" ? KIND_PEDAGOGY.get(entry) : entry;
938
1234
  const signal = pedagogy?.signal ?? "\u2014";
939
1235
  const where = pedagogy?.where ?? "\u2014";
940
- lines.push(`| ${signal} | **${kind}** | ${where} |`);
1236
+ lines.push(`| ${signal} | **${name}** | ${where} |`);
941
1237
  }
942
- const hasNote = kinds.includes("note");
943
- const hasAdrAndSpec = kinds.includes("adr") && kinds.includes("spec");
1238
+ const names = kinds.map(kindName2);
1239
+ const hasNote = names.includes("note");
1240
+ const hasAdrAndSpec = names.includes("adr") && names.includes("spec");
944
1241
  if (hasNote || hasAdrAndSpec) {
945
1242
  const hints = [];
946
1243
  if (hasNote) hints.push("If none fits \u2192 note.");
@@ -953,12 +1250,16 @@ function buildKindSelector(kinds) {
953
1250
  }
954
1251
  return lines.join("\n");
955
1252
  }
1253
+ function buildStatusLine(statuses) {
1254
+ const isCanonical = statuses.length === CANONICAL_STATUS_FLOW.length && statuses.every((s, i) => s === CANONICAL_STATUS_FLOW[i]);
1255
+ return isCanonical ? `**Statuses:** ${statuses.join(" \u2192 ")} (one-directional; superseded requires superseded_by link)` : `**Statuses:** ${statuses.join(", ")}`;
1256
+ }
956
1257
  function buildVocabulary(config) {
957
1258
  const lines = [
958
1259
  "## Controlled vocabulary",
959
1260
  "",
960
- `**Kinds:** ${config.kinds.join(", ")}`,
961
- `**Statuses:** ${config.statuses.join(" \u2192 ")} (one-directional; superseded requires superseded_by link)`,
1261
+ `**Kinds:** ${config.kinds.map(kindName2).join(", ")}`,
1262
+ buildStatusLine(config.statuses),
962
1263
  `**Methodologies:** ${config.methodologies.join(", ")}`,
963
1264
  `**Canonical tags:** ${config.tags.canonical.join(", ")}`
964
1265
  ];
@@ -968,7 +1269,7 @@ function buildVocabulary(config) {
968
1269
  }
969
1270
  return lines.join("\n");
970
1271
  }
971
- function registerAuthoringResource(mcp, _vaultRoot, vaultConfig) {
1272
+ function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
972
1273
  mcp.registerResource(
973
1274
  "Authoring guide",
974
1275
  "wiki://authoring",
@@ -980,6 +1281,8 @@ function registerAuthoringResource(mcp, _vaultRoot, vaultConfig) {
980
1281
  const sections = [
981
1282
  "# Wiki authoring guide",
982
1283
  "",
1284
+ `Vault root: \`${canonicalVaultRoot(vaultRoot2)}\` \u2014 every page path below is relative to it; write files and run \`kmd validate\` / \`kmd sync\` against it.`,
1285
+ "",
983
1286
  buildKindSelector(vaultConfig.kinds),
984
1287
  "",
985
1288
  buildVocabulary(vaultConfig),
@@ -1004,10 +1307,12 @@ function registerAuthoringResource(mcp, _vaultRoot, vaultConfig) {
1004
1307
  }
1005
1308
  );
1006
1309
  }
1007
- var KIND_PEDAGOGY, DEFAULT_AUTHORING_RULES, DEFAULT_SYNC_PROTOCOL;
1310
+ var KIND_PEDAGOGY, DEFAULT_AUTHORING_RULES, DEFAULT_SYNC_PROTOCOL, CANONICAL_STATUS_FLOW;
1008
1311
  var init_authoring = __esm({
1009
1312
  "../mcp/src/resources/authoring.ts"() {
1010
1313
  "use strict";
1314
+ init_database();
1315
+ init_vault_config();
1011
1316
  KIND_PEDAGOGY = /* @__PURE__ */ new Map([
1012
1317
  [
1013
1318
  "project",
@@ -1089,33 +1394,73 @@ var init_authoring = __esm({
1089
1394
  ]
1090
1395
  ]);
1091
1396
  DEFAULT_AUTHORING_RULES = [
1397
+ "**Where things go**",
1398
+ "",
1092
1399
  "- **Use the matching template** via `wiki://template/{domain}/{kind}` (MCP) or from `templates/` (filesystem). Don't hand-roll frontmatter.",
1093
- '- **Quote prose-bearing frontmatter scalars.** `summary: "..."` \u2014 unquoted `Word: phrase` patterns break the YAML parser.',
1094
- "- **On any edit, update the frontmatter `updated` field.**",
1095
1400
  "- **Folder name = slug prefix in `projects/`.** `spec/spec-x.md`, `adr/adr-y.md`, `plan/plan-z.md`, `ops/ops-w.md`. Stories use `story-` prefix under `plan/{plan-name}/`.",
1096
- "- **Research is flat.** Articles are `{subject}.md`; sources are `src-{slug}.md`. Avoid generic slugs (`architecture.md`, `notes.md`).",
1401
+ "- **Research is flat.** Articles are `{subject}.md`; sources are `src-{slug}.md`. The topic folder names a *frame*, the article slug names a *subject*. Avoid generic slugs (`architecture.md`, `notes.md`) and slugs that collide with project scopes.",
1402
+ "- **Extend before you split.** Prefer sharpening the existing spec, ADR, or article over creating a near-duplicate page. A new page needs a new subject, not a new session.",
1403
+ "",
1404
+ "**Frontmatter**",
1405
+ "",
1406
+ '- **`summary` is the retrieval contract.** One sentence stating the page\'s decision or claim, not its topic. Search and `prime` rank by it \u2014 "Chose X over Y because Z" surfaces; "About the sync pipeline" sinks.',
1407
+ '- **Quote prose-bearing scalars.** `summary: "..."` \u2014 unquoted `Word: phrase` patterns break the YAML parser.',
1408
+ "- **On any edit, update `updated`.** Never change `created` \u2014 it is write-once.",
1097
1409
  "- **Notes have no `kind` field** \u2014 implied by location. Sync sets `kind: note`.",
1098
1410
  "- **Reuse existing tags** (visible in `prime` response `top_tags`). No synonyms.",
1411
+ "",
1412
+ "**Content**",
1413
+ "",
1414
+ "- **ADR and ops pages are predicate-only.** No definitional preambles for established vocabulary, no narrative, no marketing. The audience is the project team \u2014 assume fluency.",
1415
+ "- **Spec / ADR edits land inline with the change that surfaces them.** Don't queue corrections in plans \u2014 the spec reflects current code at every commit.",
1416
+ "",
1417
+ "**Linking**",
1418
+ "",
1419
+ "- **Cross-reference with `[[wikilinks]]`, and link every mention of another vault page** \u2014 backlinks are the navigation graph for humans and agents alike. Don't link pages that don't exist yet: dangling links fail validation.",
1099
1420
  "- **ADR supersession is bidirectional**: `superseded_by` on the old ADR + `supersedes` on the new one.",
1100
- "- **Sources convention**: external paths/URLs go inline in body text. Vault-internal `raw/` paths go in frontmatter `sources:` array. Don't mix the two surfaces.",
1101
- "- **Spec / ADR edits land inline with the slice that surfaces them.** Don't queue corrections in plans. The spec must reflect current code at every commit."
1421
+ "- **Sources convention**: external paths/URLs go inline in body text. Vault-internal `raw/` paths go in frontmatter `sources:` array. Don't mix the two surfaces."
1422
+ ].join("\n");
1423
+ DEFAULT_SYNC_PROTOCOL = [
1424
+ "Edit the smallest set of files that reflects the change. A milestone tick is plan-only; don't cascade to index.md unless phase or status changed. Controlled-vocabulary edits (`vault.yaml`) need explicit user approval.",
1425
+ "After editing wiki pages, run `kmd validate` and fix findings before `kmd sync` \u2014 it checks frontmatter shape, vocabulary membership, and link integrity."
1102
1426
  ].join("\n");
1103
- DEFAULT_SYNC_PROTOCOL = "Edit the smallest set of files that reflects the change. A milestone tick is plan-only; don't cascade to index.md unless phase or status changed. Controlled-vocabulary edits need explicit user approval.";
1427
+ CANONICAL_STATUS_FLOW = ["draft", "active", "superseded", "archived"];
1104
1428
  }
1105
1429
  });
1106
1430
 
1107
1431
  // ../mcp/src/resources/templates.ts
1108
- import { readFile as readFile4 } from "node:fs/promises";
1109
- import { join as join5 } from "node:path";
1110
- function registerTemplateResources(mcp, vaultRoot2) {
1111
- const dir = join5(vaultRoot2, "templates");
1112
- for (const tmpl of TEMPLATES) {
1432
+ import { readFile as readFile5 } from "node:fs/promises";
1433
+ import { join as join8 } from "node:path";
1434
+ function customTemplates(config) {
1435
+ const specs = [];
1436
+ for (const entry of config.kinds) {
1437
+ if (typeof entry === "string" || BUILT_IN_KINDS2.has(entry.name)) continue;
1438
+ specs.push({
1439
+ uri: `wiki://template/${entry.name}`,
1440
+ name: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
1441
+ file: `${entry.name}.md`,
1442
+ description: entry.signal
1443
+ });
1444
+ }
1445
+ return specs;
1446
+ }
1447
+ function registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
1448
+ const dir = join8(vaultRoot2, "templates");
1449
+ const templates = [...TEMPLATES, ...customTemplates(vaultConfig)];
1450
+ for (const tmpl of templates) {
1113
1451
  mcp.registerResource(
1114
1452
  tmpl.name,
1115
1453
  tmpl.uri,
1116
1454
  { description: tmpl.description, mimeType: "text/markdown" },
1117
1455
  async (uri) => {
1118
- const text = await readFile4(join5(dir, tmpl.file), "utf8");
1456
+ let text;
1457
+ try {
1458
+ text = await readFile5(join8(dir, tmpl.file), "utf8");
1459
+ } catch (err) {
1460
+ throw new Error(`template file missing: templates/${tmpl.file} (${tmpl.uri})`, {
1461
+ cause: err
1462
+ });
1463
+ }
1119
1464
  return {
1120
1465
  contents: [
1121
1466
  {
@@ -1129,7 +1474,7 @@ function registerTemplateResources(mcp, vaultRoot2) {
1129
1474
  );
1130
1475
  }
1131
1476
  const indexLines = ["# Wiki Templates", ""];
1132
- for (const tmpl of TEMPLATES) {
1477
+ for (const tmpl of templates) {
1133
1478
  indexLines.push(`- **${tmpl.name}** \u2014 \`${tmpl.uri}\` `);
1134
1479
  indexLines.push(` ${tmpl.description}`);
1135
1480
  }
@@ -1150,6 +1495,7 @@ var TEMPLATES;
1150
1495
  var init_templates = __esm({
1151
1496
  "../mcp/src/resources/templates.ts"() {
1152
1497
  "use strict";
1498
+ init_vault_config();
1153
1499
  TEMPLATES = [
1154
1500
  {
1155
1501
  uri: "wiki://template/project/index",
@@ -1289,15 +1635,15 @@ var init_toolResponse = __esm({
1289
1635
  });
1290
1636
 
1291
1637
  // ../mcp/src/tools/prime.ts
1292
- import { readFile as readFile5 } from "node:fs/promises";
1293
- import { basename as basename2, join as join6 } from "node:path";
1294
- import { z as z4 } from "zod";
1638
+ import { readFile as readFile6 } from "node:fs/promises";
1639
+ import { basename as basename3, join as join9 } from "node:path";
1640
+ import { z as z5 } from "zod";
1295
1641
  function pathSlug(p) {
1296
- return basename2(p).replace(/\.md$/, "");
1642
+ return basename3(p).replace(/\.md$/, "");
1297
1643
  }
1298
1644
  async function readIndexFm(vaultRoot2, scope) {
1299
1645
  try {
1300
- const raw = await readFile5(join6(vaultRoot2, "projects", scope, "index.md"), "utf8");
1646
+ const raw = await readFile6(join9(vaultRoot2, "projects", scope, "index.md"), "utf8");
1301
1647
  return parseFrontmatter2(raw).data;
1302
1648
  } catch {
1303
1649
  return {};
@@ -1305,7 +1651,7 @@ async function readIndexFm(vaultRoot2, scope) {
1305
1651
  }
1306
1652
  async function readPrimer(vaultRoot2, scope) {
1307
1653
  try {
1308
- const raw = await readFile5(join6(vaultRoot2, "projects", scope, "primer.md"), "utf8");
1654
+ const raw = await readFile6(join9(vaultRoot2, "projects", scope, "primer.md"), "utf8");
1309
1655
  return parseFrontmatter2(raw).content.trim().replace(/^#\s+[^\n]+\n+/, "");
1310
1656
  } catch {
1311
1657
  return "";
@@ -1370,6 +1716,7 @@ async function prime(deps, input) {
1370
1716
  for (const row of counts) countsRecord[row.kind] = Number(row.count);
1371
1717
  const data = {
1372
1718
  scope,
1719
+ vault_root: canonicalVaultRoot(vaultRoot2),
1373
1720
  title: fm.title ?? null,
1374
1721
  methodology: fm.methodology ?? null,
1375
1722
  phase: typeof fm.phase === "number" ? fm.phase : null,
@@ -1413,6 +1760,7 @@ function renderMarkdown(d, config, task) {
1413
1760
  const header = phaseLabel ? `${d.scope} \u2014 ${phaseLabel}` : d.scope;
1414
1761
  lines.push(`# ${header}`);
1415
1762
  if (d.summary) lines.push(d.summary);
1763
+ lines.push(`Vault root: \`${d.vault_root}\``);
1416
1764
  if (d.primer) {
1417
1765
  lines.push("", "## Primer", d.primer);
1418
1766
  }
@@ -1432,7 +1780,7 @@ function renderMarkdown(d, config, task) {
1432
1780
  lines.push(countEntries.map(([k, n]) => `${k}: ${n}`).join(" | "));
1433
1781
  }
1434
1782
  lines.push("", "## Vocabulary");
1435
- lines.push(`kinds: ${config.kinds.join(", ")}`);
1783
+ lines.push(`kinds: ${config.kinds.map(kindName2).join(", ")}`);
1436
1784
  lines.push(`statuses: ${config.statuses.join(", ")}`);
1437
1785
  lines.push(`tags: ${config.tags.canonical.join(", ")}`);
1438
1786
  if (d.top_tags.length > 0) {
@@ -1466,7 +1814,7 @@ function renderMarkdown(d, config, task) {
1466
1814
  lines.push(
1467
1815
  "",
1468
1816
  "---",
1469
- "Authoring wiki content? Read `wiki://authoring` for kind selector, rules, and templates."
1817
+ "Authoring wiki content? Read `wiki://authoring` for kind selector, rules, and templates. After edits: `kmd validate`, then `kmd sync`."
1470
1818
  );
1471
1819
  return lines.join("\n");
1472
1820
  }
@@ -1492,18 +1840,20 @@ var PrimeInputSchema;
1492
1840
  var init_prime = __esm({
1493
1841
  "../mcp/src/tools/prime.ts"() {
1494
1842
  "use strict";
1843
+ init_database();
1495
1844
  init_frontmatter2();
1496
1845
  init_fts();
1497
1846
  init_toolResponse();
1498
- PrimeInputSchema = z4.object({
1499
- scope: z4.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1500
- task: z4.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
1847
+ init_vault_config();
1848
+ PrimeInputSchema = z5.object({
1849
+ scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1850
+ task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
1501
1851
  });
1502
1852
  }
1503
1853
  });
1504
1854
 
1505
1855
  // ../mcp/src/tools/search.ts
1506
- import { z as z5 } from "zod";
1856
+ import { z as z6 } from "zod";
1507
1857
  function search(deps, input) {
1508
1858
  const ftsQuery = sanitizeFtsQuery(input.query);
1509
1859
  if (!ftsQuery) return { results: [] };
@@ -1550,15 +1900,15 @@ var init_search = __esm({
1550
1900
  "use strict";
1551
1901
  init_fts();
1552
1902
  init_toolResponse();
1553
- SearchInputSchema = z5.object({
1554
- query: z5.string().min(1).describe(
1903
+ SearchInputSchema = z6.object({
1904
+ query: z6.string().min(1).describe(
1555
1905
  "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1556
1906
  ),
1557
- scope: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1558
- kind: z5.string().optional().describe(
1907
+ scope: z6.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1908
+ kind: z6.string().optional().describe(
1559
1909
  "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1560
1910
  ),
1561
- limit: z5.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1911
+ limit: z6.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1562
1912
  });
1563
1913
  }
1564
1914
  });
@@ -1586,7 +1936,7 @@ function buildServer(args) {
1586
1936
  return handleSearch({ db }, input);
1587
1937
  }
1588
1938
  );
1589
- registerTemplateResources(mcp, vaultRoot2);
1939
+ registerTemplateResources(mcp, vaultRoot2, vaultConfig);
1590
1940
  registerAuthoringResource(mcp, vaultRoot2, vaultConfig);
1591
1941
  return mcp;
1592
1942
  }
@@ -1600,51 +1950,6 @@ var init_server = __esm({
1600
1950
  }
1601
1951
  });
1602
1952
 
1603
- // ../mcp/src/vault-config.ts
1604
- import { readFile as readFile6 } from "node:fs/promises";
1605
- import { join as join7 } from "node:path";
1606
- import { parse as parse2 } from "yaml";
1607
- import { z as z6 } from "zod";
1608
- async function loadVaultConfig2(vaultRoot2) {
1609
- const path = join7(vaultRoot2, "vault.yaml");
1610
- let raw;
1611
- try {
1612
- raw = await readFile6(path, "utf8");
1613
- } catch (err) {
1614
- throw new Error(`vault.yaml not found at ${path}`, { cause: err });
1615
- }
1616
- const parsed = VaultConfigSchema2.safeParse(parse2(raw));
1617
- if (!parsed.success) {
1618
- const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1619
- throw new Error(`Invalid vault.yaml at ${path}:
1620
- ${issues}`);
1621
- }
1622
- return parsed.data;
1623
- }
1624
- var ScopeSchema2, VaultConfigSchema2;
1625
- var init_vault_config = __esm({
1626
- "../mcp/src/vault-config.ts"() {
1627
- "use strict";
1628
- ScopeSchema2 = z6.object({
1629
- repo: z6.string().optional(),
1630
- methodology: z6.enum(["sdd", "tdd", "hybrid"]).optional(),
1631
- status: z6.string()
1632
- });
1633
- VaultConfigSchema2 = z6.object({
1634
- scopes: z6.record(z6.string(), ScopeSchema2),
1635
- kinds: z6.array(z6.string()),
1636
- statuses: z6.array(z6.string()),
1637
- methodologies: z6.array(z6.string()),
1638
- tags: z6.object({
1639
- canonical: z6.array(z6.string()),
1640
- aliases: z6.record(z6.string(), z6.string())
1641
- }),
1642
- authoring_rules: z6.string().optional(),
1643
- sync_protocol: z6.string().optional()
1644
- });
1645
- }
1646
- });
1647
-
1648
1953
  // ../mcp/src/start.ts
1649
1954
  var start_exports = {};
1650
1955
  __export(start_exports, {
@@ -1666,7 +1971,7 @@ async function startMcpServer() {
1666
1971
  { vault: config.wikiVault, serverName: config.serverName, serverVersion: config.serverVersion },
1667
1972
  "starting wiki-mcp on stdio"
1668
1973
  );
1669
- const db = createDatabase();
1974
+ const db = createDatabase(config.wikiVault);
1670
1975
  diag("database opened");
1671
1976
  const mcp = buildServer({
1672
1977
  name: config.serverName,
@@ -1712,10 +2017,11 @@ import { parseArgs as parseArgs2 } from "node:util";
1712
2017
  var USAGE = `usage: kmd <command> [options]
1713
2018
 
1714
2019
  commands:
1715
- sync vault \u2192 index sync (runs validate first)
1716
- validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
1717
- mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
1718
- db reset delete and recreate the index
2020
+ sync vault \u2192 index sync (runs validate first)
2021
+ validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
2022
+ mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
2023
+ config [<vault-root>] print vault + index resolution; with no vault, list known vaults
2024
+ db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
1719
2025
 
1720
2026
  options:
1721
2027
  --version print version
@@ -1755,25 +2061,20 @@ async function run() {
1755
2061
  await startMcpServer2();
1756
2062
  break;
1757
2063
  }
2064
+ case "config": {
2065
+ applyVaultRoot(1);
2066
+ const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2067
+ await runConfig2();
2068
+ break;
2069
+ }
1758
2070
  case "db": {
1759
2071
  const sub = positionals[1];
1760
2072
  if (sub === "reset") {
1761
- const { homedir: homedir4 } = await import("node:os");
1762
- const { join: join8 } = await import("node:path");
1763
- const { unlinkSync } = await import("node:fs");
1764
- const dbPath = join8(homedir4(), ".kmd", "db", "index.db");
1765
- let deleted = false;
1766
- for (const suffix of ["", "-wal", "-shm"]) {
1767
- try {
1768
- unlinkSync(dbPath + suffix);
1769
- deleted = true;
1770
- } catch (err) {
1771
- if (err.code !== "ENOENT") throw err;
1772
- }
1773
- }
1774
- console.log(deleted ? `deleted ${dbPath}` : `${dbPath} does not exist \u2014 nothing to reset`);
2073
+ applyVaultRoot(2);
2074
+ const { runDbReset: runDbReset2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2075
+ await runDbReset2();
1775
2076
  } else {
1776
- console.error(sub ? `unknown db subcommand: ${sub}` : "usage: kmd db reset");
2077
+ console.error(sub ? `unknown db subcommand: ${sub}` : "usage: kmd db reset [<vault-root>]");
1777
2078
  process.exit(2);
1778
2079
  }
1779
2080
  break;
@@ -1781,10 +2082,10 @@ async function run() {
1781
2082
  case "--version":
1782
2083
  case "-v": {
1783
2084
  const { readFileSync } = await import("node:fs");
1784
- const { join: join8, dirname } = await import("node:path");
2085
+ const { join: join10, dirname: dirname4 } = await import("node:path");
1785
2086
  const { fileURLToPath } = await import("node:url");
1786
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
1787
- const pkg = JSON.parse(readFileSync(join8(pkgDir, "package.json"), "utf8"));
2087
+ const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
2088
+ const pkg = JSON.parse(readFileSync(join10(pkgDir, "package.json"), "utf8"));
1788
2089
  console.log(pkg.version);
1789
2090
  break;
1790
2091
  }