@bartolli/kmd 0.4.0 → 0.5.1

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,20 +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";
85
122
  function kindName(entry) {
86
123
  return typeof entry === "string" ? entry : entry.name;
87
124
  }
88
125
  async function loadVaultConfig(vaultRoot2) {
89
- const path = join(vaultRoot2, "vault.yaml");
126
+ const path = join2(vaultRoot2, "vault.yaml");
90
127
  let raw;
91
128
  try {
92
129
  raw = await readFile(path, "utf8");
@@ -184,11 +221,10 @@ var init_frontmatter = __esm({
184
221
  });
185
222
 
186
223
  // ../cli/src/sync.ts
187
- import { createHash } from "node:crypto";
224
+ import { createHash as createHash2 } from "node:crypto";
188
225
  import { mkdirSync } from "node:fs";
189
226
  import { readdir, readFile as readFile2 } from "node:fs/promises";
190
- import { homedir } from "node:os";
191
- import { join as join2, relative, sep } from "node:path";
227
+ import { dirname, join as join3, relative, sep } from "node:path";
192
228
  import { z as z2 } from "zod";
193
229
  function loadEnv() {
194
230
  const parsed = EnvSchema.safeParse({
@@ -209,20 +245,20 @@ async function walkMarkdown(root, domain) {
209
245
  for (const entry of entries) {
210
246
  if (entry.name.startsWith(".")) continue;
211
247
  if (entry.isDirectory()) {
212
- await recurse(join2(dir, entry.name));
248
+ await recurse(join3(dir, entry.name));
213
249
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
214
- out.push(join2(dir, entry.name));
250
+ out.push(join3(dir, entry.name));
215
251
  }
216
252
  }
217
253
  }
218
- await recurse(join2(root, domain));
254
+ await recurse(join3(root, domain));
219
255
  return out;
220
256
  }
221
257
  function toRelativePath(root, absolute) {
222
258
  return relative(root, absolute).split(sep).join("/");
223
259
  }
224
260
  function sha256(content) {
225
- return createHash("sha256").update(content).digest("hex");
261
+ return createHash2("sha256").update(content).digest("hex");
226
262
  }
227
263
  function extractWikilinks(body) {
228
264
  const links = [];
@@ -339,12 +375,11 @@ function syncPage(db, fields) {
339
375
  }
340
376
  async function runSync() {
341
377
  const env = loadEnv();
342
- const dbDir = join2(homedir(), ".kmd", "db");
343
- const dbPath = join2(dbDir, "index.db");
378
+ const dbPath = resolveIndexPath(env.WIKI_VAULT);
344
379
  console.log(`sync: ${env.WIKI_VAULT} \u2192 ${dbPath}`);
345
380
  const vaultConfig = await loadVaultConfig(env.WIKI_VAULT);
346
381
  const scopes = new Set(Object.keys(vaultConfig.scopes));
347
- mkdirSync(dbDir, { recursive: true });
382
+ mkdirSync(dirname(dbPath), { recursive: true });
348
383
  const db = openDatabase(dbPath);
349
384
  try {
350
385
  const files = [];
@@ -384,6 +419,8 @@ async function runSync() {
384
419
  console.warn("no indexable pages found; skipping orphan deletion (safety)");
385
420
  }
386
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());
387
424
  console.log(
388
425
  `done: ${changed} changed, ${unchanged} unchanged, ${skipped} skipped, ${pagesDeleted} pages deleted, ${linksDeleted} link orphans cleared`
389
426
  );
@@ -418,7 +455,7 @@ var init_sync = __esm({
418
455
 
419
456
  // ../cli/src/validate.ts
420
457
  import { readFile as readFile3, stat } from "node:fs/promises";
421
- import { join as join3 } from "node:path";
458
+ import { join as join4 } from "node:path";
422
459
  function hasIndexableTitle(data) {
423
460
  return typeof data.title === "string" && data.title.trim() !== "";
424
461
  }
@@ -576,7 +613,7 @@ function checkScopePath(relPath, data) {
576
613
  function refTarget(value) {
577
614
  if (typeof value !== "string") return null;
578
615
  const trimmed = value.trim();
579
- return trimmed === "" ? null : basename(trimmed);
616
+ return trimmed === "" ? null : basename2(trimmed);
580
617
  }
581
618
  function refTargets(value) {
582
619
  if (Array.isArray(value)) {
@@ -592,7 +629,7 @@ function checkBodyLinks(relPath, body, refIndex) {
592
629
  const findings = [];
593
630
  for (const link of extractWikilinks(stripCode(body))) {
594
631
  if (!link.target.endsWith(".md")) continue;
595
- if (!refIndex.has(basename(link.target))) {
632
+ if (!refIndex.has(basename2(link.target))) {
596
633
  findings.push({
597
634
  path: relPath,
598
635
  rule: "dangling-link",
@@ -689,7 +726,7 @@ function validatePage(relPath, raw, cfg, refIndex) {
689
726
  function hasErrors(findings) {
690
727
  return findings.some((f) => f.severity === "error");
691
728
  }
692
- function basename(relPath) {
729
+ function basename2(relPath) {
693
730
  const last = relPath.split("/").pop() ?? relPath;
694
731
  return last.replace(/\.md$/, "");
695
732
  }
@@ -697,7 +734,7 @@ function validateSupersession(pages) {
697
734
  const adrs = /* @__PURE__ */ new Map();
698
735
  for (const { path, data } of pages) {
699
736
  if (data.kind !== "adr") continue;
700
- adrs.set(basename(path), {
737
+ adrs.set(basename2(path), {
701
738
  path,
702
739
  supersedes: refTargets(data.supersedes),
703
740
  supersededBy: refTargets(data.superseded_by)
@@ -743,7 +780,7 @@ function validateAmbiguousLinks(pages, basenameToPaths) {
743
780
  const here = locationKey(path);
744
781
  for (const link of extractWikilinks(stripCode(body))) {
745
782
  if (!link.target.endsWith(".md") || link.target.includes("/")) continue;
746
- const base = basename(link.target);
783
+ const base = basename2(link.target);
747
784
  const owners = basenameToPaths.get(base);
748
785
  if (!owners || owners.length < 2) continue;
749
786
  if (here !== null && owners.some((p) => locationKey(p) === here)) continue;
@@ -765,7 +802,7 @@ async function validateVault(root) {
765
802
  }));
766
803
  const basenameToPaths = /* @__PURE__ */ new Map();
767
804
  for (const f of all) {
768
- const b = basename(f.relPath);
805
+ const b = basename2(f.relPath);
769
806
  const owners = basenameToPaths.get(b);
770
807
  if (owners) owners.push(f.relPath);
771
808
  else basenameToPaths.set(b, [f.relPath]);
@@ -790,7 +827,7 @@ async function validateVault(root) {
790
827
  for (const name of customKindNames(cfg)) {
791
828
  const file = `templates/${name}.md`;
792
829
  try {
793
- await stat(join3(root, file));
830
+ await stat(join4(root, file));
794
831
  } catch {
795
832
  findings.push({
796
833
  path: file,
@@ -862,10 +899,14 @@ var cli_exports = {};
862
899
  __export(cli_exports, {
863
900
  main: () => main,
864
901
  resolveCli: () => resolveCli,
902
+ runConfig: () => runConfig,
903
+ runDbReset: () => runDbReset,
865
904
  runSyncCommand: () => runSyncCommand,
866
905
  runValidate: () => runValidate,
867
906
  vaultRoot: () => vaultRoot
868
907
  });
908
+ import { existsSync, readdirSync, rmSync } from "node:fs";
909
+ import { dirname as dirname2, join as join5 } from "node:path";
869
910
  import { parseArgs } from "node:util";
870
911
  function resolveCli(argv) {
871
912
  const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
@@ -910,6 +951,76 @@ async function runSyncCommand() {
910
951
  }
911
952
  await runSync();
912
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
+ }
913
1024
  async function main() {
914
1025
  const resolution = resolveCli(process.argv.slice(2));
915
1026
  if (resolution.kind === "error") {
@@ -925,6 +1036,7 @@ async function main() {
925
1036
  var init_cli = __esm({
926
1037
  "../cli/src/cli.ts"() {
927
1038
  "use strict";
1039
+ init_database();
928
1040
  init_sync();
929
1041
  init_validate();
930
1042
  }
@@ -961,13 +1073,13 @@ var init_config2 = __esm({
961
1073
 
962
1074
  // ../mcp/src/db.ts
963
1075
  import { mkdirSync as mkdirSync2 } from "node:fs";
964
- import { homedir as homedir2 } from "node:os";
965
- import { join as join4 } from "node:path";
966
- function createDatabase() {
967
- const dbDir = join4(homedir2(), ".kmd", "db");
968
- const dbPath = join4(dbDir, "index.db");
969
- mkdirSync2(dbDir, { recursive: true });
970
- 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;
971
1083
  }
972
1084
  var init_db = __esm({
973
1085
  "../mcp/src/db.ts"() {
@@ -978,8 +1090,8 @@ var init_db = __esm({
978
1090
 
979
1091
  // ../mcp/src/lib/diag.ts
980
1092
  import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
981
- import { homedir as homedir3 } from "node:os";
982
- import { join as join5 } from "node:path";
1093
+ import { homedir as homedir2 } from "node:os";
1094
+ import { join as join6 } from "node:path";
983
1095
  function diag(msg, data) {
984
1096
  try {
985
1097
  const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
@@ -993,8 +1105,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
993
1105
  var init_diag = __esm({
994
1106
  "../mcp/src/lib/diag.ts"() {
995
1107
  "use strict";
996
- DIAG_DIR = join5(homedir3(), ".local", "state", "wiki-mcp");
997
- DIAG_LOG_PATH = join5(DIAG_DIR, "server.log");
1108
+ DIAG_DIR = join6(homedir2(), ".local", "state", "wiki-mcp");
1109
+ DIAG_LOG_PATH = join6(DIAG_DIR, "server.log");
998
1110
  try {
999
1111
  mkdirSync3(DIAG_DIR, { recursive: true });
1000
1112
  } catch {
@@ -1023,14 +1135,14 @@ var init_logger = __esm({
1023
1135
 
1024
1136
  // ../mcp/src/vault-config.ts
1025
1137
  import { readFile as readFile4 } from "node:fs/promises";
1026
- import { join as join6 } from "node:path";
1138
+ import { join as join7 } from "node:path";
1027
1139
  import { parse as parse2 } from "yaml";
1028
1140
  import { z as z4 } from "zod";
1029
1141
  function kindName2(entry) {
1030
1142
  return typeof entry === "string" ? entry : entry.name;
1031
1143
  }
1032
1144
  async function loadVaultConfig2(vaultRoot2) {
1033
- const path = join6(vaultRoot2, "vault.yaml");
1145
+ const path = join7(vaultRoot2, "vault.yaml");
1034
1146
  let raw;
1035
1147
  try {
1036
1148
  raw = await readFile4(path, "utf8");
@@ -1157,7 +1269,7 @@ function buildVocabulary(config) {
1157
1269
  }
1158
1270
  return lines.join("\n");
1159
1271
  }
1160
- function registerAuthoringResource(mcp, _vaultRoot, vaultConfig) {
1272
+ function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
1161
1273
  mcp.registerResource(
1162
1274
  "Authoring guide",
1163
1275
  "wiki://authoring",
@@ -1169,6 +1281,8 @@ function registerAuthoringResource(mcp, _vaultRoot, vaultConfig) {
1169
1281
  const sections = [
1170
1282
  "# Wiki authoring guide",
1171
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
+ "",
1172
1286
  buildKindSelector(vaultConfig.kinds),
1173
1287
  "",
1174
1288
  buildVocabulary(vaultConfig),
@@ -1197,6 +1311,7 @@ var KIND_PEDAGOGY, DEFAULT_AUTHORING_RULES, DEFAULT_SYNC_PROTOCOL, CANONICAL_STA
1197
1311
  var init_authoring = __esm({
1198
1312
  "../mcp/src/resources/authoring.ts"() {
1199
1313
  "use strict";
1314
+ init_database();
1200
1315
  init_vault_config();
1201
1316
  KIND_PEDAGOGY = /* @__PURE__ */ new Map([
1202
1317
  [
@@ -1315,7 +1430,7 @@ var init_authoring = __esm({
1315
1430
 
1316
1431
  // ../mcp/src/resources/templates.ts
1317
1432
  import { readFile as readFile5 } from "node:fs/promises";
1318
- import { join as join7 } from "node:path";
1433
+ import { join as join8 } from "node:path";
1319
1434
  function customTemplates(config) {
1320
1435
  const specs = [];
1321
1436
  for (const entry of config.kinds) {
@@ -1330,7 +1445,7 @@ function customTemplates(config) {
1330
1445
  return specs;
1331
1446
  }
1332
1447
  function registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
1333
- const dir = join7(vaultRoot2, "templates");
1448
+ const dir = join8(vaultRoot2, "templates");
1334
1449
  const templates = [...TEMPLATES, ...customTemplates(vaultConfig)];
1335
1450
  for (const tmpl of templates) {
1336
1451
  mcp.registerResource(
@@ -1340,7 +1455,7 @@ function registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
1340
1455
  async (uri) => {
1341
1456
  let text;
1342
1457
  try {
1343
- text = await readFile5(join7(dir, tmpl.file), "utf8");
1458
+ text = await readFile5(join8(dir, tmpl.file), "utf8");
1344
1459
  } catch (err) {
1345
1460
  throw new Error(`template file missing: templates/${tmpl.file} (${tmpl.uri})`, {
1346
1461
  cause: err
@@ -1519,16 +1634,78 @@ var init_toolResponse = __esm({
1519
1634
  }
1520
1635
  });
1521
1636
 
1637
+ // ../mcp/src/tools/search.ts
1638
+ import { z as z5 } from "zod";
1639
+ function search(deps, input) {
1640
+ const ftsQuery = sanitizeFtsQuery(input.query);
1641
+ if (!ftsQuery) return { results: [] };
1642
+ let sql = `SELECT p.path, p.title, p.kind, p.summary, p.scope, ${FTS_RANK} AS score
1643
+ FROM pages_fts
1644
+ JOIN pages p ON p.id = pages_fts.rowid
1645
+ WHERE pages_fts MATCH ?`;
1646
+ const params = [ftsQuery];
1647
+ if (input.scope) {
1648
+ sql += " AND p.scope = ?";
1649
+ params.push(input.scope);
1650
+ }
1651
+ if (input.kind) {
1652
+ sql += " AND p.kind = ?";
1653
+ params.push(input.kind);
1654
+ }
1655
+ sql += ` ORDER BY ${FTS_RANK} LIMIT ?`;
1656
+ params.push(input.limit);
1657
+ const rows = deps.db.prepare(sql).all(...params);
1658
+ return {
1659
+ results: rows.map((r) => ({
1660
+ path: r.path,
1661
+ title: r.title,
1662
+ kind: r.kind,
1663
+ summary: r.summary,
1664
+ scope: r.scope,
1665
+ score: r.score
1666
+ }))
1667
+ };
1668
+ }
1669
+ function handleSearch(deps, input) {
1670
+ try {
1671
+ return textJson(search(deps, input));
1672
+ } catch (err) {
1673
+ return textError({
1674
+ code: "SEARCH_FAILED",
1675
+ message: err instanceof Error ? err.message : String(err)
1676
+ });
1677
+ }
1678
+ }
1679
+ var SearchInputSchema, FTS_RANK;
1680
+ var init_search = __esm({
1681
+ "../mcp/src/tools/search.ts"() {
1682
+ "use strict";
1683
+ init_fts();
1684
+ init_toolResponse();
1685
+ SearchInputSchema = z5.object({
1686
+ query: z5.string().min(1).describe(
1687
+ "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1688
+ ),
1689
+ scope: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1690
+ kind: z5.string().optional().describe(
1691
+ "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1692
+ ),
1693
+ limit: z5.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1694
+ });
1695
+ FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
1696
+ }
1697
+ });
1698
+
1522
1699
  // ../mcp/src/tools/prime.ts
1523
1700
  import { readFile as readFile6 } from "node:fs/promises";
1524
- import { basename as basename2, join as join8 } from "node:path";
1525
- import { z as z5 } from "zod";
1701
+ import { basename as basename3, join as join9 } from "node:path";
1702
+ import { z as z6 } from "zod";
1526
1703
  function pathSlug(p) {
1527
- return basename2(p).replace(/\.md$/, "");
1704
+ return basename3(p).replace(/\.md$/, "");
1528
1705
  }
1529
1706
  async function readIndexFm(vaultRoot2, scope) {
1530
1707
  try {
1531
- const raw = await readFile6(join8(vaultRoot2, "projects", scope, "index.md"), "utf8");
1708
+ const raw = await readFile6(join9(vaultRoot2, "projects", scope, "index.md"), "utf8");
1532
1709
  return parseFrontmatter2(raw).data;
1533
1710
  } catch {
1534
1711
  return {};
@@ -1536,7 +1713,7 @@ async function readIndexFm(vaultRoot2, scope) {
1536
1713
  }
1537
1714
  async function readPrimer(vaultRoot2, scope) {
1538
1715
  try {
1539
- const raw = await readFile6(join8(vaultRoot2, "projects", scope, "primer.md"), "utf8");
1716
+ const raw = await readFile6(join9(vaultRoot2, "projects", scope, "primer.md"), "utf8");
1540
1717
  return parseFrontmatter2(raw).content.trim().replace(/^#\s+[^\n]+\n+/, "");
1541
1718
  } catch {
1542
1719
  return "";
@@ -1585,11 +1762,11 @@ async function prime(deps, input) {
1585
1762
  const ftsQuery = sanitizeFtsQuery(task);
1586
1763
  if (ftsQuery) {
1587
1764
  relevant = db.prepare(
1588
- `SELECT p.path, p.title, bm25(pages_fts) AS score
1765
+ `SELECT p.path, p.title, ${FTS_RANK} AS score
1589
1766
  FROM pages_fts
1590
1767
  JOIN pages p ON p.id = pages_fts.rowid
1591
1768
  WHERE pages_fts MATCH ? AND p.scope = ?
1592
- ORDER BY bm25(pages_fts) LIMIT 3`
1769
+ ORDER BY ${FTS_RANK} LIMIT 3`
1593
1770
  ).all(ftsQuery, scope).map((row) => ({
1594
1771
  path: row.path,
1595
1772
  title: row.title,
@@ -1601,6 +1778,7 @@ async function prime(deps, input) {
1601
1778
  for (const row of counts) countsRecord[row.kind] = Number(row.count);
1602
1779
  const data = {
1603
1780
  scope,
1781
+ vault_root: canonicalVaultRoot(vaultRoot2),
1604
1782
  title: fm.title ?? null,
1605
1783
  methodology: fm.methodology ?? null,
1606
1784
  phase: typeof fm.phase === "number" ? fm.phase : null,
@@ -1644,6 +1822,7 @@ function renderMarkdown(d, config, task) {
1644
1822
  const header = phaseLabel ? `${d.scope} \u2014 ${phaseLabel}` : d.scope;
1645
1823
  lines.push(`# ${header}`);
1646
1824
  if (d.summary) lines.push(d.summary);
1825
+ lines.push(`Vault root: \`${d.vault_root}\``);
1647
1826
  if (d.primer) {
1648
1827
  lines.push("", "## Primer", d.primer);
1649
1828
  }
@@ -1723,74 +1902,15 @@ var PrimeInputSchema;
1723
1902
  var init_prime = __esm({
1724
1903
  "../mcp/src/tools/prime.ts"() {
1725
1904
  "use strict";
1905
+ init_database();
1726
1906
  init_frontmatter2();
1727
1907
  init_fts();
1728
1908
  init_toolResponse();
1729
1909
  init_vault_config();
1730
- PrimeInputSchema = z5.object({
1731
- scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1732
- task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
1733
- });
1734
- }
1735
- });
1736
-
1737
- // ../mcp/src/tools/search.ts
1738
- import { z as z6 } from "zod";
1739
- function search(deps, input) {
1740
- const ftsQuery = sanitizeFtsQuery(input.query);
1741
- if (!ftsQuery) return { results: [] };
1742
- let sql = `SELECT p.path, p.title, p.kind, p.summary, p.scope, bm25(pages_fts) AS score
1743
- FROM pages_fts
1744
- JOIN pages p ON p.id = pages_fts.rowid
1745
- WHERE pages_fts MATCH ?`;
1746
- const params = [ftsQuery];
1747
- if (input.scope) {
1748
- sql += " AND p.scope = ?";
1749
- params.push(input.scope);
1750
- }
1751
- if (input.kind) {
1752
- sql += " AND p.kind = ?";
1753
- params.push(input.kind);
1754
- }
1755
- sql += " ORDER BY bm25(pages_fts) LIMIT ?";
1756
- params.push(input.limit);
1757
- const rows = deps.db.prepare(sql).all(...params);
1758
- return {
1759
- results: rows.map((r) => ({
1760
- path: r.path,
1761
- title: r.title,
1762
- kind: r.kind,
1763
- summary: r.summary,
1764
- scope: r.scope,
1765
- score: r.score
1766
- }))
1767
- };
1768
- }
1769
- function handleSearch(deps, input) {
1770
- try {
1771
- return textJson(search(deps, input));
1772
- } catch (err) {
1773
- return textError({
1774
- code: "SEARCH_FAILED",
1775
- message: err instanceof Error ? err.message : String(err)
1776
- });
1777
- }
1778
- }
1779
- var SearchInputSchema;
1780
- var init_search = __esm({
1781
- "../mcp/src/tools/search.ts"() {
1782
- "use strict";
1783
- init_fts();
1784
- init_toolResponse();
1785
- SearchInputSchema = z6.object({
1786
- query: z6.string().min(1).describe(
1787
- "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1788
- ),
1789
- scope: z6.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1790
- kind: z6.string().optional().describe(
1791
- "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1792
- ),
1793
- limit: z6.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1910
+ init_search();
1911
+ PrimeInputSchema = z6.object({
1912
+ scope: z6.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1913
+ task: z6.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
1794
1914
  });
1795
1915
  }
1796
1916
  });
@@ -1853,7 +1973,7 @@ async function startMcpServer() {
1853
1973
  { vault: config.wikiVault, serverName: config.serverName, serverVersion: config.serverVersion },
1854
1974
  "starting wiki-mcp on stdio"
1855
1975
  );
1856
- const db = createDatabase();
1976
+ const db = createDatabase(config.wikiVault);
1857
1977
  diag("database opened");
1858
1978
  const mcp = buildServer({
1859
1979
  name: config.serverName,
@@ -1899,10 +2019,11 @@ import { parseArgs as parseArgs2 } from "node:util";
1899
2019
  var USAGE = `usage: kmd <command> [options]
1900
2020
 
1901
2021
  commands:
1902
- sync vault \u2192 index sync (runs validate first)
1903
- validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
1904
- mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
1905
- db reset delete and recreate the index
2022
+ sync vault \u2192 index sync (runs validate first)
2023
+ validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
2024
+ mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
2025
+ config [<vault-root>] print vault + index resolution; with no vault, list known vaults
2026
+ db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
1906
2027
 
1907
2028
  options:
1908
2029
  --version print version
@@ -1942,25 +2063,20 @@ async function run() {
1942
2063
  await startMcpServer2();
1943
2064
  break;
1944
2065
  }
2066
+ case "config": {
2067
+ applyVaultRoot(1);
2068
+ const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2069
+ await runConfig2();
2070
+ break;
2071
+ }
1945
2072
  case "db": {
1946
2073
  const sub = positionals[1];
1947
2074
  if (sub === "reset") {
1948
- const { homedir: homedir4 } = await import("node:os");
1949
- const { join: join9 } = await import("node:path");
1950
- const { unlinkSync } = await import("node:fs");
1951
- const dbPath = join9(homedir4(), ".kmd", "db", "index.db");
1952
- let deleted = false;
1953
- for (const suffix of ["", "-wal", "-shm"]) {
1954
- try {
1955
- unlinkSync(dbPath + suffix);
1956
- deleted = true;
1957
- } catch (err) {
1958
- if (err.code !== "ENOENT") throw err;
1959
- }
1960
- }
1961
- console.log(deleted ? `deleted ${dbPath}` : `${dbPath} does not exist \u2014 nothing to reset`);
2075
+ applyVaultRoot(2);
2076
+ const { runDbReset: runDbReset2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2077
+ await runDbReset2();
1962
2078
  } else {
1963
- console.error(sub ? `unknown db subcommand: ${sub}` : "usage: kmd db reset");
2079
+ console.error(sub ? `unknown db subcommand: ${sub}` : "usage: kmd db reset [<vault-root>]");
1964
2080
  process.exit(2);
1965
2081
  }
1966
2082
  break;
@@ -1968,10 +2084,10 @@ async function run() {
1968
2084
  case "--version":
1969
2085
  case "-v": {
1970
2086
  const { readFileSync } = await import("node:fs");
1971
- const { join: join9, dirname } = await import("node:path");
2087
+ const { join: join10, dirname: dirname4 } = await import("node:path");
1972
2088
  const { fileURLToPath } = await import("node:url");
1973
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
1974
- const pkg = JSON.parse(readFileSync(join9(pkgDir, "package.json"), "utf8"));
2089
+ const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
2090
+ const pkg = JSON.parse(readFileSync(join10(pkgDir, "package.json"), "utf8"));
1975
2091
  console.log(pkg.version);
1976
2092
  break;
1977
2093
  }