@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 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(vaultRoot2) {
36
+ function canonicalVaultRoot(vaultRoot) {
37
37
  try {
38
- return realpathSync(vaultRoot2);
38
+ return realpathSync(vaultRoot);
39
39
  } catch {
40
- return resolve(vaultRoot2);
40
+ return resolve(vaultRoot);
41
41
  }
42
42
  }
43
- function vaultKey(vaultRoot2) {
44
- const canonical = canonicalVaultRoot(vaultRoot2);
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 resolveIndexPath(vaultRoot2) {
49
- return join(indexRootDir(), vaultKey(vaultRoot2), "index.db");
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 join2 } from "node:path";
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 z.toJSONSchema(VaultConfigSchema, { target: "draft-7" });
298
+ return z2.toJSONSchema(VaultConfigSchema, { target: "draft-7" });
136
299
  }
137
- async function loadVaultConfig(vaultRoot2) {
138
- const path = join2(vaultRoot2, "vault.yaml");
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(parse(raw));
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 = z.strictObject({
158
- repo: z.string().optional().describe(
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: z.string().optional().describe("Must appear in the methodologies list."),
162
- status: z.string().describe("Free string; keep within statuses by convention.")
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 = z.union([
165
- z.string(),
166
- z.strictObject({
167
- name: z.string(),
168
- signal: z.string().describe("When to pick this kind."),
169
- where: z.string().describe("Path pattern pages of this kind follow.")
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 = z.union([
173
- z.string(),
174
- z.strictObject({
175
- name: z.enum(["newer-than"]),
176
- fresh: z.array(z.string().min(1)).min(1),
177
- than: z.array(z.string().min(1)).min(1)
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 = z.union([
181
- z.enum(["session", "never"]),
182
- z.strictObject({ minutes: z.number().int().positive() })
343
+ DedupSchema = z2.union([
344
+ z2.enum(["session", "never"]),
345
+ z2.strictObject({ minutes: z2.number().int().positive() })
183
346
  ]);
184
- TriggerSchema = z.strictObject({
185
- id: z.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
186
- on: z.enum(["prompt", "pretool"]),
187
- enforce: z.enum(["inject", "warn", "block"]).describe("inject: context line \xB7 warn: stderr \xB7 block: deny with reason."),
188
- keywords: z.array(z.string().min(1)).optional().describe("Word-boundary, porter-stemmed match. Prompt triggers need keywords or intent."),
189
- intent: z.array(z.string()).optional().describe("Case-insensitive regexes over the raw prompt \u2014 the stemming escape hatch."),
190
- tool: z.string().optional().describe("Exact tool name; pretool matchers AND-compose."),
191
- args_match: z.string().optional().describe("Regex over the serialized tool input."),
192
- files: z.array(z.string().min(1)).optional().describe("Globs against the paths the tool touches; pretool triggers only."),
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: z.string().optional().describe("Required for inject and warn \u2014 the line emitted."),
197
- reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads."),
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 = z.record(z.string(), z.array(TriggerSchema));
244
- BuiltinHooksSchema = z.strictObject({
245
- resync: z.strictObject({
246
- reason: z.string().min(1).optional().describe("Validate-errors preamble; the engine appends the error lines."),
247
- text: z.string().min(1).optional().describe("Sync-failed note.")
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": z.strictObject({
250
- reason: z.string().min(1).optional().describe("Stop-block preamble; the engine appends the error lines.")
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: z.strictObject({
253
- text: z.string().min(1).optional().describe("Session-start prime instruction; the engine prepends the resolved scope.")
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: z.strictObject({
256
- text: z.string().min(1).optional().describe("Post-compaction re-orientation; the engine prepends the resolved scope.")
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 = z.strictObject({
260
- scopes: z.record(z.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
261
- kinds: z.array(KindEntrySchema).describe(
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: z.array(z.string()).describe("Page status vocabulary; validate-enforced."),
265
- methodologies: z.array(z.string()).describe("Methodology vocabulary for pages and scope entries."),
266
- tags: z.strictObject({
267
- canonical: z.array(z.string()).describe("Approved tags."),
268
- aliases: z.record(z.string(), z.string()).describe("Alias \u2192 canonical; validate warns on alias use.")
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: z.string().optional().describe("Replaces the served \xA7 Authoring rules entirely \u2014 escape hatch."),
271
- authoring_rules_extra: z.string().optional().describe("Appended after the served \xA7 Authoring rules."),
272
- sync_protocol: z.string().optional().describe("Replaces the served \xA7 Resync protocol entirely \u2014 escape hatch."),
273
- sync_protocol_extra: z.string().optional().describe("Appended after the served \xA7 Resync protocol."),
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 join3, resolve as resolve2 } from "node:path";
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 = join3(root, SCHEMA_FILE);
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 = resolve2(dir);
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(join3(root, domain), { recursive: true });
721
+ await mkdir(join4(root, domain), { recursive: true });
558
722
  }
559
- await mkdir(join3(root, "templates"), { recursive: true });
723
+ await mkdir(join4(root, "templates"), { recursive: true });
560
724
  for (const [file, content] of Object.entries(VAULT_TEMPLATES)) {
561
- await writeFile(join3(root, "templates", file), content);
725
+ await writeFile(join4(root, "templates", file), content);
562
726
  }
563
727
  await refreshSchemaFile(root);
564
- await writeFile(join3(root, "vault.yaml"), SCHEMA_MODELINE + stringify(STARTER_CONFIG));
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
- return /^y(es)?$/i.test(answer.trim());
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
- async function runInit(dir, yes = false) {
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 ${resolve2(".")}? [y/N] `);
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
- export WIKI_VAULT=${root}
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 join4, relative, sep } from "node:path";
638
- import { z as z2 } from "zod";
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(join4(dir, entry.name));
905
+ await recurse(join5(dir, entry.name));
659
906
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
660
- out.push(join4(dir, entry.name));
907
+ out.push(join5(dir, entry.name));
661
908
  }
662
909
  }
663
910
  }
664
- await recurse(join4(root, domain));
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(vaultRoot2) {
787
- const dbPath = resolveIndexPath(vaultRoot2);
788
- const vaultConfig = await loadVaultConfig(vaultRoot2);
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
- mkdirSync(dirname(dbPath), { recursive: true });
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(vaultRoot2, domain));
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(vaultRoot2, file);
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(vaultRoot2));
1071
+ setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot));
825
1072
  setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
826
- if (await refreshSchemaFile(vaultRoot2)) {
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 = z2.object({
861
- WIKI_VAULT: z2.string().min(1)
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 join5 } from "node:path";
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(join5(root, file));
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 dirname2, join as join6 } from "node:path";
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 vaultRoot() {
1349
- const root = process.env.WIKI_VAULT;
1350
- if (!root) {
1351
- console.error("WIKI_VAULT is not set");
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
- return root;
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 findings = await validateVault(vaultRoot());
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 findings = await validateVault(vaultRoot());
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 (existsSync(index)) {
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 (!existsSync(root)) return [];
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 = join6(root, entry.name, "index.db");
1403
- if (!existsSync(index)) continue;
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 root = process.env.WIKI_VAULT;
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 runDbReset() {
1434
- const root = process.env.WIKI_VAULT;
1435
- if (!root) {
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 = dirname2(resolveIndexPath(root));
1440
- if (!existsSync(dir)) {
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 z3 } from "zod";
1471
- function loadConfig(env = process.env) {
1472
- const parsed = EnvSchema2.safeParse(env);
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: parsed.data.WIKI_VAULT,
1480
- logLevel: parsed.data.LOG_LEVEL,
1481
- serverName: parsed.data.SERVER_NAME,
1482
- serverVersion: parsed.data.SERVER_VERSION
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
- var EnvSchema2;
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
- EnvSchema2 = z3.object({
1490
- WIKI_VAULT: z3.string().min(1).describe("Absolute path to the Obsidian vault root"),
1491
- LOG_LEVEL: z3.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."),
1492
- SERVER_NAME: z3.string().default("wiki-mcp"),
1493
- SERVER_VERSION: z3.string().default("0.0.0")
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 mkdirSync2 } from "node:fs";
1500
- import { dirname as dirname3 } from "node:path";
1501
- function createDatabase(vaultRoot2) {
1502
- const dbPath = resolveIndexPath(vaultRoot2);
1503
- mkdirSync2(dirname3(dbPath), { recursive: true });
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(vaultRoot2));
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 mkdirSync3 } from "node:fs";
1517
- import { homedir as homedir2 } from "node:os";
1518
- import { join as join7 } from "node:path";
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 = join7(homedir2(), ".local", "state", "wiki-mcp");
1533
- DIAG_LOG_PATH = join7(DIAG_DIR, "server.log");
1899
+ DIAG_DIR = join8(homedir3(), ".local", "state", "wiki-mcp");
1900
+ DIAG_LOG_PATH = join8(DIAG_DIR, "server.log");
1534
1901
  try {
1535
- mkdirSync3(DIAG_DIR, { recursive: true });
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, vaultRoot2, vaultConfig) {
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(vaultRoot2)}\` \u2014 every page path below is relative to it; write files and run \`kmd validate\` / \`kmd sync\` against it.`,
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 join8 } from "node:path";
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 registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
1790
- const dir = join8(vaultRoot2, "templates");
1791
- const templates = [...TEMPLATES, ...customTemplates(vaultConfig)];
1792
- for (const tmpl of templates) {
1793
- mcp.registerResource(
1794
- tmpl.name,
1795
- tmpl.uri,
1796
- { description: tmpl.description, mimeType: "text/markdown" },
1797
- async (uri) => {
1798
- let text;
1799
- try {
1800
- text = await readFile5(join8(dir, tmpl.file), "utf8");
1801
- } catch (err) {
1802
- throw new Error(`template file missing: templates/${tmpl.file} (${tmpl.uri})`, {
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 templates) {
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
- const indexText = indexLines.join("\n");
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
- contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: indexText }]
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 z4 } from "zod";
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 = z4.object({
2028
- query: z4.string().min(1).describe(
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: z4.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
2032
- kind: z4.string().optional().describe(
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: z4.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
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 join9 } from "node:path";
2044
- import { z as z5 } from "zod";
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(vaultRoot2, scope) {
2436
+ async function readIndexFm(vaultRoot, scope) {
2049
2437
  try {
2050
- const raw = await readFile6(join9(vaultRoot2, "projects", scope, "index.md"), "utf8");
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(vaultRoot2, scope) {
2444
+ async function readPrimer(vaultRoot, scope) {
2057
2445
  try {
2058
- const raw = await readFile6(join9(vaultRoot2, "projects", scope, "primer.md"), "utf8");
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: vaultRoot2, vaultConfig } = deps;
2453
+ const { db, vaultRoot, vaultConfig } = deps;
2066
2454
  const { scope, task } = input;
2067
2455
  const [fm, primer] = await Promise.all([
2068
- readIndexFm(vaultRoot2, scope),
2069
- readPrimer(vaultRoot2, scope)
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(vaultRoot2),
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 = z5.object({
2254
- scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
2255
- task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
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, vaultRoot: vaultRoot2, db, logger, vaultConfig } = args;
2264
- const mcp = new McpServer({ name, version }, { capabilities: { tools: {}, resources: {} } });
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
- return handlePrime({ db, vaultRoot: vaultRoot2, vaultConfig }, input);
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, vaultRoot2, vaultConfig);
2284
- registerAuthoringResource(mcp, vaultRoot2, vaultConfig);
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
- async function startMcpServer() {
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
- const shutdown = async (signal) => {
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 mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
2391
- import { homedir as homedir3 } from "node:os";
2392
- import { join as join10, resolve as resolve3, sep as sep2 } from "node:path";
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 z6 } from "zod";
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 = z6.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
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 expandHome(path) {
2447
- if (path === "~") return homedir3();
2448
- return path.startsWith("~/") ? join10(homedir3(), path.slice(2)) : path;
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 = expandHome(scope.repo).replace(/\/+$/, "");
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, vaultRoot2) {
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, vaultRoot2);
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, vaultRoot2) {
3085
+ function evaluateWhenVerdict(when, vaultRoot) {
2620
3086
  if (typeof when === "string") return "unknown";
2621
3087
  try {
2622
- const than = newestUpdated(vaultRoot2, when.than);
3088
+ const than = newestUpdated(vaultRoot, when.than);
2623
3089
  if (than === null) return "vacuous";
2624
- const fresh = newestUpdated(vaultRoot2, when.fresh);
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, vaultRoot2) {
2632
- const verdict = evaluateWhenVerdict(when, vaultRoot2);
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(vaultRoot2, globs) {
3102
+ function newestUpdated(vaultRoot, globs) {
2637
3103
  const regexes = globs.map(globToRegExp);
2638
3104
  let newest = null;
2639
- for (const entry of readdirSync2(vaultRoot2, { recursive: true })) {
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(join10(vaultRoot2, entry));
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(readFileSync(path, "utf8"));
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, vaultRoot2, cwd) {
2717
- const root = resolve3(vaultRoot2);
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 = resolve3(cwd ?? ".", candidate);
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 join10(kmdHome(), "state", "hook");
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(join10(options.stateDir, safeName(options.sessionId)));
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(join10(options.stateDir, safeName(options.sessionId)));
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 = join10(stateDir, safeName(sessionId));
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
- mkdirSync4(dir, { recursive: true });
3339
+ mkdirSync5(dir, { recursive: true });
2874
3340
  for (const key of record) {
2875
3341
  try {
2876
- writeFileSync(join10(dir, key), "", { flag: "wx" });
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 = join10(stateDir, entry);
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
- vaultRoot: vaultRoot2,
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 config = await loadVaultConfig(vaultRoot2);
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 config = await loadVaultConfig(vaultRoot2);
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: vaultRoot2,
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, vaultRoot2);
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
- if (!vaultPathTouched(event.tool_input, invocation.vaultRoot, event.cwd)) return;
3082
- const config = await loadVaultConfig(invocation.vaultRoot);
3083
- const findings = await validateVault(invocation.vaultRoot);
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(invocation.vaultRoot);
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 config = await loadVaultConfig(invocation.vaultRoot);
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(invocation.vaultRoot),
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, [{ id: "handoff-gate" }]);
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 config = await loadVaultConfig(invocation.vaultRoot);
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] scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y)
3213
- sync vault \u2192 index sync (runs validate first)
3214
- validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
3215
- mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
3216
- config [<vault-root>] print vault + index resolution; with no vault, list known vaults
3217
- db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
3218
- hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
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
- posttool auto-runs validate + sync after a vault write;
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(positionals[1], Boolean(values.yes));
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
- applyVaultRoot(1);
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
- applyVaultRoot(1);
3269
- const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
3270
- await runConfig2();
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: readFileSync2 } = await import("node:fs");
3315
- const { join: join11, dirname: dirname4 } = await import("node:path");
3316
- const { fileURLToPath } = await import("node:url");
3317
- const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
3318
- const pkg = JSON.parse(readFileSync2(join11(pkgDir, "package.json"), "utf8"));
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
  }