@bartolli/kmd 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/kmd.mjs +862 -325
- package/dist/kmd.mjs.map +4 -4
- package/package.json +2 -2
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);
|
|
1835
|
+
return {
|
|
1836
|
+
wikiVault: data.WIKI_VAULT,
|
|
1837
|
+
logLevel: data.LOG_LEVEL,
|
|
1838
|
+
serverName: data.SERVER_NAME,
|
|
1839
|
+
serverVersion: data.SERVER_VERSION
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
function loadServerEnv(env = process.env) {
|
|
1843
|
+
const data = parseEnv(BaseEnvSchema, env);
|
|
1478
1844
|
return {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
serverVersion: parsed.data.SERVER_VERSION
|
|
1845
|
+
logLevel: data.LOG_LEVEL,
|
|
1846
|
+
serverName: data.SERVER_NAME,
|
|
1847
|
+
serverVersion: data.SERVER_VERSION
|
|
1483
1848
|
};
|
|
1484
1849
|
}
|
|
1485
|
-
var EnvSchema2;
|
|
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) {
|
|
@@ -2565,7 +3031,9 @@ function pathCandidates(toolInput, cwd) {
|
|
|
2565
3031
|
return candidates;
|
|
2566
3032
|
}
|
|
2567
3033
|
function pretoolStage(trigger, toolName, toolInput, cwd) {
|
|
2568
|
-
if (trigger.tool !== void 0 && trigger.tool !== toolName)
|
|
3034
|
+
if (trigger.tool !== void 0 && trigger.tool.toLowerCase() !== toolName.toLowerCase()) {
|
|
3035
|
+
return "tool";
|
|
3036
|
+
}
|
|
2569
3037
|
if (trigger.args_match !== void 0) {
|
|
2570
3038
|
const serialized = JSON.stringify(toolInput ?? {});
|
|
2571
3039
|
if (!new RegExp(trigger.args_match).test(serialized)) return "args";
|
|
@@ -2602,7 +3070,7 @@ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
|
2602
3070
|
}
|
|
2603
3071
|
return matches;
|
|
2604
3072
|
}
|
|
2605
|
-
function evaluateMatches(matches,
|
|
3073
|
+
function evaluateMatches(matches, vaultRoot) {
|
|
2606
3074
|
const fired = [];
|
|
2607
3075
|
const skipped = [];
|
|
2608
3076
|
for (const match of matches) {
|
|
@@ -2610,38 +3078,38 @@ function evaluateMatches(matches, vaultRoot2) {
|
|
|
2610
3078
|
fired.push(match);
|
|
2611
3079
|
continue;
|
|
2612
3080
|
}
|
|
2613
|
-
const verdict = evaluateWhen(match.when,
|
|
3081
|
+
const verdict = evaluateWhen(match.when, vaultRoot);
|
|
2614
3082
|
if (verdict === null) skipped.push(match.id);
|
|
2615
3083
|
else if (!verdict) fired.push(match);
|
|
2616
3084
|
}
|
|
2617
3085
|
return { fired, skipped };
|
|
2618
3086
|
}
|
|
2619
|
-
function evaluateWhenVerdict(when,
|
|
3087
|
+
function evaluateWhenVerdict(when, vaultRoot) {
|
|
2620
3088
|
if (typeof when === "string") return "unknown";
|
|
2621
3089
|
try {
|
|
2622
|
-
const than = newestUpdated(
|
|
3090
|
+
const than = newestUpdated(vaultRoot, when.than);
|
|
2623
3091
|
if (than === null) return "vacuous";
|
|
2624
|
-
const fresh = newestUpdated(
|
|
3092
|
+
const fresh = newestUpdated(vaultRoot, when.fresh);
|
|
2625
3093
|
if (fresh === null) return "unmet";
|
|
2626
3094
|
return fresh >= than ? "satisfied" : "unmet";
|
|
2627
3095
|
} catch {
|
|
2628
3096
|
return "unknown";
|
|
2629
3097
|
}
|
|
2630
3098
|
}
|
|
2631
|
-
function evaluateWhen(when,
|
|
2632
|
-
const verdict = evaluateWhenVerdict(when,
|
|
3099
|
+
function evaluateWhen(when, vaultRoot) {
|
|
3100
|
+
const verdict = evaluateWhenVerdict(when, vaultRoot);
|
|
2633
3101
|
if (verdict === "unknown") return null;
|
|
2634
3102
|
return verdict !== "unmet";
|
|
2635
3103
|
}
|
|
2636
|
-
function newestUpdated(
|
|
3104
|
+
function newestUpdated(vaultRoot, globs) {
|
|
2637
3105
|
const regexes = globs.map(globToRegExp);
|
|
2638
3106
|
let newest = null;
|
|
2639
|
-
for (const entry of readdirSync2(
|
|
3107
|
+
for (const entry of readdirSync2(vaultRoot, { recursive: true })) {
|
|
2640
3108
|
const rel = entry.split(sep2).join("/");
|
|
2641
3109
|
if (!rel.endsWith(".md")) continue;
|
|
2642
3110
|
if (rel.startsWith(".") || rel.includes("/.")) continue;
|
|
2643
3111
|
if (!regexes.some((regex) => regex.test(rel))) continue;
|
|
2644
|
-
const updated = readUpdated(
|
|
3112
|
+
const updated = readUpdated(join11(vaultRoot, entry));
|
|
2645
3113
|
if (updated !== null && (newest === null || updated > newest)) {
|
|
2646
3114
|
newest = updated;
|
|
2647
3115
|
}
|
|
@@ -2650,7 +3118,7 @@ function newestUpdated(vaultRoot2, globs) {
|
|
|
2650
3118
|
}
|
|
2651
3119
|
function readUpdated(path) {
|
|
2652
3120
|
try {
|
|
2653
|
-
const { data } = parseFrontmatter(
|
|
3121
|
+
const { data } = parseFrontmatter(readFileSync2(path, "utf8"));
|
|
2654
3122
|
const updated = data.updated;
|
|
2655
3123
|
if (typeof updated === "string") return updated;
|
|
2656
3124
|
if (updated instanceof Date) return updated.toISOString().slice(0, 10);
|
|
@@ -2713,15 +3181,15 @@ function commandPaths(toolInput) {
|
|
|
2713
3181
|
}
|
|
2714
3182
|
return paths;
|
|
2715
3183
|
}
|
|
2716
|
-
function vaultPathTouched(toolInput,
|
|
2717
|
-
const root =
|
|
3184
|
+
function vaultPathTouched(toolInput, vaultRoot, cwd) {
|
|
3185
|
+
const root = resolve5(vaultRoot);
|
|
2718
3186
|
const candidates = [
|
|
2719
3187
|
...pathCandidates(toolInput, cwd),
|
|
2720
3188
|
...patchPaths(toolInput),
|
|
2721
3189
|
...commandPaths(toolInput)
|
|
2722
3190
|
];
|
|
2723
3191
|
return candidates.some((candidate) => {
|
|
2724
|
-
const absolute =
|
|
3192
|
+
const absolute = resolve5(cwd ?? ".", candidate);
|
|
2725
3193
|
return absolute === root || absolute.startsWith(`${root}/`);
|
|
2726
3194
|
});
|
|
2727
3195
|
}
|
|
@@ -2771,12 +3239,12 @@ function dedupePretoolMatches(stateDir, sessionId, matches, persist = true) {
|
|
|
2771
3239
|
const fresh = dedupeMatches(stateDir, sessionId, rest, Date.now(), persist);
|
|
2772
3240
|
return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
|
|
2773
3241
|
}
|
|
2774
|
-
function hookStateDir() {
|
|
2775
|
-
return
|
|
3242
|
+
function hookStateDir(vaultRoot) {
|
|
3243
|
+
return resolveStateDir(vaultRoot);
|
|
2776
3244
|
}
|
|
2777
3245
|
function explainPrompt(options) {
|
|
2778
3246
|
const now = options.now ?? Date.now();
|
|
2779
|
-
const fired = readFired(
|
|
3247
|
+
const fired = readFired(join11(options.stateDir, safeName(options.sessionId)));
|
|
2780
3248
|
const entries = [];
|
|
2781
3249
|
const rendered = [];
|
|
2782
3250
|
const state = { db: null };
|
|
@@ -2813,7 +3281,7 @@ function explainPrompt(options) {
|
|
|
2813
3281
|
}
|
|
2814
3282
|
function explainPretool(options) {
|
|
2815
3283
|
const now = options.now ?? Date.now();
|
|
2816
|
-
const fired = readFired(
|
|
3284
|
+
const fired = readFired(join11(options.stateDir, safeName(options.sessionId)));
|
|
2817
3285
|
const entries = [];
|
|
2818
3286
|
const rendered = [];
|
|
2819
3287
|
for (const trigger of options.triggers) {
|
|
@@ -2854,7 +3322,7 @@ function explainPretool(options) {
|
|
|
2854
3322
|
}
|
|
2855
3323
|
function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist = true) {
|
|
2856
3324
|
if (matches.length === 0) return [];
|
|
2857
|
-
const dir =
|
|
3325
|
+
const dir = join11(stateDir, safeName(sessionId));
|
|
2858
3326
|
const fired = readFired(dir);
|
|
2859
3327
|
const fresh = [];
|
|
2860
3328
|
const record = [];
|
|
@@ -2870,10 +3338,10 @@ function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist =
|
|
|
2870
3338
|
}
|
|
2871
3339
|
if (persist && record.length > 0) {
|
|
2872
3340
|
try {
|
|
2873
|
-
|
|
3341
|
+
mkdirSync5(dir, { recursive: true });
|
|
2874
3342
|
for (const key of record) {
|
|
2875
3343
|
try {
|
|
2876
|
-
|
|
3344
|
+
writeFileSync2(join11(dir, key), "", { flag: "wx" });
|
|
2877
3345
|
} catch (err) {
|
|
2878
3346
|
if (err.code !== "EEXIST") throw err;
|
|
2879
3347
|
}
|
|
@@ -2905,7 +3373,7 @@ function pruneStale(stateDir, keep) {
|
|
|
2905
3373
|
try {
|
|
2906
3374
|
const cutoff = Date.now() - SESSION_STATE_MAX_AGE_MS;
|
|
2907
3375
|
for (const entry of readdirSync2(stateDir)) {
|
|
2908
|
-
const path =
|
|
3376
|
+
const path = join11(stateDir, entry);
|
|
2909
3377
|
if (path !== keep && statSync(path).mtimeMs < cutoff) {
|
|
2910
3378
|
rmSync2(path, { recursive: true, force: true });
|
|
2911
3379
|
}
|
|
@@ -2922,17 +3390,14 @@ function hookInvocation() {
|
|
|
2922
3390
|
scope: { type: "string" },
|
|
2923
3391
|
harness: { type: "string" },
|
|
2924
3392
|
triggers: { type: "string" },
|
|
3393
|
+
"default-root": { type: "string" },
|
|
2925
3394
|
"dry-run": { type: "boolean" },
|
|
2926
3395
|
explain: { type: "boolean" }
|
|
2927
3396
|
}
|
|
2928
3397
|
});
|
|
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
3398
|
return {
|
|
2935
|
-
|
|
3399
|
+
positional: positionals2[2],
|
|
3400
|
+
defaultRoot: typeof values2["default-root"] === "string" ? values2["default-root"] : void 0,
|
|
2936
3401
|
scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
|
|
2937
3402
|
harness: values2.harness,
|
|
2938
3403
|
triggersFile: values2.triggers,
|
|
@@ -2940,6 +3405,28 @@ function hookInvocation() {
|
|
|
2940
3405
|
explain: values2.explain === true
|
|
2941
3406
|
};
|
|
2942
3407
|
}
|
|
3408
|
+
function resolveHookVault(invocation, eventCwd) {
|
|
3409
|
+
if (invocation.positional !== void 0 && invocation.defaultRoot !== void 0) {
|
|
3410
|
+
diag2("<vault-root> and --default-root are mutually exclusive \u2014 using the positional");
|
|
3411
|
+
}
|
|
3412
|
+
try {
|
|
3413
|
+
const resolution = resolveVaultRoot({
|
|
3414
|
+
positional: invocation.positional,
|
|
3415
|
+
projectDir: eventCwd,
|
|
3416
|
+
defaultRoot: invocation.defaultRoot,
|
|
3417
|
+
envVault: process.env.WIKI_VAULT,
|
|
3418
|
+
globalDefault: loadGlobalConfig().default_vault
|
|
3419
|
+
});
|
|
3420
|
+
if (resolution.root === null) {
|
|
3421
|
+
diag2("no vault root resolvable (positional, event cwd, --default-root, or $WIKI_VAULT)");
|
|
3422
|
+
return null;
|
|
3423
|
+
}
|
|
3424
|
+
return resolution.root;
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
diag2(`vault resolution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3427
|
+
return null;
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
2943
3430
|
function resolveFileTriggers(invocation) {
|
|
2944
3431
|
if (typeof invocation.triggersFile !== "string") return [];
|
|
2945
3432
|
const loaded = loadTriggerFile(invocation.triggersFile);
|
|
@@ -2952,8 +3439,6 @@ function resolveFileTriggers(invocation) {
|
|
|
2952
3439
|
async function runHookPrompt() {
|
|
2953
3440
|
try {
|
|
2954
3441
|
const invocation = hookInvocation();
|
|
2955
|
-
if (invocation === null) return;
|
|
2956
|
-
const { vaultRoot: vaultRoot2 } = invocation;
|
|
2957
3442
|
let event = null;
|
|
2958
3443
|
if (invocation.harness === "kiro-ide") {
|
|
2959
3444
|
event = kiroIdePromptEvent();
|
|
@@ -2965,7 +3450,9 @@ async function runHookPrompt() {
|
|
|
2965
3450
|
diag2("stdin is not a prompt event ({session_id, prompt})");
|
|
2966
3451
|
return;
|
|
2967
3452
|
}
|
|
2968
|
-
const
|
|
3453
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3454
|
+
if (vaultRoot === null) return;
|
|
3455
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
2969
3456
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2970
3457
|
const { triggers, duplicates } = effectiveTriggers(
|
|
2971
3458
|
config,
|
|
@@ -2979,7 +3466,7 @@ async function runHookPrompt() {
|
|
|
2979
3466
|
const trace = explainPrompt({
|
|
2980
3467
|
prompt: event.prompt,
|
|
2981
3468
|
triggers,
|
|
2982
|
-
stateDir: hookStateDir(),
|
|
3469
|
+
stateDir: hookStateDir(vaultRoot),
|
|
2983
3470
|
sessionId: event.session_id
|
|
2984
3471
|
});
|
|
2985
3472
|
console.log(JSON.stringify({ event: "prompt", scope: scope ?? null, duplicates, ...trace }));
|
|
@@ -2987,7 +3474,7 @@ async function runHookPrompt() {
|
|
|
2987
3474
|
}
|
|
2988
3475
|
const matches = matchPromptTriggers(event.prompt, triggers);
|
|
2989
3476
|
const fresh = dedupeMatches(
|
|
2990
|
-
hookStateDir(),
|
|
3477
|
+
hookStateDir(vaultRoot),
|
|
2991
3478
|
event.session_id,
|
|
2992
3479
|
matches,
|
|
2993
3480
|
Date.now(),
|
|
@@ -3003,8 +3490,6 @@ async function runHookPrompt() {
|
|
|
3003
3490
|
async function runHookPretool() {
|
|
3004
3491
|
try {
|
|
3005
3492
|
const invocation = hookInvocation();
|
|
3006
|
-
if (invocation === null) return;
|
|
3007
|
-
const { vaultRoot: vaultRoot2 } = invocation;
|
|
3008
3493
|
let format = "neutral";
|
|
3009
3494
|
if (invocation.harness === "claude") {
|
|
3010
3495
|
format = "claude";
|
|
@@ -3016,7 +3501,9 @@ async function runHookPretool() {
|
|
|
3016
3501
|
diag2("stdin is not a pretool event ({session_id, tool_name})");
|
|
3017
3502
|
return;
|
|
3018
3503
|
}
|
|
3019
|
-
const
|
|
3504
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3505
|
+
if (vaultRoot === null) return;
|
|
3506
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3020
3507
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
3021
3508
|
const { triggers, duplicates } = effectiveTriggers(
|
|
3022
3509
|
config,
|
|
@@ -3031,8 +3518,8 @@ async function runHookPretool() {
|
|
|
3031
3518
|
toolName: event.tool_name,
|
|
3032
3519
|
toolInput: event.tool_input,
|
|
3033
3520
|
triggers,
|
|
3034
|
-
vaultRoot
|
|
3035
|
-
stateDir: hookStateDir(),
|
|
3521
|
+
vaultRoot,
|
|
3522
|
+
stateDir: hookStateDir(vaultRoot),
|
|
3036
3523
|
sessionId: event.session_id,
|
|
3037
3524
|
...event.cwd !== void 0 && { cwd: event.cwd },
|
|
3038
3525
|
format
|
|
@@ -3041,12 +3528,12 @@ async function runHookPretool() {
|
|
|
3041
3528
|
return;
|
|
3042
3529
|
}
|
|
3043
3530
|
const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
|
|
3044
|
-
const { fired, skipped } = evaluateMatches(matches,
|
|
3531
|
+
const { fired, skipped } = evaluateMatches(matches, vaultRoot);
|
|
3045
3532
|
for (const id of skipped) {
|
|
3046
3533
|
diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
|
|
3047
3534
|
}
|
|
3048
3535
|
const rendered = renderPretool(
|
|
3049
|
-
dedupePretoolMatches(hookStateDir(), event.session_id, fired, !invocation.dryRun),
|
|
3536
|
+
dedupePretoolMatches(hookStateDir(vaultRoot), event.session_id, fired, !invocation.dryRun),
|
|
3050
3537
|
format
|
|
3051
3538
|
);
|
|
3052
3539
|
for (const line of rendered.stderr) {
|
|
@@ -3062,7 +3549,6 @@ async function runHookPretool() {
|
|
|
3062
3549
|
async function runHookPosttool() {
|
|
3063
3550
|
try {
|
|
3064
3551
|
const invocation = hookInvocation();
|
|
3065
|
-
if (invocation === null) return;
|
|
3066
3552
|
if (invocation.dryRun) {
|
|
3067
3553
|
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3068
3554
|
return;
|
|
@@ -3078,13 +3564,15 @@ async function runHookPosttool() {
|
|
|
3078
3564
|
diag2("stdin is not a posttool event ({session_id, tool_name})");
|
|
3079
3565
|
return;
|
|
3080
3566
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3567
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3568
|
+
if (vaultRoot === null) return;
|
|
3569
|
+
if (!vaultPathTouched(event.tool_input, vaultRoot, event.cwd)) return;
|
|
3570
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3571
|
+
const findings = await validateVault(vaultRoot);
|
|
3084
3572
|
let synced = false;
|
|
3085
3573
|
if (!hasErrors(findings)) {
|
|
3086
3574
|
try {
|
|
3087
|
-
await syncVault(
|
|
3575
|
+
await syncVault(vaultRoot);
|
|
3088
3576
|
synced = true;
|
|
3089
3577
|
} catch (err) {
|
|
3090
3578
|
diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3101,7 +3589,6 @@ async function runHookPosttool() {
|
|
|
3101
3589
|
async function runHookStop() {
|
|
3102
3590
|
try {
|
|
3103
3591
|
const invocation = hookInvocation();
|
|
3104
|
-
if (invocation === null) return;
|
|
3105
3592
|
if (invocation.dryRun) {
|
|
3106
3593
|
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3107
3594
|
return;
|
|
@@ -3112,15 +3599,19 @@ async function runHookStop() {
|
|
|
3112
3599
|
return;
|
|
3113
3600
|
}
|
|
3114
3601
|
if (event.stop_hook_active === true) return;
|
|
3115
|
-
const
|
|
3602
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3603
|
+
if (vaultRoot === null) return;
|
|
3604
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3116
3605
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
3117
3606
|
if (scope === void 0) return;
|
|
3118
3607
|
const rendered = renderStop(
|
|
3119
|
-
await validateVault(
|
|
3608
|
+
await validateVault(vaultRoot),
|
|
3120
3609
|
config.builtin_hooks?.["handoff-gate"]?.reason
|
|
3121
3610
|
);
|
|
3122
3611
|
if (rendered === null) return;
|
|
3123
|
-
const fired = dedupeMatches(hookStateDir(), event.session_id, [
|
|
3612
|
+
const fired = dedupeMatches(hookStateDir(vaultRoot), event.session_id, [
|
|
3613
|
+
{ id: "handoff-gate" }
|
|
3614
|
+
]);
|
|
3124
3615
|
if (fired.length === 0) return;
|
|
3125
3616
|
console.log(rendered);
|
|
3126
3617
|
} catch (err) {
|
|
@@ -3145,7 +3636,6 @@ function renderSessionStart(scope, source, messages = {}) {
|
|
|
3145
3636
|
async function runHookSessionStart() {
|
|
3146
3637
|
try {
|
|
3147
3638
|
const invocation = hookInvocation();
|
|
3148
|
-
if (invocation === null) return;
|
|
3149
3639
|
if (invocation.dryRun) {
|
|
3150
3640
|
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3151
3641
|
return;
|
|
@@ -3155,7 +3645,9 @@ async function runHookSessionStart() {
|
|
|
3155
3645
|
diag2("stdin is not a session-start event ({session_id})");
|
|
3156
3646
|
return;
|
|
3157
3647
|
}
|
|
3158
|
-
const
|
|
3648
|
+
const vaultRoot = resolveHookVault(invocation, event.cwd);
|
|
3649
|
+
if (vaultRoot === null) return;
|
|
3650
|
+
const config = await loadVaultConfig(vaultRoot);
|
|
3159
3651
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
3160
3652
|
if (scope === void 0) return;
|
|
3161
3653
|
console.log(renderSessionStart(scope, event.source, config.builtin_hooks ?? {}));
|
|
@@ -3179,6 +3671,7 @@ var init_hook = __esm({
|
|
|
3179
3671
|
"../cli/src/hook.ts"() {
|
|
3180
3672
|
"use strict";
|
|
3181
3673
|
init_database();
|
|
3674
|
+
init_kmd_config();
|
|
3182
3675
|
init_vault_config();
|
|
3183
3676
|
init_frontmatter();
|
|
3184
3677
|
init_sync();
|
|
@@ -3208,16 +3701,33 @@ process.on("warning", (warning) => {
|
|
|
3208
3701
|
});
|
|
3209
3702
|
var USAGE = `usage: kmd <command> [options]
|
|
3210
3703
|
|
|
3704
|
+
vault resolution (every command): positional > project tier (.kmd/config.local.yaml >
|
|
3705
|
+
.kmd/config.yaml > vault/vault.yaml > vault.yaml with a .kmd marker beside it,
|
|
3706
|
+
nearest ancestor of $KMD_PROJECT_DIR or cwd) > --default-root > $WIKI_VAULT >
|
|
3707
|
+
~/.kmd/config.yaml default_vault
|
|
3708
|
+
|
|
3211
3709
|
commands:
|
|
3212
|
-
init [<dir>] [-y]
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3710
|
+
init [<dir>] [-y] [--set-default]
|
|
3711
|
+
scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y);
|
|
3712
|
+
TTY offers to record it as default_vault, --set-default records it
|
|
3713
|
+
without asking (-y alone never touches the machine default)
|
|
3714
|
+
init --local [-y] scaffold a project vault: <git-root>/vault + <git-root>/.kmd state home
|
|
3715
|
+
sync [<vault-root>] vault \u2192 index sync (runs validate first)
|
|
3716
|
+
validate [<path>] deterministic vault checker
|
|
3717
|
+
mcp [<vault-root>] [--default-root <path>]
|
|
3718
|
+
start the stdio MCP server; --default-root is the plugin form
|
|
3719
|
+
(config default the project tier may beat); flags are mutually exclusive;
|
|
3720
|
+
no positional and no $KMD_PROJECT_DIR: the vault binds after
|
|
3721
|
+
initialization, from the client's roots/list when it declares the
|
|
3722
|
+
roots capability
|
|
3723
|
+
config [<vault-root>] print resolved vault + winning source; with nothing resolvable, list known vaults
|
|
3724
|
+
config <set|get|unset> default_vault [<path>]
|
|
3725
|
+
read/write the global default in ~/.kmd/config.yaml
|
|
3726
|
+
db reset [<vault-root>] delete the vault's index
|
|
3727
|
+
hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--default-root <path>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
3219
3728
|
harness gate engine: JSON event on stdin, decision/context on stdout;
|
|
3220
|
-
|
|
3729
|
+
the vault resolves through the chain with the event cwd as project
|
|
3730
|
+
signal; posttool auto-runs validate + sync after a vault write;
|
|
3221
3731
|
stop blocks the handoff once while validate errors hold the sync
|
|
3222
3732
|
|
|
3223
3733
|
options:
|
|
@@ -3230,52 +3740,79 @@ var { positionals, values } = parseArgs3({
|
|
|
3230
3740
|
options: {
|
|
3231
3741
|
version: { type: "boolean", short: "v" },
|
|
3232
3742
|
help: { type: "boolean", short: "h" },
|
|
3233
|
-
yes: { type: "boolean", short: "y" }
|
|
3743
|
+
yes: { type: "boolean", short: "y" },
|
|
3744
|
+
"default-root": { type: "string" },
|
|
3745
|
+
"set-default": { type: "boolean" },
|
|
3746
|
+
local: { type: "boolean" }
|
|
3234
3747
|
}
|
|
3235
3748
|
});
|
|
3236
3749
|
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
3750
|
async function run() {
|
|
3244
3751
|
switch (command) {
|
|
3245
3752
|
case "init": {
|
|
3246
3753
|
const { runInit: runInit2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3247
|
-
await runInit2(
|
|
3754
|
+
await runInit2(
|
|
3755
|
+
positionals[1],
|
|
3756
|
+
Boolean(values.yes),
|
|
3757
|
+
Boolean(values.local),
|
|
3758
|
+
Boolean(values["set-default"])
|
|
3759
|
+
);
|
|
3248
3760
|
break;
|
|
3249
3761
|
}
|
|
3250
3762
|
case "sync": {
|
|
3251
3763
|
const { runSyncCommand: runSyncCommand2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3252
|
-
await runSyncCommand2();
|
|
3764
|
+
await runSyncCommand2(positionals[1]);
|
|
3253
3765
|
break;
|
|
3254
3766
|
}
|
|
3255
3767
|
case "validate": {
|
|
3256
|
-
applyVaultRoot(1);
|
|
3257
3768
|
const { runValidate: runValidate2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3258
|
-
await runValidate2();
|
|
3769
|
+
await runValidate2(positionals[1]);
|
|
3259
3770
|
break;
|
|
3260
3771
|
}
|
|
3261
3772
|
case "mcp": {
|
|
3262
|
-
|
|
3773
|
+
const positional = positionals[1];
|
|
3774
|
+
const defaultRoot = typeof values["default-root"] === "string" ? values["default-root"] : void 0;
|
|
3775
|
+
if (positional && defaultRoot !== void 0) {
|
|
3776
|
+
console.error("kmd mcp: <vault-root> and --default-root are mutually exclusive");
|
|
3777
|
+
process.exit(2);
|
|
3778
|
+
}
|
|
3263
3779
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_start(), start_exports));
|
|
3780
|
+
if (!positional && !process.env.KMD_PROJECT_DIR) {
|
|
3781
|
+
const { loadGlobalConfig: loadGlobalConfig2 } = await Promise.resolve().then(() => (init_kmd_config(), kmd_config_exports));
|
|
3782
|
+
await startMcpServer2({
|
|
3783
|
+
cwd: process.cwd(),
|
|
3784
|
+
defaultRoot,
|
|
3785
|
+
envVault: process.env.WIKI_VAULT,
|
|
3786
|
+
globalDefault: loadGlobalConfig2().default_vault
|
|
3787
|
+
});
|
|
3788
|
+
break;
|
|
3789
|
+
}
|
|
3790
|
+
const { resolveCliVault: resolveCliVault2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3791
|
+
const resolved = resolveCliVault2(positional, defaultRoot);
|
|
3792
|
+
if (resolved.root === null) {
|
|
3793
|
+
console.error(
|
|
3794
|
+
"kmd mcp: no vault resolvable \u2014 pass <vault-root>, run inside a project vault, set WIKI_VAULT, or `kmd config set default_vault <path>`"
|
|
3795
|
+
);
|
|
3796
|
+
process.exit(1);
|
|
3797
|
+
}
|
|
3798
|
+
process.env.WIKI_VAULT = resolved.root;
|
|
3264
3799
|
await startMcpServer2();
|
|
3265
3800
|
break;
|
|
3266
3801
|
}
|
|
3267
3802
|
case "config": {
|
|
3268
|
-
|
|
3269
|
-
const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3270
|
-
await
|
|
3803
|
+
const sub = positionals[1];
|
|
3804
|
+
const { runConfig: runConfig2, runConfigGet: runConfigGet2, runConfigSet: runConfigSet2, runConfigUnset: runConfigUnset2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3805
|
+
if (sub === "set") await runConfigSet2(positionals[2], positionals[3]);
|
|
3806
|
+
else if (sub === "get") await runConfigGet2(positionals[2]);
|
|
3807
|
+
else if (sub === "unset") await runConfigUnset2(positionals[2]);
|
|
3808
|
+
else await runConfig2(sub);
|
|
3271
3809
|
break;
|
|
3272
3810
|
}
|
|
3273
3811
|
case "db": {
|
|
3274
3812
|
const sub = positionals[1];
|
|
3275
3813
|
if (sub === "reset") {
|
|
3276
|
-
applyVaultRoot(2);
|
|
3277
3814
|
const { runDbReset: runDbReset2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
3278
|
-
await runDbReset2();
|
|
3815
|
+
await runDbReset2(positionals[2]);
|
|
3279
3816
|
} else {
|
|
3280
3817
|
console.error(sub ? `unknown db subcommand: ${sub}` : "usage: kmd db reset [<vault-root>]");
|
|
3281
3818
|
process.exit(2);
|
|
@@ -3311,11 +3848,11 @@ async function run() {
|
|
|
3311
3848
|
}
|
|
3312
3849
|
case "--version":
|
|
3313
3850
|
case "-v": {
|
|
3314
|
-
const { readFileSync:
|
|
3315
|
-
const { join:
|
|
3316
|
-
const { fileURLToPath } = await import("node:url");
|
|
3317
|
-
const pkgDir =
|
|
3318
|
-
const pkg = JSON.parse(
|
|
3851
|
+
const { readFileSync: readFileSync3 } = await import("node:fs");
|
|
3852
|
+
const { join: join12, dirname: dirname7 } = await import("node:path");
|
|
3853
|
+
const { fileURLToPath: fileURLToPath2 } = await import("node:url");
|
|
3854
|
+
const pkgDir = dirname7(dirname7(fileURLToPath2(import.meta.url)));
|
|
3855
|
+
const pkg = JSON.parse(readFileSync3(join12(pkgDir, "package.json"), "utf8"));
|
|
3319
3856
|
console.log(pkg.version);
|
|
3320
3857
|
break;
|
|
3321
3858
|
}
|