@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 +450 -149
- 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,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 =
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
248
|
+
await recurse(join3(dir, entry.name));
|
|
176
249
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
177
|
-
out.push(
|
|
250
|
+
out.push(join3(dir, entry.name));
|
|
178
251
|
}
|
|
179
252
|
}
|
|
180
253
|
}
|
|
181
|
-
await recurse(
|
|
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
|
|
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
|
|
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(
|
|
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 (
|
|
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.
|
|
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 :
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
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;
|
|
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
|
|
885
|
-
import { join as
|
|
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 =
|
|
900
|
-
DIAG_LOG_PATH =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
937
|
-
const
|
|
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} | **${
|
|
1236
|
+
lines.push(`| ${signal} | **${name}** | ${where} |`);
|
|
941
1237
|
}
|
|
942
|
-
const
|
|
943
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1109
|
-
import { join as
|
|
1110
|
-
function
|
|
1111
|
-
const
|
|
1112
|
-
for (const
|
|
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
|
-
|
|
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
|
|
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
|
|
1293
|
-
import { basename as
|
|
1294
|
-
import { z as
|
|
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
|
|
1642
|
+
return basename3(p).replace(/\.md$/, "");
|
|
1297
1643
|
}
|
|
1298
1644
|
async function readIndexFm(vaultRoot2, scope) {
|
|
1299
1645
|
try {
|
|
1300
|
-
const raw = await
|
|
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
|
|
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
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
|
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 =
|
|
1554
|
-
query:
|
|
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:
|
|
1558
|
-
kind:
|
|
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:
|
|
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
|
|
1716
|
-
validate [<path>]
|
|
1717
|
-
mcp [<vault-root>]
|
|
1718
|
-
|
|
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
|
-
|
|
1762
|
-
const {
|
|
1763
|
-
|
|
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:
|
|
2085
|
+
const { join: join10, dirname: dirname4 } = await import("node:path");
|
|
1785
2086
|
const { fileURLToPath } = await import("node:url");
|
|
1786
|
-
const pkgDir =
|
|
1787
|
-
const pkg = JSON.parse(readFileSync(
|
|
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
|
}
|