@bartolli/kmd 0.11.0 → 0.12.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 +859 -324
- package/dist/kmd.mjs.map +4 -4
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -16,9 +16,9 @@ var __export = (target, all) => {
|
|
|
16
16
|
|
|
17
17
|
// ../db/src/database.ts
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
|
-
import { realpathSync } from "node:fs";
|
|
19
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
|
-
import { basename, join, resolve } from "node:path";
|
|
21
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
22
22
|
import { DatabaseSync } from "node:sqlite";
|
|
23
23
|
function openDatabase(dbPath) {
|
|
24
24
|
const db = new DatabaseSync(dbPath);
|
|
@@ -33,20 +33,35 @@ function kmdHome() {
|
|
|
33
33
|
function indexRootDir() {
|
|
34
34
|
return join(kmdHome(), "db");
|
|
35
35
|
}
|
|
36
|
-
function canonicalVaultRoot(
|
|
36
|
+
function canonicalVaultRoot(vaultRoot) {
|
|
37
37
|
try {
|
|
38
|
-
return realpathSync(
|
|
38
|
+
return realpathSync(vaultRoot);
|
|
39
39
|
} catch {
|
|
40
|
-
return resolve(
|
|
40
|
+
return resolve(vaultRoot);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
function vaultKey(
|
|
44
|
-
const canonical = canonicalVaultRoot(
|
|
43
|
+
function vaultKey(vaultRoot) {
|
|
44
|
+
const canonical = canonicalVaultRoot(vaultRoot);
|
|
45
45
|
const hash = createHash("sha256").update(canonical).digest("hex").slice(0, 8);
|
|
46
46
|
return `${basename(canonical)}-${hash}`;
|
|
47
47
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
48
|
+
function tierKmdDir(canonical) {
|
|
49
|
+
for (const stateHome of [join(canonical, ".kmd"), join(dirname(canonical), ".kmd")]) {
|
|
50
|
+
if (existsSync(stateHome)) return stateHome;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function resolveIndexPath(vaultRoot) {
|
|
55
|
+
const canonical = canonicalVaultRoot(vaultRoot);
|
|
56
|
+
const tier = tierKmdDir(canonical);
|
|
57
|
+
if (tier !== null) return join(tier, "db", "index.db");
|
|
58
|
+
return join(indexRootDir(), vaultKey(canonical), "index.db");
|
|
59
|
+
}
|
|
60
|
+
function resolveStateDir(vaultRoot) {
|
|
61
|
+
const canonical = canonicalVaultRoot(vaultRoot);
|
|
62
|
+
const tier = tierKmdDir(canonical);
|
|
63
|
+
if (tier !== null) return join(tier, "state", "hook");
|
|
64
|
+
return join(kmdHome(), "state", "hook");
|
|
50
65
|
}
|
|
51
66
|
function getMeta(db, key) {
|
|
52
67
|
const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
@@ -116,11 +131,159 @@ CREATE TABLE IF NOT EXISTS meta (
|
|
|
116
131
|
}
|
|
117
132
|
});
|
|
118
133
|
|
|
134
|
+
// ../db/src/kmd-config.ts
|
|
135
|
+
var kmd_config_exports = {};
|
|
136
|
+
__export(kmd_config_exports, {
|
|
137
|
+
expandVars: () => expandVars,
|
|
138
|
+
findProjectTier: () => findProjectTier,
|
|
139
|
+
globalConfigPath: () => globalConfigPath,
|
|
140
|
+
loadGlobalConfig: () => loadGlobalConfig,
|
|
141
|
+
resolveVaultRoot: () => resolveVaultRoot,
|
|
142
|
+
setGlobalConfigValue: () => setGlobalConfigValue,
|
|
143
|
+
unsetGlobalConfigValue: () => unsetGlobalConfigValue
|
|
144
|
+
});
|
|
145
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
146
|
+
import { homedir as homedir2 } from "node:os";
|
|
147
|
+
import { dirname as dirname2, isAbsolute, join as join2, resolve as resolve2 } from "node:path";
|
|
148
|
+
import { Document, parse, parseDocument } from "yaml";
|
|
149
|
+
import { z } from "zod";
|
|
150
|
+
function expandVars(value, env = process.env) {
|
|
151
|
+
return value.replace(
|
|
152
|
+
/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g,
|
|
153
|
+
(_match, name, fallback) => {
|
|
154
|
+
const resolved = env[name];
|
|
155
|
+
if (resolved !== void 0 && resolved !== "") return resolved;
|
|
156
|
+
if (fallback !== void 0) return fallback;
|
|
157
|
+
throw new Error(`unresolved \${${name}} (variable unset, no :-default)`);
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
function expandHome(value) {
|
|
162
|
+
if (value === "~" || value.startsWith("~/")) return join2(homedir2(), value.slice(1));
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
function loadYamlFile(path, schema) {
|
|
166
|
+
let raw;
|
|
167
|
+
try {
|
|
168
|
+
raw = readFileSync(path, "utf8");
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const parsed = schema.safeParse(parse(raw) ?? {});
|
|
173
|
+
if (!parsed.success) {
|
|
174
|
+
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
175
|
+
throw new Error(`invalid config: ${path}
|
|
176
|
+
${issues}`);
|
|
177
|
+
}
|
|
178
|
+
return parsed.data;
|
|
179
|
+
}
|
|
180
|
+
function globalConfigPath() {
|
|
181
|
+
return join2(kmdHome(), "config.yaml");
|
|
182
|
+
}
|
|
183
|
+
function loadGlobalConfig(env = process.env) {
|
|
184
|
+
const config = loadYamlFile(globalConfigPath(), GlobalConfigSchema);
|
|
185
|
+
if (config === null || config.default_vault === void 0) return {};
|
|
186
|
+
const expanded = expandHome(expandVars(config.default_vault, env));
|
|
187
|
+
if (!isAbsolute(expanded)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`invalid config: ${globalConfigPath()}
|
|
190
|
+
- default_vault: must be absolute or ~/ (got "${config.default_vault}")`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return { default_vault: expanded };
|
|
194
|
+
}
|
|
195
|
+
function loadGlobalDocument() {
|
|
196
|
+
try {
|
|
197
|
+
return parseDocument(readFileSync(globalConfigPath(), "utf8"));
|
|
198
|
+
} catch {
|
|
199
|
+
return new Document({});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function flushGlobalDocument(doc) {
|
|
203
|
+
mkdirSync(kmdHome(), { recursive: true });
|
|
204
|
+
writeFileSync(globalConfigPath(), doc.toString());
|
|
205
|
+
}
|
|
206
|
+
function setGlobalConfigValue(key, value) {
|
|
207
|
+
const doc = loadGlobalDocument();
|
|
208
|
+
doc.set(key, value);
|
|
209
|
+
flushGlobalDocument(doc);
|
|
210
|
+
}
|
|
211
|
+
function unsetGlobalConfigValue(key) {
|
|
212
|
+
const doc = loadGlobalDocument();
|
|
213
|
+
if (!doc.has(key)) return false;
|
|
214
|
+
doc.delete(key);
|
|
215
|
+
flushGlobalDocument(doc);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
function projectVaultAt(level, env, onSkip) {
|
|
219
|
+
for (const [file, via] of TIER_CONFIG_FILES) {
|
|
220
|
+
const path = join2(level, ".kmd", file);
|
|
221
|
+
const config = loadYamlFile(path, ProjectConfigSchema);
|
|
222
|
+
if (config === null || config.vault === void 0) continue;
|
|
223
|
+
const expanded = expandHome(expandVars(config.vault, env));
|
|
224
|
+
return {
|
|
225
|
+
tierRoot: level,
|
|
226
|
+
vaultRoot: isAbsolute(expanded) ? expanded : resolve2(level, expanded),
|
|
227
|
+
via
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (existsSync2(join2(level, "vault", "vault.yaml"))) {
|
|
231
|
+
return { tierRoot: level, vaultRoot: join2(level, "vault"), via: "convention" };
|
|
232
|
+
}
|
|
233
|
+
if (existsSync2(join2(level, "vault.yaml"))) {
|
|
234
|
+
if (existsSync2(join2(level, ".kmd"))) {
|
|
235
|
+
return { tierRoot: level, vaultRoot: level, via: "convention" };
|
|
236
|
+
}
|
|
237
|
+
onSkip?.(join2(level, "vault.yaml"));
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
function findProjectTier(fromDir, env = process.env, onSkip) {
|
|
242
|
+
let level = canonicalVaultRoot(fromDir);
|
|
243
|
+
for (; ; ) {
|
|
244
|
+
const hit = projectVaultAt(level, env, onSkip);
|
|
245
|
+
if (hit !== null) return hit;
|
|
246
|
+
const parent = dirname2(level);
|
|
247
|
+
if (parent === level) return null;
|
|
248
|
+
level = parent;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function resolveVaultRoot(input) {
|
|
252
|
+
if (input.positional) return { root: input.positional, source: "positional" };
|
|
253
|
+
if (input.projectDir) {
|
|
254
|
+
const tier = findProjectTier(input.projectDir, input.env ?? process.env, input.onSkip);
|
|
255
|
+
if (tier !== null) {
|
|
256
|
+
return { root: tier.vaultRoot, source: `project-${tier.via}`, tierRoot: tier.tierRoot };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (input.defaultRoot) return { root: input.defaultRoot, source: "default-root" };
|
|
260
|
+
if (input.envVault) return { root: input.envVault, source: "env" };
|
|
261
|
+
if (input.globalDefault) return { root: input.globalDefault, source: "global-config" };
|
|
262
|
+
return { root: null, source: "none" };
|
|
263
|
+
}
|
|
264
|
+
var GlobalConfigSchema, ProjectConfigSchema, TIER_CONFIG_FILES;
|
|
265
|
+
var init_kmd_config = __esm({
|
|
266
|
+
"../db/src/kmd-config.ts"() {
|
|
267
|
+
"use strict";
|
|
268
|
+
init_database();
|
|
269
|
+
GlobalConfigSchema = z.strictObject({
|
|
270
|
+
default_vault: z.string().min(1).optional()
|
|
271
|
+
});
|
|
272
|
+
ProjectConfigSchema = z.strictObject({
|
|
273
|
+
vault: z.string().min(1).optional()
|
|
274
|
+
});
|
|
275
|
+
TIER_CONFIG_FILES = [
|
|
276
|
+
["config.local.yaml", "local-config"],
|
|
277
|
+
["config.yaml", "config"]
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
119
282
|
// ../db/src/vault-config.ts
|
|
120
283
|
import { readFile } from "node:fs/promises";
|
|
121
|
-
import { join as
|
|
122
|
-
import { parse } from "yaml";
|
|
123
|
-
import { z } from "zod";
|
|
284
|
+
import { join as join3 } from "node:path";
|
|
285
|
+
import { parse as parse2 } from "yaml";
|
|
286
|
+
import { z as z2 } from "zod";
|
|
124
287
|
function isValidRegex(pattern) {
|
|
125
288
|
try {
|
|
126
289
|
return Boolean(new RegExp(pattern));
|
|
@@ -132,17 +295,17 @@ function kindName(entry) {
|
|
|
132
295
|
return typeof entry === "string" ? entry : entry.name;
|
|
133
296
|
}
|
|
134
297
|
function configJsonSchema() {
|
|
135
|
-
return
|
|
298
|
+
return z2.toJSONSchema(VaultConfigSchema, { target: "draft-7" });
|
|
136
299
|
}
|
|
137
|
-
async function loadVaultConfig(
|
|
138
|
-
const path =
|
|
300
|
+
async function loadVaultConfig(vaultRoot) {
|
|
301
|
+
const path = join3(vaultRoot, "vault.yaml");
|
|
139
302
|
let raw;
|
|
140
303
|
try {
|
|
141
304
|
raw = await readFile(path, "utf8");
|
|
142
305
|
} catch (err) {
|
|
143
306
|
throw new Error(`vault.yaml not found at ${path}`, { cause: err });
|
|
144
307
|
}
|
|
145
|
-
const parsed = VaultConfigSchema.safeParse(
|
|
308
|
+
const parsed = VaultConfigSchema.safeParse(parse2(raw));
|
|
146
309
|
if (!parsed.success) {
|
|
147
310
|
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
148
311
|
throw new Error(`Invalid vault.yaml at ${path}:
|
|
@@ -154,47 +317,47 @@ var ScopeSchema, KindEntrySchema, WhenSchema, DedupSchema, TriggerSchema, Trigge
|
|
|
154
317
|
var init_vault_config = __esm({
|
|
155
318
|
"../db/src/vault-config.ts"() {
|
|
156
319
|
"use strict";
|
|
157
|
-
ScopeSchema =
|
|
158
|
-
repo:
|
|
320
|
+
ScopeSchema = z2.strictObject({
|
|
321
|
+
repo: z2.string().optional().describe(
|
|
159
322
|
"Consumer repo path (~ expands). Load-bearing for kmd hook: the active scope resolves by matching the session cwd against it."
|
|
160
323
|
),
|
|
161
|
-
methodology:
|
|
162
|
-
status:
|
|
324
|
+
methodology: z2.string().optional().describe("Must appear in the methodologies list."),
|
|
325
|
+
status: z2.string().describe("Free string; keep within statuses by convention.")
|
|
163
326
|
});
|
|
164
|
-
KindEntrySchema =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
name:
|
|
168
|
-
signal:
|
|
169
|
-
where:
|
|
327
|
+
KindEntrySchema = z2.union([
|
|
328
|
+
z2.string(),
|
|
329
|
+
z2.strictObject({
|
|
330
|
+
name: z2.string(),
|
|
331
|
+
signal: z2.string().describe("When to pick this kind."),
|
|
332
|
+
where: z2.string().describe("Path pattern pages of this kind follow.")
|
|
170
333
|
})
|
|
171
334
|
]);
|
|
172
|
-
WhenSchema =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
name:
|
|
176
|
-
fresh:
|
|
177
|
-
than:
|
|
335
|
+
WhenSchema = z2.union([
|
|
336
|
+
z2.string(),
|
|
337
|
+
z2.strictObject({
|
|
338
|
+
name: z2.enum(["newer-than"]),
|
|
339
|
+
fresh: z2.array(z2.string().min(1)).min(1),
|
|
340
|
+
than: z2.array(z2.string().min(1)).min(1)
|
|
178
341
|
})
|
|
179
342
|
]);
|
|
180
|
-
DedupSchema =
|
|
181
|
-
|
|
182
|
-
|
|
343
|
+
DedupSchema = z2.union([
|
|
344
|
+
z2.enum(["session", "never"]),
|
|
345
|
+
z2.strictObject({ minutes: z2.number().int().positive() })
|
|
183
346
|
]);
|
|
184
|
-
TriggerSchema =
|
|
185
|
-
id:
|
|
186
|
-
on:
|
|
187
|
-
enforce:
|
|
188
|
-
keywords:
|
|
189
|
-
intent:
|
|
190
|
-
tool:
|
|
191
|
-
args_match:
|
|
192
|
-
files:
|
|
347
|
+
TriggerSchema = z2.strictObject({
|
|
348
|
+
id: z2.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
|
|
349
|
+
on: z2.enum(["prompt", "pretool"]),
|
|
350
|
+
enforce: z2.enum(["inject", "warn", "block"]).describe("inject: context line \xB7 warn: stderr \xB7 block: deny with reason."),
|
|
351
|
+
keywords: z2.array(z2.string().min(1)).optional().describe("Word-boundary, porter-stemmed match. Prompt triggers need keywords or intent."),
|
|
352
|
+
intent: z2.array(z2.string()).optional().describe("Case-insensitive regexes over the raw prompt \u2014 the stemming escape hatch."),
|
|
353
|
+
tool: z2.string().optional().describe("Exact tool name; pretool matchers AND-compose."),
|
|
354
|
+
args_match: z2.string().optional().describe("Regex over the serialized tool input."),
|
|
355
|
+
files: z2.array(z2.string().min(1)).optional().describe("Globs against the paths the tool touches; pretool triggers only."),
|
|
193
356
|
when: WhenSchema.optional().describe(
|
|
194
357
|
"Precondition \u2014 the gate fires only when it is UNMET. newer-than: the newest page matching fresh must carry frontmatter updated at or after the newest matching than."
|
|
195
358
|
),
|
|
196
|
-
text:
|
|
197
|
-
reason:
|
|
359
|
+
text: z2.string().optional().describe("Required for inject and warn \u2014 the line emitted."),
|
|
360
|
+
reason: z2.string().optional().describe("Required for block \u2014 the denial the agent reads."),
|
|
198
361
|
dedup: DedupSchema.optional().describe(
|
|
199
362
|
"Re-fire policy: session (default, once per session), never, or {minutes: N} for at most once per bucket. Rejected on block triggers \u2014 blocks are dedup-exempt."
|
|
200
363
|
)
|
|
@@ -240,37 +403,37 @@ var init_vault_config = __esm({
|
|
|
240
403
|
}
|
|
241
404
|
}
|
|
242
405
|
});
|
|
243
|
-
TriggersSchema =
|
|
244
|
-
BuiltinHooksSchema =
|
|
245
|
-
resync:
|
|
246
|
-
reason:
|
|
247
|
-
text:
|
|
406
|
+
TriggersSchema = z2.record(z2.string(), z2.array(TriggerSchema));
|
|
407
|
+
BuiltinHooksSchema = z2.strictObject({
|
|
408
|
+
resync: z2.strictObject({
|
|
409
|
+
reason: z2.string().min(1).optional().describe("Validate-errors preamble; the engine appends the error lines."),
|
|
410
|
+
text: z2.string().min(1).optional().describe("Sync-failed note.")
|
|
248
411
|
}).optional(),
|
|
249
|
-
"handoff-gate":
|
|
250
|
-
reason:
|
|
412
|
+
"handoff-gate": z2.strictObject({
|
|
413
|
+
reason: z2.string().min(1).optional().describe("Stop-block preamble; the engine appends the error lines.")
|
|
251
414
|
}).optional(),
|
|
252
|
-
orient:
|
|
253
|
-
text:
|
|
415
|
+
orient: z2.strictObject({
|
|
416
|
+
text: z2.string().min(1).optional().describe("Session-start prime instruction; the engine prepends the resolved scope.")
|
|
254
417
|
}).optional(),
|
|
255
|
-
reorient:
|
|
256
|
-
text:
|
|
418
|
+
reorient: z2.strictObject({
|
|
419
|
+
text: z2.string().min(1).optional().describe("Post-compaction re-orientation; the engine prepends the resolved scope.")
|
|
257
420
|
}).optional()
|
|
258
421
|
});
|
|
259
|
-
VaultConfigSchema =
|
|
260
|
-
scopes:
|
|
261
|
-
kinds:
|
|
422
|
+
VaultConfigSchema = z2.strictObject({
|
|
423
|
+
scopes: z2.record(z2.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
|
|
424
|
+
kinds: z2.array(KindEntrySchema).describe(
|
|
262
425
|
"Page kind vocabulary; validate-enforced. Object form adds a kind-selector row to wiki://authoring."
|
|
263
426
|
),
|
|
264
|
-
statuses:
|
|
265
|
-
methodologies:
|
|
266
|
-
tags:
|
|
267
|
-
canonical:
|
|
268
|
-
aliases:
|
|
427
|
+
statuses: z2.array(z2.string()).describe("Page status vocabulary; validate-enforced."),
|
|
428
|
+
methodologies: z2.array(z2.string()).describe("Methodology vocabulary for pages and scope entries."),
|
|
429
|
+
tags: z2.strictObject({
|
|
430
|
+
canonical: z2.array(z2.string()).describe("Approved tags."),
|
|
431
|
+
aliases: z2.record(z2.string(), z2.string()).describe("Alias \u2192 canonical; validate warns on alias use.")
|
|
269
432
|
}),
|
|
270
|
-
authoring_rules:
|
|
271
|
-
authoring_rules_extra:
|
|
272
|
-
sync_protocol:
|
|
273
|
-
sync_protocol_extra:
|
|
433
|
+
authoring_rules: z2.string().optional().describe("Replaces the served \xA7 Authoring rules entirely \u2014 escape hatch."),
|
|
434
|
+
authoring_rules_extra: z2.string().optional().describe("Appended after the served \xA7 Authoring rules."),
|
|
435
|
+
sync_protocol: z2.string().optional().describe("Replaces the served \xA7 Resync protocol entirely \u2014 escape hatch."),
|
|
436
|
+
sync_protocol_extra: z2.string().optional().describe("Appended after the served \xA7 Resync protocol."),
|
|
274
437
|
triggers: TriggersSchema.optional().describe(
|
|
275
438
|
'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
|
|
276
439
|
),
|
|
@@ -521,11 +684,12 @@ and must match current code at every commit.
|
|
|
521
684
|
});
|
|
522
685
|
|
|
523
686
|
// ../cli/src/init.ts
|
|
687
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
524
688
|
import { mkdir, readdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
525
|
-
import { join as
|
|
689
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
|
|
526
690
|
import { stringify } from "yaml";
|
|
527
691
|
async function refreshSchemaFile(root) {
|
|
528
|
-
const path =
|
|
692
|
+
const path = join4(root, SCHEMA_FILE);
|
|
529
693
|
const next = `${JSON.stringify(configJsonSchema(), null, 2)}
|
|
530
694
|
`;
|
|
531
695
|
try {
|
|
@@ -536,7 +700,7 @@ async function refreshSchemaFile(root) {
|
|
|
536
700
|
return true;
|
|
537
701
|
}
|
|
538
702
|
async function scaffoldVault(dir) {
|
|
539
|
-
const root =
|
|
703
|
+
const root = resolve3(dir);
|
|
540
704
|
let entries = [];
|
|
541
705
|
try {
|
|
542
706
|
entries = await readdir(root);
|
|
@@ -554,33 +718,97 @@ async function scaffoldVault(dir) {
|
|
|
554
718
|
);
|
|
555
719
|
}
|
|
556
720
|
for (const domain of DOMAIN_DIRS) {
|
|
557
|
-
await mkdir(
|
|
721
|
+
await mkdir(join4(root, domain), { recursive: true });
|
|
558
722
|
}
|
|
559
|
-
await mkdir(
|
|
723
|
+
await mkdir(join4(root, "templates"), { recursive: true });
|
|
560
724
|
for (const [file, content] of Object.entries(VAULT_TEMPLATES)) {
|
|
561
|
-
await writeFile(
|
|
725
|
+
await writeFile(join4(root, "templates", file), content);
|
|
562
726
|
}
|
|
563
727
|
await refreshSchemaFile(root);
|
|
564
|
-
await writeFile(
|
|
728
|
+
await writeFile(join4(root, "vault.yaml"), SCHEMA_MODELINE + stringify(STARTER_CONFIG));
|
|
565
729
|
return root;
|
|
566
730
|
}
|
|
567
|
-
async function promptYesNo(question, input = process.stdin, output = process.stderr) {
|
|
731
|
+
async function promptYesNo(question, input = process.stdin, output = process.stderr, defaultYes = false) {
|
|
568
732
|
const { createInterface } = await import("node:readline/promises");
|
|
569
733
|
const rl = createInterface({ input, output });
|
|
570
734
|
try {
|
|
571
|
-
const answer = await rl.question(question);
|
|
572
|
-
|
|
735
|
+
const answer = (await rl.question(question)).trim();
|
|
736
|
+
if (answer === "") return defaultYes;
|
|
737
|
+
return /^y(es)?$/i.test(answer);
|
|
573
738
|
} finally {
|
|
574
739
|
rl.close();
|
|
575
740
|
}
|
|
576
741
|
}
|
|
577
|
-
|
|
742
|
+
function findGitRoot(from) {
|
|
743
|
+
let level = resolve3(from);
|
|
744
|
+
for (; ; ) {
|
|
745
|
+
if (existsSync3(join4(level, ".git"))) return level;
|
|
746
|
+
const parent = dirname3(level);
|
|
747
|
+
if (parent === level) return null;
|
|
748
|
+
level = parent;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function runInitLocal(yes) {
|
|
752
|
+
const root = findGitRoot(process.cwd()) ?? process.cwd();
|
|
753
|
+
const vaultTarget = join4(root, "vault");
|
|
754
|
+
const kmdDir = join4(root, ".kmd");
|
|
755
|
+
if (!yes) {
|
|
756
|
+
if (process.stdin.isTTY) {
|
|
757
|
+
const ok = await promptYesNo(
|
|
758
|
+
`initialize project vault at ${vaultTarget} (state in ${kmdDir})? [Y/n] `,
|
|
759
|
+
process.stdin,
|
|
760
|
+
process.stderr,
|
|
761
|
+
true
|
|
762
|
+
);
|
|
763
|
+
if (!ok) {
|
|
764
|
+
console.error("init: aborted");
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
console.error("usage: kmd init --local -y (piped stdin cannot confirm the target)");
|
|
769
|
+
process.exit(2);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
let vaultRoot;
|
|
773
|
+
try {
|
|
774
|
+
vaultRoot = await scaffoldVault(vaultTarget);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
console.error(`init: ${err instanceof Error ? err.message : err}`);
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
await mkdir(kmdDir, { recursive: true });
|
|
780
|
+
const gitignore = join4(kmdDir, ".gitignore");
|
|
781
|
+
if (!existsSync3(gitignore)) await writeFile(gitignore, TIER_GITIGNORE);
|
|
782
|
+
console.log(`initialized project vault at ${vaultRoot}
|
|
783
|
+
|
|
784
|
+
${kmdDir}/ state home \u2014 index and hook state live with the repo
|
|
785
|
+
${kmdDir}/.gitignore db/, state/, config.local.yaml stay untracked
|
|
786
|
+
|
|
787
|
+
every kmd command run inside ${root} now resolves this vault (project tier).
|
|
788
|
+
next steps:
|
|
789
|
+
kmd sync # builds the index at .kmd/db/index.db`);
|
|
790
|
+
}
|
|
791
|
+
async function runInit(dir, yes = false, local = false, setDefaultFlag = false) {
|
|
792
|
+
if (local) {
|
|
793
|
+
if (dir !== void 0) {
|
|
794
|
+
console.error("usage: kmd init --local (the root is the nearest .git ancestor, not chosen)");
|
|
795
|
+
process.exit(2);
|
|
796
|
+
}
|
|
797
|
+
if (setDefaultFlag) {
|
|
798
|
+
console.error(
|
|
799
|
+
"kmd init: --set-default applies to global init only (a project vault resolves by location)"
|
|
800
|
+
);
|
|
801
|
+
process.exit(2);
|
|
802
|
+
}
|
|
803
|
+
await runInitLocal(yes);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
578
806
|
let target = dir;
|
|
579
807
|
if (!target) {
|
|
580
808
|
if (yes) {
|
|
581
809
|
target = ".";
|
|
582
810
|
} else if (process.stdin.isTTY) {
|
|
583
|
-
const ok = await promptYesNo(`initialize a vault in ${
|
|
811
|
+
const ok = await promptYesNo(`initialize a vault in ${resolve3(".")}? [y/N] `);
|
|
584
812
|
if (!ok) {
|
|
585
813
|
console.error("init: aborted");
|
|
586
814
|
process.exit(1);
|
|
@@ -604,16 +832,34 @@ async function runInit(dir, yes = false) {
|
|
|
604
832
|
vault.yaml starter vocabulary \u2014 add your first scope under scopes:
|
|
605
833
|
vault.schema.json IDE validation via the yaml-language-server modeline
|
|
606
834
|
templates/ ${templateCount} built-in templates (served at wiki://template/...)
|
|
607
|
-
projects/ research/ notes
|
|
608
|
-
|
|
835
|
+
projects/ research/ notes/`);
|
|
836
|
+
let setDefault = setDefaultFlag;
|
|
837
|
+
if (!setDefault && !yes && process.stdin.isTTY) {
|
|
838
|
+
setDefault = await promptYesNo(
|
|
839
|
+
`set as default vault in ~/.kmd/config.yaml? [Y/n] `,
|
|
840
|
+
process.stdin,
|
|
841
|
+
process.stderr,
|
|
842
|
+
true
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (setDefault) {
|
|
846
|
+
setGlobalConfigValue("default_vault", root);
|
|
847
|
+
console.log(`
|
|
848
|
+
default_vault: ${root}
|
|
849
|
+
next steps:
|
|
850
|
+
kmd mcp # stdio MCP server (prime, search) \u2014 resolves the default`);
|
|
851
|
+
} else {
|
|
852
|
+
console.log(`
|
|
609
853
|
next steps:
|
|
610
|
-
|
|
854
|
+
kmd config set default_vault ${root} # make it the machine default
|
|
611
855
|
kmd mcp ${root} # stdio MCP server (prime, search)`);
|
|
856
|
+
}
|
|
612
857
|
}
|
|
613
|
-
var SCHEMA_FILE, SCHEMA_MODELINE, STARTER_CONFIG, DOMAIN_DIRS;
|
|
858
|
+
var SCHEMA_FILE, SCHEMA_MODELINE, STARTER_CONFIG, DOMAIN_DIRS, TIER_GITIGNORE;
|
|
614
859
|
var init_init = __esm({
|
|
615
860
|
"../cli/src/init.ts"() {
|
|
616
861
|
"use strict";
|
|
862
|
+
init_kmd_config();
|
|
617
863
|
init_vault_config();
|
|
618
864
|
init_init_templates();
|
|
619
865
|
SCHEMA_FILE = "vault.schema.json";
|
|
@@ -627,15 +873,16 @@ var init_init = __esm({
|
|
|
627
873
|
tags: { canonical: [], aliases: {} }
|
|
628
874
|
};
|
|
629
875
|
DOMAIN_DIRS = ["projects", "research", "notes"];
|
|
876
|
+
TIER_GITIGNORE = "db/\nstate/\nconfig.local.yaml\n";
|
|
630
877
|
}
|
|
631
878
|
});
|
|
632
879
|
|
|
633
880
|
// ../cli/src/sync.ts
|
|
634
881
|
import { createHash as createHash2 } from "node:crypto";
|
|
635
|
-
import { mkdirSync } from "node:fs";
|
|
882
|
+
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
636
883
|
import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
|
|
637
|
-
import { dirname, join as
|
|
638
|
-
import { z as
|
|
884
|
+
import { dirname as dirname4, join as join5, relative, sep } from "node:path";
|
|
885
|
+
import { z as z3 } from "zod";
|
|
639
886
|
function loadEnv() {
|
|
640
887
|
const parsed = EnvSchema.safeParse({
|
|
641
888
|
WIKI_VAULT: process.env.WIKI_VAULT
|
|
@@ -655,13 +902,13 @@ async function walkMarkdown(root, domain) {
|
|
|
655
902
|
for (const entry of entries) {
|
|
656
903
|
if (entry.name.startsWith(".")) continue;
|
|
657
904
|
if (entry.isDirectory()) {
|
|
658
|
-
await recurse(
|
|
905
|
+
await recurse(join5(dir, entry.name));
|
|
659
906
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
660
|
-
out.push(
|
|
907
|
+
out.push(join5(dir, entry.name));
|
|
661
908
|
}
|
|
662
909
|
}
|
|
663
910
|
}
|
|
664
|
-
await recurse(
|
|
911
|
+
await recurse(join5(root, domain));
|
|
665
912
|
return out;
|
|
666
913
|
}
|
|
667
914
|
function toRelativePath(root, absolute) {
|
|
@@ -783,23 +1030,23 @@ function syncPage(db, fields) {
|
|
|
783
1030
|
}
|
|
784
1031
|
return "changed";
|
|
785
1032
|
}
|
|
786
|
-
async function syncVault(
|
|
787
|
-
const dbPath = resolveIndexPath(
|
|
788
|
-
const vaultConfig = await loadVaultConfig(
|
|
1033
|
+
async function syncVault(vaultRoot) {
|
|
1034
|
+
const dbPath = resolveIndexPath(vaultRoot);
|
|
1035
|
+
const vaultConfig = await loadVaultConfig(vaultRoot);
|
|
789
1036
|
const scopes = new Set(Object.keys(vaultConfig.scopes));
|
|
790
|
-
|
|
1037
|
+
mkdirSync2(dirname4(dbPath), { recursive: true });
|
|
791
1038
|
const db = openDatabase(dbPath);
|
|
792
1039
|
try {
|
|
793
1040
|
const files = [];
|
|
794
1041
|
for (const domain of SCAN_DOMAINS) {
|
|
795
|
-
files.push(...await walkMarkdown(
|
|
1042
|
+
files.push(...await walkMarkdown(vaultRoot, domain));
|
|
796
1043
|
}
|
|
797
1044
|
const indexedPaths = [];
|
|
798
1045
|
let changed = 0;
|
|
799
1046
|
let unchanged = 0;
|
|
800
1047
|
let skipped = 0;
|
|
801
1048
|
for (const file of files) {
|
|
802
|
-
const path = toRelativePath(
|
|
1049
|
+
const path = toRelativePath(vaultRoot, file);
|
|
803
1050
|
const raw = await readFile3(file, "utf8");
|
|
804
1051
|
const parsed = parseFrontmatter(raw);
|
|
805
1052
|
const fields = buildPageFields(path, raw, parsed, scopes);
|
|
@@ -821,9 +1068,9 @@ async function syncVault(vaultRoot2) {
|
|
|
821
1068
|
const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
|
|
822
1069
|
const linksDeleted = Number(linkResult.changes);
|
|
823
1070
|
db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
|
|
824
|
-
setMeta(db, "vault_root", canonicalVaultRoot(
|
|
1071
|
+
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot));
|
|
825
1072
|
setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
|
|
826
|
-
if (await refreshSchemaFile(
|
|
1073
|
+
if (await refreshSchemaFile(vaultRoot)) {
|
|
827
1074
|
console.error("sync: vault.schema.json refreshed to the running engine");
|
|
828
1075
|
}
|
|
829
1076
|
return {
|
|
@@ -857,8 +1104,8 @@ var init_sync = __esm({
|
|
|
857
1104
|
init_vault_config();
|
|
858
1105
|
init_frontmatter();
|
|
859
1106
|
init_init();
|
|
860
|
-
EnvSchema =
|
|
861
|
-
WIKI_VAULT:
|
|
1107
|
+
EnvSchema = z3.object({
|
|
1108
|
+
WIKI_VAULT: z3.string().min(1)
|
|
862
1109
|
});
|
|
863
1110
|
SCAN_DOMAINS = ["projects", "research", "notes"];
|
|
864
1111
|
WIKILINK_RE = /\[\[([^\]|#^]+)(?:[#^][^\]|]*)?(?:\|([^\]]+))?\]\]/g;
|
|
@@ -877,7 +1124,7 @@ var init_sync = __esm({
|
|
|
877
1124
|
|
|
878
1125
|
// ../cli/src/validate.ts
|
|
879
1126
|
import { readFile as readFile4, stat } from "node:fs/promises";
|
|
880
|
-
import { join as
|
|
1127
|
+
import { join as join6 } from "node:path";
|
|
881
1128
|
function hasIndexableTitle(data) {
|
|
882
1129
|
return typeof data.title === "string" && data.title.trim() !== "";
|
|
883
1130
|
}
|
|
@@ -1249,7 +1496,7 @@ async function validateVault(root) {
|
|
|
1249
1496
|
for (const name of customKindNames(cfg)) {
|
|
1250
1497
|
const file = `templates/${name}.md`;
|
|
1251
1498
|
try {
|
|
1252
|
-
await stat(
|
|
1499
|
+
await stat(join6(root, file));
|
|
1253
1500
|
} catch {
|
|
1254
1501
|
findings.push({
|
|
1255
1502
|
path: file,
|
|
@@ -1321,15 +1568,18 @@ var cli_exports = {};
|
|
|
1321
1568
|
__export(cli_exports, {
|
|
1322
1569
|
main: () => main,
|
|
1323
1570
|
resolveCli: () => resolveCli,
|
|
1571
|
+
resolveCliVault: () => resolveCliVault,
|
|
1324
1572
|
runConfig: () => runConfig,
|
|
1573
|
+
runConfigGet: () => runConfigGet,
|
|
1574
|
+
runConfigSet: () => runConfigSet,
|
|
1575
|
+
runConfigUnset: () => runConfigUnset,
|
|
1325
1576
|
runDbReset: () => runDbReset,
|
|
1326
1577
|
runInit: () => runInit,
|
|
1327
1578
|
runSyncCommand: () => runSyncCommand,
|
|
1328
|
-
runValidate: () => runValidate
|
|
1329
|
-
vaultRoot: () => vaultRoot
|
|
1579
|
+
runValidate: () => runValidate
|
|
1330
1580
|
});
|
|
1331
|
-
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
1332
|
-
import { dirname as
|
|
1581
|
+
import { existsSync as existsSync4, readdirSync, rmSync } from "node:fs";
|
|
1582
|
+
import { dirname as dirname5, join as join7, resolve as resolve4 } from "node:path";
|
|
1333
1583
|
import { parseArgs } from "node:util";
|
|
1334
1584
|
function resolveCli(argv) {
|
|
1335
1585
|
const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
|
|
@@ -1345,27 +1595,42 @@ function resolveCli(argv) {
|
|
|
1345
1595
|
}
|
|
1346
1596
|
return { kind: "error", message: `unknown command: ${command2}` };
|
|
1347
1597
|
}
|
|
1348
|
-
function
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1598
|
+
function resolveCliVault(positional, defaultRoot) {
|
|
1599
|
+
return resolveVaultRoot({
|
|
1600
|
+
positional,
|
|
1601
|
+
defaultRoot,
|
|
1602
|
+
projectDir: process.env.KMD_PROJECT_DIR ?? process.cwd(),
|
|
1603
|
+
envVault: process.env.WIKI_VAULT,
|
|
1604
|
+
globalDefault: loadGlobalConfig().default_vault,
|
|
1605
|
+
onSkip: (candidate) => console.error(
|
|
1606
|
+
`kmd: ignoring ${candidate} \u2014 no .kmd marker beside it; mkdir ${dirname5(candidate)}/.kmd to bind it as the project vault`
|
|
1607
|
+
)
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
function requireCliVault(positional) {
|
|
1611
|
+
const resolution = resolveCliVault(positional);
|
|
1612
|
+
if (resolution.root === null) {
|
|
1613
|
+
console.error(NO_VAULT_HINT);
|
|
1352
1614
|
process.exit(1);
|
|
1353
1615
|
}
|
|
1354
|
-
|
|
1616
|
+
process.env.WIKI_VAULT = resolution.root;
|
|
1617
|
+
return { root: resolution.root, resolution };
|
|
1355
1618
|
}
|
|
1356
1619
|
function reportFindings(findings) {
|
|
1357
1620
|
for (const f of findings) {
|
|
1358
1621
|
console.error(`${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
1359
1622
|
}
|
|
1360
1623
|
}
|
|
1361
|
-
async function runValidate() {
|
|
1362
|
-
const
|
|
1624
|
+
async function runValidate(positional) {
|
|
1625
|
+
const { root } = requireCliVault(positional);
|
|
1626
|
+
const findings = await validateVault(root);
|
|
1363
1627
|
reportFindings(findings);
|
|
1364
1628
|
console.log(`validate: ${findings.length} finding(s)`);
|
|
1365
1629
|
process.exit(hasErrors(findings) ? 1 : 0);
|
|
1366
1630
|
}
|
|
1367
|
-
async function runSyncCommand() {
|
|
1368
|
-
const
|
|
1631
|
+
async function runSyncCommand(positional) {
|
|
1632
|
+
const { root } = requireCliVault(positional);
|
|
1633
|
+
const findings = await validateVault(root);
|
|
1369
1634
|
reportFindings(findings);
|
|
1370
1635
|
if (hasErrors(findings)) {
|
|
1371
1636
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
@@ -1378,7 +1643,7 @@ function describeVault(root) {
|
|
|
1378
1643
|
const vault = canonicalVaultRoot(root);
|
|
1379
1644
|
const index = resolveIndexPath(vault);
|
|
1380
1645
|
let synced = "never";
|
|
1381
|
-
if (
|
|
1646
|
+
if (existsSync4(index)) {
|
|
1382
1647
|
const db = openDatabase(index);
|
|
1383
1648
|
try {
|
|
1384
1649
|
synced = getMeta(db, "last_synced") ?? "never";
|
|
@@ -1395,12 +1660,12 @@ function printVault(d) {
|
|
|
1395
1660
|
}
|
|
1396
1661
|
function knownVaults() {
|
|
1397
1662
|
const root = indexRootDir();
|
|
1398
|
-
if (!
|
|
1663
|
+
if (!existsSync4(root)) return [];
|
|
1399
1664
|
const known = [];
|
|
1400
1665
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
1401
1666
|
if (!entry.isDirectory()) continue;
|
|
1402
|
-
const index =
|
|
1403
|
-
if (!
|
|
1667
|
+
const index = join7(root, entry.name, "index.db");
|
|
1668
|
+
if (!existsSync4(index)) continue;
|
|
1404
1669
|
const db = openDatabase(index);
|
|
1405
1670
|
try {
|
|
1406
1671
|
const vault = getMeta(db, "vault_root");
|
|
@@ -1412,17 +1677,16 @@ function knownVaults() {
|
|
|
1412
1677
|
}
|
|
1413
1678
|
return known;
|
|
1414
1679
|
}
|
|
1415
|
-
async function runConfig() {
|
|
1416
|
-
const
|
|
1417
|
-
if (root) {
|
|
1418
|
-
printVault(describeVault(root));
|
|
1680
|
+
async function runConfig(positional) {
|
|
1681
|
+
const resolution = resolveCliVault(positional);
|
|
1682
|
+
if (resolution.root !== null) {
|
|
1683
|
+
printVault(describeVault(resolution.root));
|
|
1684
|
+
console.log(`source: ${SOURCE_LABELS[resolution.source]}`);
|
|
1419
1685
|
return;
|
|
1420
1686
|
}
|
|
1421
1687
|
const known = knownVaults();
|
|
1422
1688
|
if (known.length === 0) {
|
|
1423
|
-
console.error(
|
|
1424
|
-
"no vault specified and none known \u2014 pass a vault root, set WIKI_VAULT, or run `kmd sync <vault-root>` once"
|
|
1425
|
-
);
|
|
1689
|
+
console.error(NO_VAULT_HINT);
|
|
1426
1690
|
process.exit(1);
|
|
1427
1691
|
}
|
|
1428
1692
|
known.forEach((d, i) => {
|
|
@@ -1430,14 +1694,44 @@ async function runConfig() {
|
|
|
1430
1694
|
printVault(d);
|
|
1431
1695
|
});
|
|
1432
1696
|
}
|
|
1433
|
-
async function
|
|
1434
|
-
|
|
1435
|
-
|
|
1697
|
+
async function runConfigSet(key, value) {
|
|
1698
|
+
if (key !== "default_vault" || !value) {
|
|
1699
|
+
console.error("usage: kmd config set default_vault <path>");
|
|
1700
|
+
process.exit(2);
|
|
1701
|
+
}
|
|
1702
|
+
const vault = resolve4(value);
|
|
1703
|
+
await loadVaultConfig(vault);
|
|
1704
|
+
setGlobalConfigValue("default_vault", vault);
|
|
1705
|
+
console.log(`default_vault: ${vault}`);
|
|
1706
|
+
}
|
|
1707
|
+
async function runConfigGet(key) {
|
|
1708
|
+
if (key !== "default_vault") {
|
|
1709
|
+
console.error("usage: kmd config get default_vault");
|
|
1710
|
+
process.exit(2);
|
|
1711
|
+
}
|
|
1712
|
+
const value = loadGlobalConfig().default_vault;
|
|
1713
|
+
if (value === void 0) process.exit(1);
|
|
1714
|
+
console.log(value);
|
|
1715
|
+
}
|
|
1716
|
+
async function runConfigUnset(key) {
|
|
1717
|
+
if (key !== "default_vault") {
|
|
1718
|
+
console.error("usage: kmd config unset default_vault");
|
|
1719
|
+
process.exit(2);
|
|
1720
|
+
}
|
|
1721
|
+
if (!unsetGlobalConfigValue("default_vault")) {
|
|
1722
|
+
console.error("default_vault is not set");
|
|
1723
|
+
process.exit(1);
|
|
1724
|
+
}
|
|
1725
|
+
console.log("default_vault unset");
|
|
1726
|
+
}
|
|
1727
|
+
async function runDbReset(positional) {
|
|
1728
|
+
const resolution = resolveCliVault(positional);
|
|
1729
|
+
if (resolution.root === null) {
|
|
1436
1730
|
console.error("usage: kmd db reset [<vault-root>] (or set WIKI_VAULT)");
|
|
1437
1731
|
process.exit(2);
|
|
1438
1732
|
}
|
|
1439
|
-
const dir =
|
|
1440
|
-
if (!
|
|
1733
|
+
const dir = dirname5(resolveIndexPath(resolution.root));
|
|
1734
|
+
if (!existsSync4(dir)) {
|
|
1441
1735
|
console.log(`${dir} does not exist \u2014 nothing to reset`);
|
|
1442
1736
|
return;
|
|
1443
1737
|
}
|
|
@@ -1456,53 +1750,126 @@ async function main() {
|
|
|
1456
1750
|
await runValidate();
|
|
1457
1751
|
}
|
|
1458
1752
|
}
|
|
1753
|
+
var SOURCE_LABELS, NO_VAULT_HINT;
|
|
1459
1754
|
var init_cli = __esm({
|
|
1460
1755
|
"../cli/src/cli.ts"() {
|
|
1461
1756
|
"use strict";
|
|
1462
1757
|
init_database();
|
|
1758
|
+
init_kmd_config();
|
|
1759
|
+
init_vault_config();
|
|
1463
1760
|
init_sync();
|
|
1464
1761
|
init_validate();
|
|
1465
1762
|
init_init();
|
|
1763
|
+
SOURCE_LABELS = {
|
|
1764
|
+
positional: "positional",
|
|
1765
|
+
"project-local-config": "project tier (local config)",
|
|
1766
|
+
"project-config": "project tier (config)",
|
|
1767
|
+
"project-convention": "project tier (convention)",
|
|
1768
|
+
"default-root": "--default-root",
|
|
1769
|
+
env: "$WIKI_VAULT",
|
|
1770
|
+
"global-config": "global config (default_vault)",
|
|
1771
|
+
none: "none"
|
|
1772
|
+
};
|
|
1773
|
+
NO_VAULT_HINT = "no vault resolvable \u2014 pass a vault root, run inside a project vault, set WIKI_VAULT, or `kmd config set default_vault <path>`";
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
// ../mcp/src/binding.ts
|
|
1778
|
+
import { fileURLToPath } from "node:url";
|
|
1779
|
+
function projectDirsFromRoots(roots) {
|
|
1780
|
+
const dirs = [];
|
|
1781
|
+
for (const root of roots) {
|
|
1782
|
+
if (!root.uri.startsWith("file://")) continue;
|
|
1783
|
+
try {
|
|
1784
|
+
dirs.push(fileURLToPath(root.uri));
|
|
1785
|
+
} catch {
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return dirs;
|
|
1789
|
+
}
|
|
1790
|
+
function resolveDeferredVault(input) {
|
|
1791
|
+
const env = input.env ?? process.env;
|
|
1792
|
+
if (input.rootDirs === null) {
|
|
1793
|
+
return resolveVaultRoot({
|
|
1794
|
+
projectDir: input.cwd,
|
|
1795
|
+
defaultRoot: input.defaultRoot,
|
|
1796
|
+
envVault: input.envVault,
|
|
1797
|
+
globalDefault: input.globalDefault,
|
|
1798
|
+
env,
|
|
1799
|
+
onSkip: input.onSkip
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
for (const dir of input.rootDirs) {
|
|
1803
|
+
const tier = findProjectTier(dir, env, input.onSkip);
|
|
1804
|
+
if (tier !== null) {
|
|
1805
|
+
return { root: tier.vaultRoot, source: `project-${tier.via}`, tierRoot: tier.tierRoot };
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return resolveVaultRoot({
|
|
1809
|
+
defaultRoot: input.defaultRoot,
|
|
1810
|
+
envVault: input.envVault,
|
|
1811
|
+
globalDefault: input.globalDefault,
|
|
1812
|
+
env
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
var init_binding = __esm({
|
|
1816
|
+
"../mcp/src/binding.ts"() {
|
|
1817
|
+
"use strict";
|
|
1818
|
+
init_kmd_config();
|
|
1466
1819
|
}
|
|
1467
1820
|
});
|
|
1468
1821
|
|
|
1469
1822
|
// ../mcp/src/config.ts
|
|
1470
|
-
import { z as
|
|
1471
|
-
function
|
|
1472
|
-
const parsed =
|
|
1823
|
+
import { z as z4 } from "zod";
|
|
1824
|
+
function parseEnv(schema, env) {
|
|
1825
|
+
const parsed = schema.safeParse(env);
|
|
1473
1826
|
if (!parsed.success) {
|
|
1474
1827
|
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
1475
1828
|
throw new Error(`Invalid environment configuration:
|
|
1476
1829
|
${issues}`);
|
|
1477
1830
|
}
|
|
1831
|
+
return parsed.data;
|
|
1832
|
+
}
|
|
1833
|
+
function loadConfig(env = process.env) {
|
|
1834
|
+
const data = parseEnv(EnvSchema2, env);
|
|
1478
1835
|
return {
|
|
1479
|
-
wikiVault:
|
|
1480
|
-
logLevel:
|
|
1481
|
-
serverName:
|
|
1482
|
-
serverVersion:
|
|
1836
|
+
wikiVault: data.WIKI_VAULT,
|
|
1837
|
+
logLevel: data.LOG_LEVEL,
|
|
1838
|
+
serverName: data.SERVER_NAME,
|
|
1839
|
+
serverVersion: data.SERVER_VERSION
|
|
1483
1840
|
};
|
|
1484
1841
|
}
|
|
1485
|
-
|
|
1842
|
+
function loadServerEnv(env = process.env) {
|
|
1843
|
+
const data = parseEnv(BaseEnvSchema, env);
|
|
1844
|
+
return {
|
|
1845
|
+
logLevel: data.LOG_LEVEL,
|
|
1846
|
+
serverName: data.SERVER_NAME,
|
|
1847
|
+
serverVersion: data.SERVER_VERSION
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
var BaseEnvSchema, EnvSchema2;
|
|
1486
1851
|
var init_config = __esm({
|
|
1487
1852
|
"../mcp/src/config.ts"() {
|
|
1488
1853
|
"use strict";
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1854
|
+
BaseEnvSchema = z4.object({
|
|
1855
|
+
LOG_LEVEL: z4.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]).default("info").describe("Pino log level. Logs go to stderr to keep the stdio JSON-RPC stream clean."),
|
|
1856
|
+
SERVER_NAME: z4.string().default("wiki-mcp"),
|
|
1857
|
+
SERVER_VERSION: z4.string().default("0.0.0")
|
|
1858
|
+
});
|
|
1859
|
+
EnvSchema2 = BaseEnvSchema.extend({
|
|
1860
|
+
WIKI_VAULT: z4.string().min(1).describe("Absolute path to the Obsidian vault root")
|
|
1494
1861
|
});
|
|
1495
1862
|
}
|
|
1496
1863
|
});
|
|
1497
1864
|
|
|
1498
1865
|
// ../mcp/src/db.ts
|
|
1499
|
-
import { mkdirSync as
|
|
1500
|
-
import { dirname as
|
|
1501
|
-
function createDatabase(
|
|
1502
|
-
const dbPath = resolveIndexPath(
|
|
1503
|
-
|
|
1866
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
1867
|
+
import { dirname as dirname6 } from "node:path";
|
|
1868
|
+
function createDatabase(vaultRoot) {
|
|
1869
|
+
const dbPath = resolveIndexPath(vaultRoot);
|
|
1870
|
+
mkdirSync3(dirname6(dbPath), { recursive: true });
|
|
1504
1871
|
const db = openDatabase(dbPath);
|
|
1505
|
-
setMeta(db, "vault_root", canonicalVaultRoot(
|
|
1872
|
+
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot));
|
|
1506
1873
|
return db;
|
|
1507
1874
|
}
|
|
1508
1875
|
var init_db = __esm({
|
|
@@ -1513,9 +1880,9 @@ var init_db = __esm({
|
|
|
1513
1880
|
});
|
|
1514
1881
|
|
|
1515
1882
|
// ../mcp/src/lib/diag.ts
|
|
1516
|
-
import { appendFileSync, mkdirSync as
|
|
1517
|
-
import { homedir as
|
|
1518
|
-
import { join as
|
|
1883
|
+
import { appendFileSync, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1884
|
+
import { homedir as homedir3 } from "node:os";
|
|
1885
|
+
import { join as join8 } from "node:path";
|
|
1519
1886
|
function diag(msg, data) {
|
|
1520
1887
|
try {
|
|
1521
1888
|
const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
|
|
@@ -1529,10 +1896,10 @@ var DIAG_DIR, DIAG_LOG_PATH;
|
|
|
1529
1896
|
var init_diag = __esm({
|
|
1530
1897
|
"../mcp/src/lib/diag.ts"() {
|
|
1531
1898
|
"use strict";
|
|
1532
|
-
DIAG_DIR =
|
|
1533
|
-
DIAG_LOG_PATH =
|
|
1899
|
+
DIAG_DIR = join8(homedir3(), ".local", "state", "wiki-mcp");
|
|
1900
|
+
DIAG_LOG_PATH = join8(DIAG_DIR, "server.log");
|
|
1534
1901
|
try {
|
|
1535
|
-
|
|
1902
|
+
mkdirSync4(DIAG_DIR, { recursive: true });
|
|
1536
1903
|
} catch {
|
|
1537
1904
|
}
|
|
1538
1905
|
}
|
|
@@ -1611,7 +1978,7 @@ function buildVocabulary(config) {
|
|
|
1611
1978
|
}
|
|
1612
1979
|
return lines.join("\n");
|
|
1613
1980
|
}
|
|
1614
|
-
function registerAuthoringResource(mcp,
|
|
1981
|
+
function registerAuthoringResource(mcp, binding) {
|
|
1615
1982
|
mcp.registerResource(
|
|
1616
1983
|
"Authoring guide",
|
|
1617
1984
|
"wiki://authoring",
|
|
@@ -1620,10 +1987,11 @@ function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
|
|
|
1620
1987
|
mimeType: "text/markdown"
|
|
1621
1988
|
},
|
|
1622
1989
|
async (uri) => {
|
|
1990
|
+
const { vaultRoot, vaultConfig } = await binding;
|
|
1623
1991
|
const sections = [
|
|
1624
1992
|
"# Wiki authoring guide",
|
|
1625
1993
|
"",
|
|
1626
|
-
`Vault root: \`${canonicalVaultRoot(
|
|
1994
|
+
`Vault root: \`${canonicalVaultRoot(vaultRoot)}\` \u2014 every page path below is relative to it; write files and run \`kmd validate\` / \`kmd sync\` against it.`,
|
|
1627
1995
|
"",
|
|
1628
1996
|
buildKindSelector(vaultConfig.kinds),
|
|
1629
1997
|
"",
|
|
@@ -1772,7 +2140,7 @@ var init_authoring = __esm({
|
|
|
1772
2140
|
|
|
1773
2141
|
// ../mcp/src/resources/templates.ts
|
|
1774
2142
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
1775
|
-
import { join as
|
|
2143
|
+
import { join as join9 } from "node:path";
|
|
1776
2144
|
function customTemplates(config) {
|
|
1777
2145
|
const specs = [];
|
|
1778
2146
|
for (const entry of config.kinds) {
|
|
@@ -1786,41 +2154,52 @@ function customTemplates(config) {
|
|
|
1786
2154
|
}
|
|
1787
2155
|
return specs;
|
|
1788
2156
|
}
|
|
1789
|
-
function
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
cause: err
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
return {
|
|
1807
|
-
contents: [
|
|
1808
|
-
{
|
|
1809
|
-
uri: uri.toString(),
|
|
1810
|
-
mimeType: "text/markdown",
|
|
1811
|
-
text
|
|
1812
|
-
}
|
|
1813
|
-
]
|
|
1814
|
-
};
|
|
2157
|
+
function registerTemplate(mcp, tmpl, binding) {
|
|
2158
|
+
mcp.registerResource(
|
|
2159
|
+
tmpl.name,
|
|
2160
|
+
tmpl.uri,
|
|
2161
|
+
{ description: tmpl.description, mimeType: "text/markdown" },
|
|
2162
|
+
async (uri) => {
|
|
2163
|
+
const { vaultRoot } = await binding;
|
|
2164
|
+
let text;
|
|
2165
|
+
try {
|
|
2166
|
+
text = await readFile5(join9(vaultRoot, "templates", tmpl.file), "utf8");
|
|
2167
|
+
} catch (err) {
|
|
2168
|
+
throw new Error(`template file missing: templates/${tmpl.file} (${tmpl.uri})`, {
|
|
2169
|
+
cause: err
|
|
2170
|
+
});
|
|
1815
2171
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
2172
|
+
return {
|
|
2173
|
+
contents: [
|
|
2174
|
+
{
|
|
2175
|
+
uri: uri.toString(),
|
|
2176
|
+
mimeType: "text/markdown",
|
|
2177
|
+
text
|
|
2178
|
+
}
|
|
2179
|
+
]
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
function buildIndexText(vaultConfig) {
|
|
1818
2185
|
const indexLines = ["# Wiki Templates", ""];
|
|
1819
|
-
for (const tmpl of
|
|
2186
|
+
for (const tmpl of [...TEMPLATES, ...customTemplates(vaultConfig)]) {
|
|
1820
2187
|
indexLines.push(`- **${tmpl.name}** \u2014 \`${tmpl.uri}\` `);
|
|
1821
2188
|
indexLines.push(` ${tmpl.description}`);
|
|
1822
2189
|
}
|
|
1823
|
-
|
|
2190
|
+
return indexLines.join("\n");
|
|
2191
|
+
}
|
|
2192
|
+
function registerTemplateResources(mcp, binding) {
|
|
2193
|
+
for (const tmpl of TEMPLATES) {
|
|
2194
|
+
registerTemplate(mcp, tmpl, binding);
|
|
2195
|
+
}
|
|
2196
|
+
const registerCustom = (bound) => {
|
|
2197
|
+
for (const tmpl of customTemplates(bound.vaultConfig)) {
|
|
2198
|
+
registerTemplate(mcp, tmpl, binding);
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
if (binding instanceof Promise) void binding.then(registerCustom);
|
|
2202
|
+
else registerCustom(binding);
|
|
1824
2203
|
mcp.registerResource(
|
|
1825
2204
|
"Template index",
|
|
1826
2205
|
"wiki://templates",
|
|
@@ -1828,9 +2207,18 @@ function registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
|
|
|
1828
2207
|
description: "Index of all wiki templates with URIs and descriptions. Read a specific template via its URI.",
|
|
1829
2208
|
mimeType: "text/markdown"
|
|
1830
2209
|
},
|
|
1831
|
-
async (uri) =>
|
|
1832
|
-
|
|
1833
|
-
|
|
2210
|
+
async (uri) => {
|
|
2211
|
+
const { vaultConfig } = await binding;
|
|
2212
|
+
return {
|
|
2213
|
+
contents: [
|
|
2214
|
+
{
|
|
2215
|
+
uri: uri.toString(),
|
|
2216
|
+
mimeType: "text/markdown",
|
|
2217
|
+
text: buildIndexText(vaultConfig)
|
|
2218
|
+
}
|
|
2219
|
+
]
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
1834
2222
|
);
|
|
1835
2223
|
}
|
|
1836
2224
|
var TEMPLATES;
|
|
@@ -1977,7 +2365,7 @@ var init_toolResponse = __esm({
|
|
|
1977
2365
|
});
|
|
1978
2366
|
|
|
1979
2367
|
// ../mcp/src/tools/search.ts
|
|
1980
|
-
import { z as
|
|
2368
|
+
import { z as z5 } from "zod";
|
|
1981
2369
|
function search(deps, input) {
|
|
1982
2370
|
const ftsQuery = sanitizeFtsQuery(input.query);
|
|
1983
2371
|
if (!ftsQuery) return { results: [] };
|
|
@@ -2024,15 +2412,15 @@ var init_search = __esm({
|
|
|
2024
2412
|
"use strict";
|
|
2025
2413
|
init_fts();
|
|
2026
2414
|
init_toolResponse();
|
|
2027
|
-
SearchInputSchema =
|
|
2028
|
-
query:
|
|
2415
|
+
SearchInputSchema = z5.object({
|
|
2416
|
+
query: z5.string().min(1).describe(
|
|
2029
2417
|
"Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
|
|
2030
2418
|
),
|
|
2031
|
-
scope:
|
|
2032
|
-
kind:
|
|
2419
|
+
scope: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
|
|
2420
|
+
kind: z5.string().optional().describe(
|
|
2033
2421
|
"Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
|
|
2034
2422
|
),
|
|
2035
|
-
limit:
|
|
2423
|
+
limit: z5.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
|
|
2036
2424
|
});
|
|
2037
2425
|
FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
|
|
2038
2426
|
}
|
|
@@ -2040,33 +2428,33 @@ var init_search = __esm({
|
|
|
2040
2428
|
|
|
2041
2429
|
// ../mcp/src/tools/prime.ts
|
|
2042
2430
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
2043
|
-
import { basename as basename3, join as
|
|
2044
|
-
import { z as
|
|
2431
|
+
import { basename as basename3, join as join10 } from "node:path";
|
|
2432
|
+
import { z as z6 } from "zod";
|
|
2045
2433
|
function pathSlug(p) {
|
|
2046
2434
|
return basename3(p).replace(/\.md$/, "");
|
|
2047
2435
|
}
|
|
2048
|
-
async function readIndexFm(
|
|
2436
|
+
async function readIndexFm(vaultRoot, scope) {
|
|
2049
2437
|
try {
|
|
2050
|
-
const raw = await readFile6(
|
|
2438
|
+
const raw = await readFile6(join10(vaultRoot, "projects", scope, "index.md"), "utf8");
|
|
2051
2439
|
return parseFrontmatter2(raw).data;
|
|
2052
2440
|
} catch {
|
|
2053
2441
|
return {};
|
|
2054
2442
|
}
|
|
2055
2443
|
}
|
|
2056
|
-
async function readPrimer(
|
|
2444
|
+
async function readPrimer(vaultRoot, scope) {
|
|
2057
2445
|
try {
|
|
2058
|
-
const raw = await readFile6(
|
|
2446
|
+
const raw = await readFile6(join10(vaultRoot, "projects", scope, "primer.md"), "utf8");
|
|
2059
2447
|
return parseFrontmatter2(raw).content.trim().replace(/^#\s+[^\n]+\n+/, "");
|
|
2060
2448
|
} catch {
|
|
2061
2449
|
return "";
|
|
2062
2450
|
}
|
|
2063
2451
|
}
|
|
2064
2452
|
async function prime(deps, input) {
|
|
2065
|
-
const { db, vaultRoot
|
|
2453
|
+
const { db, vaultRoot, vaultConfig } = deps;
|
|
2066
2454
|
const { scope, task } = input;
|
|
2067
2455
|
const [fm, primer] = await Promise.all([
|
|
2068
|
-
readIndexFm(
|
|
2069
|
-
readPrimer(
|
|
2456
|
+
readIndexFm(vaultRoot, scope),
|
|
2457
|
+
readPrimer(vaultRoot, scope)
|
|
2070
2458
|
]);
|
|
2071
2459
|
const counts = db.prepare("SELECT kind, count(*) AS count FROM pages WHERE scope = ? GROUP BY kind").all(scope);
|
|
2072
2460
|
const adrs = db.prepare(
|
|
@@ -2120,7 +2508,7 @@ async function prime(deps, input) {
|
|
|
2120
2508
|
for (const row of counts) countsRecord[row.kind] = Number(row.count);
|
|
2121
2509
|
const data = {
|
|
2122
2510
|
scope,
|
|
2123
|
-
vault_root: canonicalVaultRoot(
|
|
2511
|
+
vault_root: canonicalVaultRoot(vaultRoot),
|
|
2124
2512
|
title: fm.title ?? null,
|
|
2125
2513
|
methodology: fm.methodology ?? null,
|
|
2126
2514
|
phase: typeof fm.phase === "number" ? fm.phase : null,
|
|
@@ -2250,9 +2638,9 @@ var init_prime = __esm({
|
|
|
2250
2638
|
init_fts();
|
|
2251
2639
|
init_toolResponse();
|
|
2252
2640
|
init_search();
|
|
2253
|
-
PrimeInputSchema =
|
|
2254
|
-
scope:
|
|
2255
|
-
task:
|
|
2641
|
+
PrimeInputSchema = z6.object({
|
|
2642
|
+
scope: z6.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
|
|
2643
|
+
task: z6.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
|
|
2256
2644
|
});
|
|
2257
2645
|
}
|
|
2258
2646
|
});
|
|
@@ -2260,15 +2648,20 @@ var init_prime = __esm({
|
|
|
2260
2648
|
// ../mcp/src/server.ts
|
|
2261
2649
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2262
2650
|
function buildServer(args) {
|
|
2263
|
-
const { name, version,
|
|
2264
|
-
const
|
|
2651
|
+
const { name, version, logger, binding } = args;
|
|
2652
|
+
const deferred = binding instanceof Promise;
|
|
2653
|
+
const mcp = new McpServer(
|
|
2654
|
+
{ name, version },
|
|
2655
|
+
{ capabilities: { tools: {}, resources: deferred ? { listChanged: true } : {} } }
|
|
2656
|
+
);
|
|
2265
2657
|
mcp.tool(
|
|
2266
2658
|
"prime",
|
|
2267
2659
|
"Orient on a project. Returns a markdown briefing with: identity (scope, phase, methodology, summary), the human-authored primer.md inlined, active ADRs, current plan, page counts, top tags, hub pages (most-linked-to), recent events, cross-scope references, and \u2014 when `task` is provided \u2014 the top 3 tsvector-ranked relevant pages. Call once at session start. Empty sections are omitted to keep the surface lean.",
|
|
2268
2660
|
PrimeInputSchema.shape,
|
|
2269
2661
|
async (input) => {
|
|
2270
2662
|
logger.debug({ tool: "prime", input }, "tool call");
|
|
2271
|
-
|
|
2663
|
+
const { db, vaultRoot, vaultConfig } = await binding;
|
|
2664
|
+
return handlePrime({ db, vaultRoot, vaultConfig }, input);
|
|
2272
2665
|
}
|
|
2273
2666
|
);
|
|
2274
2667
|
mcp.tool(
|
|
@@ -2277,11 +2670,12 @@ function buildServer(args) {
|
|
|
2277
2670
|
SearchInputSchema.shape,
|
|
2278
2671
|
async (input) => {
|
|
2279
2672
|
logger.debug({ tool: "search", input }, "tool call");
|
|
2673
|
+
const { db } = await binding;
|
|
2280
2674
|
return handleSearch({ db }, input);
|
|
2281
2675
|
}
|
|
2282
2676
|
);
|
|
2283
|
-
registerTemplateResources(mcp,
|
|
2284
|
-
registerAuthoringResource(mcp,
|
|
2677
|
+
registerTemplateResources(mcp, binding);
|
|
2678
|
+
registerAuthoringResource(mcp, binding);
|
|
2285
2679
|
return mcp;
|
|
2286
2680
|
}
|
|
2287
2681
|
var init_server = __esm({
|
|
@@ -2300,8 +2694,25 @@ __export(start_exports, {
|
|
|
2300
2694
|
startMcpServer: () => startMcpServer
|
|
2301
2695
|
});
|
|
2302
2696
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2303
|
-
|
|
2697
|
+
import { RootsListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2698
|
+
function installShutdown(mcp, logger, getDb) {
|
|
2699
|
+
const shutdown = async (signal) => {
|
|
2700
|
+
logger.info({ signal }, "shutting down");
|
|
2701
|
+
diag("shutting down", { signal });
|
|
2702
|
+
try {
|
|
2703
|
+
await mcp.close();
|
|
2704
|
+
getDb()?.close();
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
logger.error({ err }, "error during shutdown");
|
|
2707
|
+
}
|
|
2708
|
+
process.exit(0);
|
|
2709
|
+
};
|
|
2710
|
+
process.once("SIGINT", (s) => void shutdown(s));
|
|
2711
|
+
process.once("SIGTERM", (s) => void shutdown(s));
|
|
2712
|
+
}
|
|
2713
|
+
async function startMcpServer(deferred) {
|
|
2304
2714
|
diag("main entered");
|
|
2715
|
+
if (deferred) return startDeferred(deferred);
|
|
2305
2716
|
const config = loadConfig();
|
|
2306
2717
|
diag("config loaded", { vault: config.wikiVault, level: config.logLevel });
|
|
2307
2718
|
const vaultConfig = await loadVaultConfig(config.wikiVault);
|
|
@@ -2320,34 +2731,89 @@ async function startMcpServer() {
|
|
|
2320
2731
|
const mcp = buildServer({
|
|
2321
2732
|
name: config.serverName,
|
|
2322
2733
|
version: config.serverVersion,
|
|
2323
|
-
vaultRoot: config.wikiVault,
|
|
2324
|
-
db,
|
|
2325
2734
|
logger,
|
|
2326
|
-
vaultConfig
|
|
2735
|
+
binding: { vaultRoot: config.wikiVault, db, vaultConfig }
|
|
2327
2736
|
});
|
|
2328
2737
|
diag("server built");
|
|
2329
|
-
|
|
2330
|
-
logger.info({ signal }, "shutting down");
|
|
2331
|
-
diag("shutting down", { signal });
|
|
2332
|
-
try {
|
|
2333
|
-
await mcp.close();
|
|
2334
|
-
db.close();
|
|
2335
|
-
} catch (err) {
|
|
2336
|
-
logger.error({ err }, "error during shutdown");
|
|
2337
|
-
}
|
|
2338
|
-
process.exit(0);
|
|
2339
|
-
};
|
|
2340
|
-
process.once("SIGINT", (s) => void shutdown(s));
|
|
2341
|
-
process.once("SIGTERM", (s) => void shutdown(s));
|
|
2738
|
+
installShutdown(mcp, logger, () => db);
|
|
2342
2739
|
const transport = new StdioServerTransport();
|
|
2343
2740
|
await mcp.connect(transport);
|
|
2344
2741
|
logger.info("wiki-mcp ready");
|
|
2345
2742
|
diag("ready and connected to transport");
|
|
2346
2743
|
}
|
|
2744
|
+
async function startDeferred(input) {
|
|
2745
|
+
const env = loadServerEnv();
|
|
2746
|
+
diag("config loaded, vault binding deferred", { level: env.logLevel, cwd: input.cwd });
|
|
2747
|
+
const logger = createLogger(env.logLevel, env.serverName);
|
|
2748
|
+
logger.info(
|
|
2749
|
+
{ serverName: env.serverName, serverVersion: env.serverVersion },
|
|
2750
|
+
"starting wiki-mcp on stdio; vault binding deferred to after initialization"
|
|
2751
|
+
);
|
|
2752
|
+
let resolveBinding;
|
|
2753
|
+
const binding = new Promise((resolve6) => {
|
|
2754
|
+
resolveBinding = resolve6;
|
|
2755
|
+
});
|
|
2756
|
+
const mcp = buildServer({
|
|
2757
|
+
name: env.serverName,
|
|
2758
|
+
version: env.serverVersion,
|
|
2759
|
+
logger,
|
|
2760
|
+
binding
|
|
2761
|
+
});
|
|
2762
|
+
diag("server built (deferred binding)");
|
|
2763
|
+
let bound = null;
|
|
2764
|
+
const bind = async () => {
|
|
2765
|
+
let rootDirs = null;
|
|
2766
|
+
if (mcp.server.getClientCapabilities()?.roots) {
|
|
2767
|
+
const { roots } = await mcp.server.listRoots();
|
|
2768
|
+
rootDirs = projectDirsFromRoots(roots);
|
|
2769
|
+
diag("client roots received", { uris: roots.map((r) => r.uri), dirs: rootDirs });
|
|
2770
|
+
} else {
|
|
2771
|
+
diag("client declares no roots capability; falling back to the cwd-fed chain");
|
|
2772
|
+
}
|
|
2773
|
+
const resolution = resolveDeferredVault({
|
|
2774
|
+
rootDirs,
|
|
2775
|
+
cwd: input.cwd,
|
|
2776
|
+
defaultRoot: input.defaultRoot,
|
|
2777
|
+
envVault: input.envVault,
|
|
2778
|
+
globalDefault: input.globalDefault,
|
|
2779
|
+
onSkip: (candidate) => logger.warn({ candidate }, "ignoring unmarked vault.yaml \u2014 no .kmd sibling")
|
|
2780
|
+
});
|
|
2781
|
+
if (resolution.root === null) {
|
|
2782
|
+
throw new Error(
|
|
2783
|
+
"no vault resolvable \u2014 no client root maps to a vault; pass <vault-root>, set KMD_PROJECT_DIR or WIKI_VAULT, use --default-root, or `kmd config set default_vault <path>`"
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
process.env.WIKI_VAULT = resolution.root;
|
|
2787
|
+
const vaultConfig = await loadVaultConfig(resolution.root);
|
|
2788
|
+
const db = createDatabase(resolution.root);
|
|
2789
|
+
bound = { vaultRoot: resolution.root, db, vaultConfig };
|
|
2790
|
+
diag("vault bound", { vault: resolution.root, source: resolution.source });
|
|
2791
|
+
logger.info({ vault: resolution.root, source: resolution.source }, "vault bound");
|
|
2792
|
+
resolveBinding(bound);
|
|
2793
|
+
};
|
|
2794
|
+
mcp.server.oninitialized = () => {
|
|
2795
|
+
void bind().catch((err) => {
|
|
2796
|
+
const msg = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
2797
|
+
diag("FATAL at vault bind", { err: msg });
|
|
2798
|
+
process.stderr.write(`fatal: vault bind failed: ${msg}
|
|
2799
|
+
`);
|
|
2800
|
+
process.exit(1);
|
|
2801
|
+
});
|
|
2802
|
+
};
|
|
2803
|
+
mcp.server.setNotificationHandler(RootsListChangedNotificationSchema, () => {
|
|
2804
|
+
logger.debug("roots/list_changed received; rebinding declined \u2014 vault binds once per server");
|
|
2805
|
+
});
|
|
2806
|
+
installShutdown(mcp, logger, () => bound?.db ?? null);
|
|
2807
|
+
const transport = new StdioServerTransport();
|
|
2808
|
+
await mcp.connect(transport);
|
|
2809
|
+
logger.info("wiki-mcp ready; awaiting initialization to bind the vault");
|
|
2810
|
+
diag("ready and connected to transport; vault binds after initialize");
|
|
2811
|
+
}
|
|
2347
2812
|
var init_start = __esm({
|
|
2348
2813
|
"../mcp/src/start.ts"() {
|
|
2349
2814
|
"use strict";
|
|
2350
2815
|
init_vault_config();
|
|
2816
|
+
init_binding();
|
|
2351
2817
|
init_config();
|
|
2352
2818
|
init_db();
|
|
2353
2819
|
init_diag();
|
|
@@ -2387,13 +2853,13 @@ __export(hook_exports, {
|
|
|
2387
2853
|
runHookStop: () => runHookStop,
|
|
2388
2854
|
vaultPathTouched: () => vaultPathTouched
|
|
2389
2855
|
});
|
|
2390
|
-
import { mkdirSync as
|
|
2391
|
-
import { homedir as
|
|
2392
|
-
import { join as
|
|
2856
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2857
|
+
import { homedir as homedir4 } from "node:os";
|
|
2858
|
+
import { join as join11, resolve as resolve5, sep as sep2 } from "node:path";
|
|
2393
2859
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
2394
2860
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
2395
2861
|
import { parse as parseYaml3 } from "yaml";
|
|
2396
|
-
import { z as
|
|
2862
|
+
import { z as z7 } from "zod";
|
|
2397
2863
|
function eventFields(raw) {
|
|
2398
2864
|
let data;
|
|
2399
2865
|
try {
|
|
@@ -2419,7 +2885,7 @@ function kiroIdePromptEvent(now = Date.now()) {
|
|
|
2419
2885
|
}
|
|
2420
2886
|
function loadTriggerFile(path) {
|
|
2421
2887
|
try {
|
|
2422
|
-
const result =
|
|
2888
|
+
const result = z7.array(TriggerSchema).safeParse(parseYaml3(readFileSync2(path, "utf8")));
|
|
2423
2889
|
return result.success ? result.data : null;
|
|
2424
2890
|
} catch {
|
|
2425
2891
|
return null;
|
|
@@ -2443,9 +2909,9 @@ function effectiveTriggers(config, scope, fileTriggers = []) {
|
|
|
2443
2909
|
}
|
|
2444
2910
|
return { triggers, duplicates };
|
|
2445
2911
|
}
|
|
2446
|
-
function
|
|
2447
|
-
if (path === "~") return
|
|
2448
|
-
return path.startsWith("~/") ?
|
|
2912
|
+
function expandHome2(path) {
|
|
2913
|
+
if (path === "~") return homedir4();
|
|
2914
|
+
return path.startsWith("~/") ? join11(homedir4(), path.slice(2)) : path;
|
|
2449
2915
|
}
|
|
2450
2916
|
function resolveScope(config, cwd) {
|
|
2451
2917
|
if (cwd === void 0 || cwd === "") return void 0;
|
|
@@ -2453,7 +2919,7 @@ function resolveScope(config, cwd) {
|
|
|
2453
2919
|
let bestLength = -1;
|
|
2454
2920
|
for (const [name, scope] of Object.entries(config.scopes)) {
|
|
2455
2921
|
if (scope.repo === void 0) continue;
|
|
2456
|
-
const repo =
|
|
2922
|
+
const repo = expandHome2(scope.repo).replace(/\/+$/, "");
|
|
2457
2923
|
if (!repo.startsWith("/")) continue;
|
|
2458
2924
|
if (cwd !== repo && !cwd.startsWith(`${repo}/`)) continue;
|
|
2459
2925
|
if (repo.length > bestLength) {
|
|
@@ -2602,7 +3068,7 @@ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
|
2602
3068
|
}
|
|
2603
3069
|
return matches;
|
|
2604
3070
|
}
|
|
2605
|
-
function evaluateMatches(matches,
|
|
3071
|
+
function evaluateMatches(matches, vaultRoot) {
|
|
2606
3072
|
const fired = [];
|
|
2607
3073
|
const skipped = [];
|
|
2608
3074
|
for (const match of matches) {
|
|
@@ -2610,38 +3076,38 @@ function evaluateMatches(matches, vaultRoot2) {
|
|
|
2610
3076
|
fired.push(match);
|
|
2611
3077
|
continue;
|
|
2612
3078
|
}
|
|
2613
|
-
const verdict = evaluateWhen(match.when,
|
|
3079
|
+
const verdict = evaluateWhen(match.when, vaultRoot);
|
|
2614
3080
|
if (verdict === null) skipped.push(match.id);
|
|
2615
3081
|
else if (!verdict) fired.push(match);
|
|
2616
3082
|
}
|
|
2617
3083
|
return { fired, skipped };
|
|
2618
3084
|
}
|
|
2619
|
-
function evaluateWhenVerdict(when,
|
|
3085
|
+
function evaluateWhenVerdict(when, vaultRoot) {
|
|
2620
3086
|
if (typeof when === "string") return "unknown";
|
|
2621
3087
|
try {
|
|
2622
|
-
const than = newestUpdated(
|
|
3088
|
+
const than = newestUpdated(vaultRoot, when.than);
|
|
2623
3089
|
if (than === null) return "vacuous";
|
|
2624
|
-
const fresh = newestUpdated(
|
|
3090
|
+
const fresh = newestUpdated(vaultRoot, when.fresh);
|
|
2625
3091
|
if (fresh === null) return "unmet";
|
|
2626
3092
|
return fresh >= than ? "satisfied" : "unmet";
|
|
2627
3093
|
} catch {
|
|
2628
3094
|
return "unknown";
|
|
2629
3095
|
}
|
|
2630
3096
|
}
|
|
2631
|
-
function evaluateWhen(when,
|
|
2632
|
-
const verdict = evaluateWhenVerdict(when,
|
|
3097
|
+
function evaluateWhen(when, vaultRoot) {
|
|
3098
|
+
const verdict = evaluateWhenVerdict(when, vaultRoot);
|
|
2633
3099
|
if (verdict === "unknown") return null;
|
|
2634
3100
|
return verdict !== "unmet";
|
|
2635
3101
|
}
|
|
2636
|
-
function newestUpdated(
|
|
3102
|
+
function newestUpdated(vaultRoot, globs) {
|
|
2637
3103
|
const regexes = globs.map(globToRegExp);
|
|
2638
3104
|
let newest = null;
|
|
2639
|
-
for (const entry of readdirSync2(
|
|
3105
|
+
for (const entry of readdirSync2(vaultRoot, { recursive: true })) {
|
|
2640
3106
|
const rel = entry.split(sep2).join("/");
|
|
2641
3107
|
if (!rel.endsWith(".md")) continue;
|
|
2642
3108
|
if (rel.startsWith(".") || rel.includes("/.")) continue;
|
|
2643
3109
|
if (!regexes.some((regex) => regex.test(rel))) continue;
|
|
2644
|
-
const updated = readUpdated(
|
|
3110
|
+
const updated = readUpdated(join11(vaultRoot, entry));
|
|
2645
3111
|
if (updated !== null && (newest === null || updated > newest)) {
|
|
2646
3112
|
newest = updated;
|
|
2647
3113
|
}
|
|
@@ -2650,7 +3116,7 @@ function newestUpdated(vaultRoot2, globs) {
|
|
|
2650
3116
|
}
|
|
2651
3117
|
function readUpdated(path) {
|
|
2652
3118
|
try {
|
|
2653
|
-
const { data } = parseFrontmatter(
|
|
3119
|
+
const { data } = parseFrontmatter(readFileSync2(path, "utf8"));
|
|
2654
3120
|
const updated = data.updated;
|
|
2655
3121
|
if (typeof updated === "string") return updated;
|
|
2656
3122
|
if (updated instanceof Date) return updated.toISOString().slice(0, 10);
|
|
@@ -2713,15 +3179,15 @@ function commandPaths(toolInput) {
|
|
|
2713
3179
|
}
|
|
2714
3180
|
return paths;
|
|
2715
3181
|
}
|
|
2716
|
-
function vaultPathTouched(toolInput,
|
|
2717
|
-
const root =
|
|
3182
|
+
function vaultPathTouched(toolInput, vaultRoot, cwd) {
|
|
3183
|
+
const root = resolve5(vaultRoot);
|
|
2718
3184
|
const candidates = [
|
|
2719
3185
|
...pathCandidates(toolInput, cwd),
|
|
2720
3186
|
...patchPaths(toolInput),
|
|
2721
3187
|
...commandPaths(toolInput)
|
|
2722
3188
|
];
|
|
2723
3189
|
return candidates.some((candidate) => {
|
|
2724
|
-
const absolute =
|
|
3190
|
+
const absolute = resolve5(cwd ?? ".", candidate);
|
|
2725
3191
|
return absolute === root || absolute.startsWith(`${root}/`);
|
|
2726
3192
|
});
|
|
2727
3193
|
}
|
|
@@ -2771,12 +3237,12 @@ function dedupePretoolMatches(stateDir, sessionId, matches, persist = true) {
|
|
|
2771
3237
|
const fresh = dedupeMatches(stateDir, sessionId, rest, Date.now(), persist);
|
|
2772
3238
|
return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
|
|
2773
3239
|
}
|
|
2774
|
-
function hookStateDir() {
|
|
2775
|
-
return
|
|
3240
|
+
function hookStateDir(vaultRoot) {
|
|
3241
|
+
return resolveStateDir(vaultRoot);
|
|
2776
3242
|
}
|
|
2777
3243
|
function explainPrompt(options) {
|
|
2778
3244
|
const now = options.now ?? Date.now();
|
|
2779
|
-
const fired = readFired(
|
|
3245
|
+
const fired = readFired(join11(options.stateDir, safeName(options.sessionId)));
|
|
2780
3246
|
const entries = [];
|
|
2781
3247
|
const rendered = [];
|
|
2782
3248
|
const state = { db: null };
|
|
@@ -2813,7 +3279,7 @@ function explainPrompt(options) {
|
|
|
2813
3279
|
}
|
|
2814
3280
|
function explainPretool(options) {
|
|
2815
3281
|
const now = options.now ?? Date.now();
|
|
2816
|
-
const fired = readFired(
|
|
3282
|
+
const fired = readFired(join11(options.stateDir, safeName(options.sessionId)));
|
|
2817
3283
|
const entries = [];
|
|
2818
3284
|
const rendered = [];
|
|
2819
3285
|
for (const trigger of options.triggers) {
|
|
@@ -2854,7 +3320,7 @@ function explainPretool(options) {
|
|
|
2854
3320
|
}
|
|
2855
3321
|
function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist = true) {
|
|
2856
3322
|
if (matches.length === 0) return [];
|
|
2857
|
-
const dir =
|
|
3323
|
+
const dir = join11(stateDir, safeName(sessionId));
|
|
2858
3324
|
const fired = readFired(dir);
|
|
2859
3325
|
const fresh = [];
|
|
2860
3326
|
const record = [];
|
|
@@ -2870,10 +3336,10 @@ function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist =
|
|
|
2870
3336
|
}
|
|
2871
3337
|
if (persist && record.length > 0) {
|
|
2872
3338
|
try {
|
|
2873
|
-
|
|
3339
|
+
mkdirSync5(dir, { recursive: true });
|
|
2874
3340
|
for (const key of record) {
|
|
2875
3341
|
try {
|
|
2876
|
-
|
|
3342
|
+
writeFileSync2(join11(dir, key), "", { flag: "wx" });
|
|
2877
3343
|
} catch (err) {
|
|
2878
3344
|
if (err.code !== "EEXIST") throw err;
|
|
2879
3345
|
}
|
|
@@ -2905,7 +3371,7 @@ function pruneStale(stateDir, keep) {
|
|
|
2905
3371
|
try {
|
|
2906
3372
|
const cutoff = Date.now() - SESSION_STATE_MAX_AGE_MS;
|
|
2907
3373
|
for (const entry of readdirSync2(stateDir)) {
|
|
2908
|
-
const path =
|
|
3374
|
+
const path = join11(stateDir, entry);
|
|
2909
3375
|
if (path !== keep && statSync(path).mtimeMs < cutoff) {
|
|
2910
3376
|
rmSync2(path, { recursive: true, force: true });
|
|
2911
3377
|
}
|
|
@@ -2922,17 +3388,14 @@ function hookInvocation() {
|
|
|
2922
3388
|
scope: { type: "string" },
|
|
2923
3389
|
harness: { type: "string" },
|
|
2924
3390
|
triggers: { type: "string" },
|
|
3391
|
+
"default-root": { type: "string" },
|
|
2925
3392
|
"dry-run": { type: "boolean" },
|
|
2926
3393
|
explain: { type: "boolean" }
|
|
2927
3394
|
}
|
|
2928
3395
|
});
|
|
2929
|
-
const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
|
|
2930
|
-
if (vaultRoot2 === void 0 || vaultRoot2 === "") {
|
|
2931
|
-
diag2("no vault root (positional or $WIKI_VAULT)");
|
|
2932
|
-
return null;
|
|
2933
|
-
}
|
|
2934
3396
|
return {
|
|
2935
|
-
|
|
3397
|
+
positional: positionals2[2],
|
|
3398
|
+
defaultRoot: typeof values2["default-root"] === "string" ? values2["default-root"] : void 0,
|
|
2936
3399
|
scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
|
|
2937
3400
|
harness: values2.harness,
|
|
2938
3401
|
triggersFile: values2.triggers,
|
|
@@ -2940,6 +3403,28 @@ function hookInvocation() {
|
|
|
2940
3403
|
explain: values2.explain === true
|
|
2941
3404
|
};
|
|
2942
3405
|
}
|
|
3406
|
+
function resolveHookVault(invocation, eventCwd) {
|
|
3407
|
+
if (invocation.positional !== void 0 && invocation.defaultRoot !== void 0) {
|
|
3408
|
+
diag2("<vault-root> and --default-root are mutually exclusive \u2014 using the positional");
|
|
3409
|
+
}
|
|
3410
|
+
try {
|
|
3411
|
+
const resolution = resolveVaultRoot({
|
|
3412
|
+
positional: invocation.positional,
|
|
3413
|
+
projectDir: eventCwd,
|
|
3414
|
+
defaultRoot: invocation.defaultRoot,
|
|
3415
|
+
envVault: process.env.WIKI_VAULT,
|
|
3416
|
+
globalDefault: loadGlobalConfig().default_vault
|
|
3417
|
+
});
|
|
3418
|
+
if (resolution.root === null) {
|
|
3419
|
+
diag2("no vault root resolvable (positional, event cwd, --default-root, or $WIKI_VAULT)");
|
|
3420
|
+
return null;
|
|
3421
|
+
}
|
|
3422
|
+
return resolution.root;
|
|
3423
|
+
} catch (err) {
|
|
3424
|
+
diag2(`vault resolution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3425
|
+
return null;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
2943
3428
|
function resolveFileTriggers(invocation) {
|
|
2944
3429
|
if (typeof invocation.triggersFile !== "string") return [];
|
|
2945
3430
|
const loaded = loadTriggerFile(invocation.triggersFile);
|
|
@@ -2952,8 +3437,6 @@ function resolveFileTriggers(invocation) {
|
|
|
2952
3437
|
async function runHookPrompt() {
|
|
2953
3438
|
try {
|
|
2954
3439
|
const invocation = hookInvocation();
|
|
2955
|
-
if (invocation === null) return;
|
|
2956
|
-
const { vaultRoot: vaultRoot2 } = invocation;
|
|
2957
3440
|
let event = null;
|
|
2958
3441
|
if (invocation.harness === "kiro-ide") {
|
|
2959
3442
|
event = kiroIdePromptEvent();
|
|
@@ -2965,7 +3448,9 @@ async function runHookPrompt() {
|
|
|
2965
3448
|
diag2("stdin is not a prompt event ({session_id, prompt})");
|
|
2966
3449
|
return;
|
|
2967
3450
|
}
|
|
2968
|
-
const
|
|
3451
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3452
|
+
if (vaultRoot === null) return;
|
|
3453
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
2969
3454
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2970
3455
|
const { triggers, duplicates } = effectiveTriggers(
|
|
2971
3456
|
config,
|
|
@@ -2979,7 +3464,7 @@ async function runHookPrompt() {
|
|
|
2979
3464
|
const trace = explainPrompt({
|
|
2980
3465
|
prompt: event.prompt,
|
|
2981
3466
|
triggers,
|
|
2982
|
-
stateDir: hookStateDir(),
|
|
3467
|
+
stateDir: hookStateDir(vaultRoot),
|
|
2983
3468
|
sessionId: event.session_id
|
|
2984
3469
|
});
|
|
2985
3470
|
console.log(JSON.stringify({ event: "prompt", scope: scope ?? null, duplicates, ...trace }));
|
|
@@ -2987,7 +3472,7 @@ async function runHookPrompt() {
|
|
|
2987
3472
|
}
|
|
2988
3473
|
const matches = matchPromptTriggers(event.prompt, triggers);
|
|
2989
3474
|
const fresh = dedupeMatches(
|
|
2990
|
-
hookStateDir(),
|
|
3475
|
+
hookStateDir(vaultRoot),
|
|
2991
3476
|
event.session_id,
|
|
2992
3477
|
matches,
|
|
2993
3478
|
Date.now(),
|
|
@@ -3003,8 +3488,6 @@ async function runHookPrompt() {
|
|
|
3003
3488
|
async function runHookPretool() {
|
|
3004
3489
|
try {
|
|
3005
3490
|
const invocation = hookInvocation();
|
|
3006
|
-
if (invocation === null) return;
|
|
3007
|
-
const { vaultRoot: vaultRoot2 } = invocation;
|
|
3008
3491
|
let format = "neutral";
|
|
3009
3492
|
if (invocation.harness === "claude") {
|
|
3010
3493
|
format = "claude";
|
|
@@ -3016,7 +3499,9 @@ async function runHookPretool() {
|
|
|
3016
3499
|
diag2("stdin is not a pretool event ({session_id, tool_name})");
|
|
3017
3500
|
return;
|
|
3018
3501
|
}
|
|
3019
|
-
const
|
|
3502
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3503
|
+
if (vaultRoot === null) return;
|
|
3504
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3020
3505
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
3021
3506
|
const { triggers, duplicates } = effectiveTriggers(
|
|
3022
3507
|
config,
|
|
@@ -3031,8 +3516,8 @@ async function runHookPretool() {
|
|
|
3031
3516
|
toolName: event.tool_name,
|
|
3032
3517
|
toolInput: event.tool_input,
|
|
3033
3518
|
triggers,
|
|
3034
|
-
vaultRoot
|
|
3035
|
-
stateDir: hookStateDir(),
|
|
3519
|
+
vaultRoot,
|
|
3520
|
+
stateDir: hookStateDir(vaultRoot),
|
|
3036
3521
|
sessionId: event.session_id,
|
|
3037
3522
|
...event.cwd !== void 0 && { cwd: event.cwd },
|
|
3038
3523
|
format
|
|
@@ -3041,12 +3526,12 @@ async function runHookPretool() {
|
|
|
3041
3526
|
return;
|
|
3042
3527
|
}
|
|
3043
3528
|
const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
|
|
3044
|
-
const { fired, skipped } = evaluateMatches(matches,
|
|
3529
|
+
const { fired, skipped } = evaluateMatches(matches, vaultRoot);
|
|
3045
3530
|
for (const id of skipped) {
|
|
3046
3531
|
diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
|
|
3047
3532
|
}
|
|
3048
3533
|
const rendered = renderPretool(
|
|
3049
|
-
dedupePretoolMatches(hookStateDir(), event.session_id, fired, !invocation.dryRun),
|
|
3534
|
+
dedupePretoolMatches(hookStateDir(vaultRoot), event.session_id, fired, !invocation.dryRun),
|
|
3050
3535
|
format
|
|
3051
3536
|
);
|
|
3052
3537
|
for (const line of rendered.stderr) {
|
|
@@ -3062,7 +3547,6 @@ async function runHookPretool() {
|
|
|
3062
3547
|
async function runHookPosttool() {
|
|
3063
3548
|
try {
|
|
3064
3549
|
const invocation = hookInvocation();
|
|
3065
|
-
if (invocation === null) return;
|
|
3066
3550
|
if (invocation.dryRun) {
|
|
3067
3551
|
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3068
3552
|
return;
|
|
@@ -3078,13 +3562,15 @@ async function runHookPosttool() {
|
|
|
3078
3562
|
diag2("stdin is not a posttool event ({session_id, tool_name})");
|
|
3079
3563
|
return;
|
|
3080
3564
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3565
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3566
|
+
if (vaultRoot === null) return;
|
|
3567
|
+
if (!vaultPathTouched(event.tool_input, vaultRoot, event.cwd)) return;
|
|
3568
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3569
|
+
const findings = await validateVault(vaultRoot);
|
|
3084
3570
|
let synced = false;
|
|
3085
3571
|
if (!hasErrors(findings)) {
|
|
3086
3572
|
try {
|
|
3087
|
-
await syncVault(
|
|
3573
|
+
await syncVault(vaultRoot);
|
|
3088
3574
|
synced = true;
|
|
3089
3575
|
} catch (err) {
|
|
3090
3576
|
diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3101,7 +3587,6 @@ async function runHookPosttool() {
|
|
|
3101
3587
|
async function runHookStop() {
|
|
3102
3588
|
try {
|
|
3103
3589
|
const invocation = hookInvocation();
|
|
3104
|
-
if (invocation === null) return;
|
|
3105
3590
|
if (invocation.dryRun) {
|
|
3106
3591
|
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3107
3592
|
return;
|
|
@@ -3112,15 +3597,19 @@ async function runHookStop() {
|
|
|
3112
3597
|
return;
|
|
3113
3598
|
}
|
|
3114
3599
|
if (event.stop_hook_active === true) return;
|
|
3115
|
-
const
|
|
3600
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3601
|
+
if (vaultRoot === null) return;
|
|
3602
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3116
3603
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
3117
3604
|
if (scope === void 0) return;
|
|
3118
3605
|
const rendered = renderStop(
|
|
3119
|
-
await validateVault(
|
|
3606
|
+
await validateVault(vaultRoot),
|
|
3120
3607
|
config.builtin_hooks?.["handoff-gate"]?.reason
|
|
3121
3608
|
);
|
|
3122
3609
|
if (rendered === null) return;
|
|
3123
|
-
const fired = dedupeMatches(hookStateDir(), event.session_id, [
|
|
3610
|
+
const fired = dedupeMatches(hookStateDir(vaultRoot), event.session_id, [
|
|
3611
|
+
{ id: "handoff-gate" }
|
|
3612
|
+
]);
|
|
3124
3613
|
if (fired.length === 0) return;
|
|
3125
3614
|
console.log(rendered);
|
|
3126
3615
|
} catch (err) {
|
|
@@ -3145,7 +3634,6 @@ function renderSessionStart(scope, source, messages = {}) {
|
|
|
3145
3634
|
async function runHookSessionStart() {
|
|
3146
3635
|
try {
|
|
3147
3636
|
const invocation = hookInvocation();
|
|
3148
|
-
if (invocation === null) return;
|
|
3149
3637
|
if (invocation.dryRun) {
|
|
3150
3638
|
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3151
3639
|
return;
|
|
@@ -3155,7 +3643,9 @@ async function runHookSessionStart() {
|
|
|
3155
3643
|
diag2("stdin is not a session-start event ({session_id})");
|
|
3156
3644
|
return;
|
|
3157
3645
|
}
|
|
3158
|
-
const
|
|
3646
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3647
|
+
if (vaultRoot === null) return;
|
|
3648
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3159
3649
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
3160
3650
|
if (scope === void 0) return;
|
|
3161
3651
|
console.log(renderSessionStart(scope, event.source, config.builtin_hooks ?? {}));
|
|
@@ -3179,6 +3669,7 @@ var init_hook = __esm({
|
|
|
3179
3669
|
"../cli/src/hook.ts"() {
|
|
3180
3670
|
"use strict";
|
|
3181
3671
|
init_database();
|
|
3672
|
+
init_kmd_config();
|
|
3182
3673
|
init_vault_config();
|
|
3183
3674
|
init_frontmatter();
|
|
3184
3675
|
init_sync();
|
|
@@ -3208,16 +3699,33 @@ process.on("warning", (warning) => {
|
|
|
3208
3699
|
});
|
|
3209
3700
|
var USAGE = `usage: kmd <command> [options]
|
|
3210
3701
|
|
|
3702
|
+
vault resolution (every command): positional > project tier (.kmd/config.local.yaml >
|
|
3703
|
+
.kmd/config.yaml > vault/vault.yaml > vault.yaml with a .kmd marker beside it,
|
|
3704
|
+
nearest ancestor of $KMD_PROJECT_DIR or cwd) > --default-root > $WIKI_VAULT >
|
|
3705
|
+
~/.kmd/config.yaml default_vault
|
|
3706
|
+
|
|
3211
3707
|
commands:
|
|
3212
|
-
init [<dir>] [-y]
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3708
|
+
init [<dir>] [-y] [--set-default]
|
|
3709
|
+
scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y);
|
|
3710
|
+
TTY offers to record it as default_vault, --set-default records it
|
|
3711
|
+
without asking (-y alone never touches the machine default)
|
|
3712
|
+
init --local [-y] scaffold a project vault: <git-root>/vault + <git-root>/.kmd state home
|
|
3713
|
+
sync [<vault-root>] vault \u2192 index sync (runs validate first)
|
|
3714
|
+
validate [<path>] deterministic vault checker
|
|
3715
|
+
mcp [<vault-root>] [--default-root <path>]
|
|
3716
|
+
start the stdio MCP server; --default-root is the plugin form
|
|
3717
|
+
(config default the project tier may beat); flags are mutually exclusive;
|
|
3718
|
+
no positional and no $KMD_PROJECT_DIR: the vault binds after
|
|
3719
|
+
initialization, from the client's roots/list when it declares the
|
|
3720
|
+
roots capability
|
|
3721
|
+
config [<vault-root>] print resolved vault + winning source; with nothing resolvable, list known vaults
|
|
3722
|
+
config <set|get|unset> default_vault [<path>]
|
|
3723
|
+
read/write the global default in ~/.kmd/config.yaml
|
|
3724
|
+
db reset [<vault-root>] delete the vault's index
|
|
3725
|
+
hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--default-root <path>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
3219
3726
|
harness gate engine: JSON event on stdin, decision/context on stdout;
|
|
3220
|
-
|
|
3727
|
+
the vault resolves through the chain with the event cwd as project
|
|
3728
|
+
signal; posttool auto-runs validate + sync after a vault write;
|
|
3221
3729
|
stop blocks the handoff once while validate errors hold the sync
|
|
3222
3730
|
|
|
3223
3731
|
options:
|
|
@@ -3230,52 +3738,79 @@ var { positionals, values } = parseArgs3({
|
|
|
3230
3738
|
options: {
|
|
3231
3739
|
version: { type: "boolean", short: "v" },
|
|
3232
3740
|
help: { type: "boolean", short: "h" },
|
|
3233
|
-
yes: { type: "boolean", short: "y" }
|
|
3741
|
+
yes: { type: "boolean", short: "y" },
|
|
3742
|
+
"default-root": { type: "string" },
|
|
3743
|
+
"set-default": { type: "boolean" },
|
|
3744
|
+
local: { type: "boolean" }
|
|
3234
3745
|
}
|
|
3235
3746
|
});
|
|
3236
3747
|
var command = values.version ? "--version" : values.help ? "--help" : positionals[0];
|
|
3237
|
-
function applyVaultRoot(positionalIndex) {
|
|
3238
|
-
const arg = positionals[positionalIndex];
|
|
3239
|
-
if (arg) {
|
|
3240
|
-
process.env.WIKI_VAULT = arg;
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
3748
|
async function run() {
|
|
3244
3749
|
switch (command) {
|
|
3245
3750
|
case "init": {
|
|
3246
3751
|
const { runInit: runInit2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3247
|
-
await runInit2(
|
|
3752
|
+
await runInit2(
|
|
3753
|
+
positionals[1],
|
|
3754
|
+
Boolean(values.yes),
|
|
3755
|
+
Boolean(values.local),
|
|
3756
|
+
Boolean(values["set-default"])
|
|
3757
|
+
);
|
|
3248
3758
|
break;
|
|
3249
3759
|
}
|
|
3250
3760
|
case "sync": {
|
|
3251
3761
|
const { runSyncCommand: runSyncCommand2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3252
|
-
await runSyncCommand2();
|
|
3762
|
+
await runSyncCommand2(positionals[1]);
|
|
3253
3763
|
break;
|
|
3254
3764
|
}
|
|
3255
3765
|
case "validate": {
|
|
3256
|
-
applyVaultRoot(1);
|
|
3257
3766
|
const { runValidate: runValidate2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3258
|
-
await runValidate2();
|
|
3767
|
+
await runValidate2(positionals[1]);
|
|
3259
3768
|
break;
|
|
3260
3769
|
}
|
|
3261
3770
|
case "mcp": {
|
|
3262
|
-
|
|
3771
|
+
const positional = positionals[1];
|
|
3772
|
+
const defaultRoot = typeof values["default-root"] === "string" ? values["default-root"] : void 0;
|
|
3773
|
+
if (positional && defaultRoot !== void 0) {
|
|
3774
|
+
console.error("kmd mcp: <vault-root> and --default-root are mutually exclusive");
|
|
3775
|
+
process.exit(2);
|
|
3776
|
+
}
|
|
3263
3777
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_start(), start_exports));
|
|
3778
|
+
if (!positional && !process.env.KMD_PROJECT_DIR) {
|
|
3779
|
+
const { loadGlobalConfig: loadGlobalConfig2 } = await Promise.resolve().then(() => (init_kmd_config(), kmd_config_exports));
|
|
3780
|
+
await startMcpServer2({
|
|
3781
|
+
cwd: process.cwd(),
|
|
3782
|
+
defaultRoot,
|
|
3783
|
+
envVault: process.env.WIKI_VAULT,
|
|
3784
|
+
globalDefault: loadGlobalConfig2().default_vault
|
|
3785
|
+
});
|
|
3786
|
+
break;
|
|
3787
|
+
}
|
|
3788
|
+
const { resolveCliVault: resolveCliVault2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3789
|
+
const resolved = resolveCliVault2(positional, defaultRoot);
|
|
3790
|
+
if (resolved.root === null) {
|
|
3791
|
+
console.error(
|
|
3792
|
+
"kmd mcp: no vault resolvable \u2014 pass <vault-root>, run inside a project vault, set WIKI_VAULT, or `kmd config set default_vault <path>`"
|
|
3793
|
+
);
|
|
3794
|
+
process.exit(1);
|
|
3795
|
+
}
|
|
3796
|
+
process.env.WIKI_VAULT = resolved.root;
|
|
3264
3797
|
await startMcpServer2();
|
|
3265
3798
|
break;
|
|
3266
3799
|
}
|
|
3267
3800
|
case "config": {
|
|
3268
|
-
|
|
3269
|
-
const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3270
|
-
await
|
|
3801
|
+
const sub = positionals[1];
|
|
3802
|
+
const { runConfig: runConfig2, runConfigGet: runConfigGet2, runConfigSet: runConfigSet2, runConfigUnset: runConfigUnset2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3803
|
+
if (sub === "set") await runConfigSet2(positionals[2], positionals[3]);
|
|
3804
|
+
else if (sub === "get") await runConfigGet2(positionals[2]);
|
|
3805
|
+
else if (sub === "unset") await runConfigUnset2(positionals[2]);
|
|
3806
|
+
else await runConfig2(sub);
|
|
3271
3807
|
break;
|
|
3272
3808
|
}
|
|
3273
3809
|
case "db": {
|
|
3274
3810
|
const sub = positionals[1];
|
|
3275
3811
|
if (sub === "reset") {
|
|
3276
|
-
applyVaultRoot(2);
|
|
3277
3812
|
const { runDbReset: runDbReset2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3278
|
-
await runDbReset2();
|
|
3813
|
+
await runDbReset2(positionals[2]);
|
|
3279
3814
|
} else {
|
|
3280
3815
|
console.error(sub ? `unknown db subcommand: ${sub}` : "usage: kmd db reset [<vault-root>]");
|
|
3281
3816
|
process.exit(2);
|
|
@@ -3311,11 +3846,11 @@ async function run() {
|
|
|
3311
3846
|
}
|
|
3312
3847
|
case "--version":
|
|
3313
3848
|
case "-v": {
|
|
3314
|
-
const { readFileSync:
|
|
3315
|
-
const { join:
|
|
3316
|
-
const { fileURLToPath } = await import("node:url");
|
|
3317
|
-
const pkgDir =
|
|
3318
|
-
const pkg = JSON.parse(
|
|
3849
|
+
const { readFileSync: readFileSync3 } = await import("node:fs");
|
|
3850
|
+
const { join: join12, dirname: dirname7 } = await import("node:path");
|
|
3851
|
+
const { fileURLToPath: fileURLToPath2 } = await import("node:url");
|
|
3852
|
+
const pkgDir = dirname7(dirname7(fileURLToPath2(import.meta.url)));
|
|
3853
|
+
const pkg = JSON.parse(readFileSync3(join12(pkgDir, "package.json"), "utf8"));
|
|
3319
3854
|
console.log(pkg.version);
|
|
3320
3855
|
break;
|
|
3321
3856
|
}
|