@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 +247 -131
- package/dist/kmd.mjs.map +4 -4
- package/package.json +1 -1
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 =
|
|
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 {
|
|
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(
|
|
248
|
+
await recurse(join3(dir, entry.name));
|
|
213
249
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
214
|
-
out.push(
|
|
250
|
+
out.push(join3(dir, entry.name));
|
|
215
251
|
}
|
|
216
252
|
}
|
|
217
253
|
}
|
|
218
|
-
await recurse(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 :
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
return
|
|
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
|
|
982
|
-
import { join as
|
|
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 =
|
|
997
|
-
DIAG_LOG_PATH =
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
1525
|
-
import { z as
|
|
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
|
|
1704
|
+
return basename3(p).replace(/\.md$/, "");
|
|
1528
1705
|
}
|
|
1529
1706
|
async function readIndexFm(vaultRoot2, scope) {
|
|
1530
1707
|
try {
|
|
1531
|
-
const raw = await readFile6(
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
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
|
|
1903
|
-
validate [<path>]
|
|
1904
|
-
mcp [<vault-root>]
|
|
1905
|
-
|
|
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
|
-
|
|
1949
|
-
const {
|
|
1950
|
-
|
|
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:
|
|
2087
|
+
const { join: join10, dirname: dirname4 } = await import("node:path");
|
|
1972
2088
|
const { fileURLToPath } = await import("node:url");
|
|
1973
|
-
const pkgDir =
|
|
1974
|
-
const pkg = JSON.parse(readFileSync(
|
|
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
|
}
|