@bartolli/kmd 0.10.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,36 +403,42 @@ 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.")
414
+ }).optional(),
415
+ orient: z2.strictObject({
416
+ text: z2.string().min(1).optional().describe("Session-start prime instruction; the engine prepends the resolved scope.")
417
+ }).optional(),
418
+ reorient: z2.strictObject({
419
+ text: z2.string().min(1).optional().describe("Post-compaction re-orientation; the engine prepends the resolved scope.")
251
420
  }).optional()
252
421
  });
253
- VaultConfigSchema = z.strictObject({
254
- scopes: z.record(z.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
255
- 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(
256
425
  "Page kind vocabulary; validate-enforced. Object form adds a kind-selector row to wiki://authoring."
257
426
  ),
258
- statuses: z.array(z.string()).describe("Page status vocabulary; validate-enforced."),
259
- methodologies: z.array(z.string()).describe("Methodology vocabulary for pages and scope entries."),
260
- tags: z.strictObject({
261
- canonical: z.array(z.string()).describe("Approved tags."),
262
- 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.")
263
432
  }),
264
- authoring_rules: z.string().optional().describe("Replaces the served \xA7 Authoring rules entirely \u2014 escape hatch."),
265
- authoring_rules_extra: z.string().optional().describe("Appended after the served \xA7 Authoring rules."),
266
- sync_protocol: z.string().optional().describe("Replaces the served \xA7 Resync protocol entirely \u2014 escape hatch."),
267
- 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."),
268
437
  triggers: TriggersSchema.optional().describe(
269
438
  'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
270
439
  ),
271
440
  builtin_hooks: BuiltinHooksSchema.optional().describe(
272
- "Message overrides for the fixed-function hooks (resync, handoff-gate) by public id."
441
+ "Message overrides for the fixed-function hooks (resync, handoff-gate, orient, reorient) by public id."
273
442
  ),
274
443
  triggers_extra: TriggersSchema.optional().describe(
275
444
  'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
@@ -515,11 +684,12 @@ and must match current code at every commit.
515
684
  });
516
685
 
517
686
  // ../cli/src/init.ts
687
+ import { existsSync as existsSync3 } from "node:fs";
518
688
  import { mkdir, readdir, readFile as readFile2, writeFile } from "node:fs/promises";
519
- import { join as join3, resolve as resolve2 } from "node:path";
689
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
520
690
  import { stringify } from "yaml";
521
691
  async function refreshSchemaFile(root) {
522
- const path = join3(root, SCHEMA_FILE);
692
+ const path = join4(root, SCHEMA_FILE);
523
693
  const next = `${JSON.stringify(configJsonSchema(), null, 2)}
524
694
  `;
525
695
  try {
@@ -530,7 +700,7 @@ async function refreshSchemaFile(root) {
530
700
  return true;
531
701
  }
532
702
  async function scaffoldVault(dir) {
533
- const root = resolve2(dir);
703
+ const root = resolve3(dir);
534
704
  let entries = [];
535
705
  try {
536
706
  entries = await readdir(root);
@@ -548,33 +718,97 @@ async function scaffoldVault(dir) {
548
718
  );
549
719
  }
550
720
  for (const domain of DOMAIN_DIRS) {
551
- await mkdir(join3(root, domain), { recursive: true });
721
+ await mkdir(join4(root, domain), { recursive: true });
552
722
  }
553
- await mkdir(join3(root, "templates"), { recursive: true });
723
+ await mkdir(join4(root, "templates"), { recursive: true });
554
724
  for (const [file, content] of Object.entries(VAULT_TEMPLATES)) {
555
- await writeFile(join3(root, "templates", file), content);
725
+ await writeFile(join4(root, "templates", file), content);
556
726
  }
557
727
  await refreshSchemaFile(root);
558
- await writeFile(join3(root, "vault.yaml"), SCHEMA_MODELINE + stringify(STARTER_CONFIG));
728
+ await writeFile(join4(root, "vault.yaml"), SCHEMA_MODELINE + stringify(STARTER_CONFIG));
559
729
  return root;
560
730
  }
561
- async function promptYesNo(question, input = process.stdin, output = process.stderr) {
731
+ async function promptYesNo(question, input = process.stdin, output = process.stderr, defaultYes = false) {
562
732
  const { createInterface } = await import("node:readline/promises");
563
733
  const rl = createInterface({ input, output });
564
734
  try {
565
- const answer = await rl.question(question);
566
- 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);
567
738
  } finally {
568
739
  rl.close();
569
740
  }
570
741
  }
571
- 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
+ }
572
806
  let target = dir;
573
807
  if (!target) {
574
808
  if (yes) {
575
809
  target = ".";
576
810
  } else if (process.stdin.isTTY) {
577
- const ok = await promptYesNo(`initialize a vault in ${resolve2(".")}? [y/N] `);
811
+ const ok = await promptYesNo(`initialize a vault in ${resolve3(".")}? [y/N] `);
578
812
  if (!ok) {
579
813
  console.error("init: aborted");
580
814
  process.exit(1);
@@ -598,16 +832,34 @@ async function runInit(dir, yes = false) {
598
832
  vault.yaml starter vocabulary \u2014 add your first scope under scopes:
599
833
  vault.schema.json IDE validation via the yaml-language-server modeline
600
834
  templates/ ${templateCount} built-in templates (served at wiki://template/...)
601
- projects/ research/ notes/
602
-
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}
603
849
  next steps:
604
- export WIKI_VAULT=${root}
850
+ kmd mcp # stdio MCP server (prime, search) \u2014 resolves the default`);
851
+ } else {
852
+ console.log(`
853
+ next steps:
854
+ kmd config set default_vault ${root} # make it the machine default
605
855
  kmd mcp ${root} # stdio MCP server (prime, search)`);
856
+ }
606
857
  }
607
- var SCHEMA_FILE, SCHEMA_MODELINE, STARTER_CONFIG, DOMAIN_DIRS;
858
+ var SCHEMA_FILE, SCHEMA_MODELINE, STARTER_CONFIG, DOMAIN_DIRS, TIER_GITIGNORE;
608
859
  var init_init = __esm({
609
860
  "../cli/src/init.ts"() {
610
861
  "use strict";
862
+ init_kmd_config();
611
863
  init_vault_config();
612
864
  init_init_templates();
613
865
  SCHEMA_FILE = "vault.schema.json";
@@ -621,15 +873,16 @@ var init_init = __esm({
621
873
  tags: { canonical: [], aliases: {} }
622
874
  };
623
875
  DOMAIN_DIRS = ["projects", "research", "notes"];
876
+ TIER_GITIGNORE = "db/\nstate/\nconfig.local.yaml\n";
624
877
  }
625
878
  });
626
879
 
627
880
  // ../cli/src/sync.ts
628
881
  import { createHash as createHash2 } from "node:crypto";
629
- import { mkdirSync } from "node:fs";
882
+ import { mkdirSync as mkdirSync2 } from "node:fs";
630
883
  import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
631
- import { dirname, join as join4, relative, sep } from "node:path";
632
- 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";
633
886
  function loadEnv() {
634
887
  const parsed = EnvSchema.safeParse({
635
888
  WIKI_VAULT: process.env.WIKI_VAULT
@@ -649,13 +902,13 @@ async function walkMarkdown(root, domain) {
649
902
  for (const entry of entries) {
650
903
  if (entry.name.startsWith(".")) continue;
651
904
  if (entry.isDirectory()) {
652
- await recurse(join4(dir, entry.name));
905
+ await recurse(join5(dir, entry.name));
653
906
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
654
- out.push(join4(dir, entry.name));
907
+ out.push(join5(dir, entry.name));
655
908
  }
656
909
  }
657
910
  }
658
- await recurse(join4(root, domain));
911
+ await recurse(join5(root, domain));
659
912
  return out;
660
913
  }
661
914
  function toRelativePath(root, absolute) {
@@ -777,23 +1030,23 @@ function syncPage(db, fields) {
777
1030
  }
778
1031
  return "changed";
779
1032
  }
780
- async function syncVault(vaultRoot2) {
781
- const dbPath = resolveIndexPath(vaultRoot2);
782
- const vaultConfig = await loadVaultConfig(vaultRoot2);
1033
+ async function syncVault(vaultRoot) {
1034
+ const dbPath = resolveIndexPath(vaultRoot);
1035
+ const vaultConfig = await loadVaultConfig(vaultRoot);
783
1036
  const scopes = new Set(Object.keys(vaultConfig.scopes));
784
- mkdirSync(dirname(dbPath), { recursive: true });
1037
+ mkdirSync2(dirname4(dbPath), { recursive: true });
785
1038
  const db = openDatabase(dbPath);
786
1039
  try {
787
1040
  const files = [];
788
1041
  for (const domain of SCAN_DOMAINS) {
789
- files.push(...await walkMarkdown(vaultRoot2, domain));
1042
+ files.push(...await walkMarkdown(vaultRoot, domain));
790
1043
  }
791
1044
  const indexedPaths = [];
792
1045
  let changed = 0;
793
1046
  let unchanged = 0;
794
1047
  let skipped = 0;
795
1048
  for (const file of files) {
796
- const path = toRelativePath(vaultRoot2, file);
1049
+ const path = toRelativePath(vaultRoot, file);
797
1050
  const raw = await readFile3(file, "utf8");
798
1051
  const parsed = parseFrontmatter(raw);
799
1052
  const fields = buildPageFields(path, raw, parsed, scopes);
@@ -815,9 +1068,9 @@ async function syncVault(vaultRoot2) {
815
1068
  const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
816
1069
  const linksDeleted = Number(linkResult.changes);
817
1070
  db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
818
- setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
1071
+ setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot));
819
1072
  setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
820
- if (await refreshSchemaFile(vaultRoot2)) {
1073
+ if (await refreshSchemaFile(vaultRoot)) {
821
1074
  console.error("sync: vault.schema.json refreshed to the running engine");
822
1075
  }
823
1076
  return {
@@ -851,8 +1104,8 @@ var init_sync = __esm({
851
1104
  init_vault_config();
852
1105
  init_frontmatter();
853
1106
  init_init();
854
- EnvSchema = z2.object({
855
- WIKI_VAULT: z2.string().min(1)
1107
+ EnvSchema = z3.object({
1108
+ WIKI_VAULT: z3.string().min(1)
856
1109
  });
857
1110
  SCAN_DOMAINS = ["projects", "research", "notes"];
858
1111
  WIKILINK_RE = /\[\[([^\]|#^]+)(?:[#^][^\]|]*)?(?:\|([^\]]+))?\]\]/g;
@@ -871,7 +1124,7 @@ var init_sync = __esm({
871
1124
 
872
1125
  // ../cli/src/validate.ts
873
1126
  import { readFile as readFile4, stat } from "node:fs/promises";
874
- import { join as join5 } from "node:path";
1127
+ import { join as join6 } from "node:path";
875
1128
  function hasIndexableTitle(data) {
876
1129
  return typeof data.title === "string" && data.title.trim() !== "";
877
1130
  }
@@ -1243,7 +1496,7 @@ async function validateVault(root) {
1243
1496
  for (const name of customKindNames(cfg)) {
1244
1497
  const file = `templates/${name}.md`;
1245
1498
  try {
1246
- await stat(join5(root, file));
1499
+ await stat(join6(root, file));
1247
1500
  } catch {
1248
1501
  findings.push({
1249
1502
  path: file,
@@ -1315,15 +1568,18 @@ var cli_exports = {};
1315
1568
  __export(cli_exports, {
1316
1569
  main: () => main,
1317
1570
  resolveCli: () => resolveCli,
1571
+ resolveCliVault: () => resolveCliVault,
1318
1572
  runConfig: () => runConfig,
1573
+ runConfigGet: () => runConfigGet,
1574
+ runConfigSet: () => runConfigSet,
1575
+ runConfigUnset: () => runConfigUnset,
1319
1576
  runDbReset: () => runDbReset,
1320
1577
  runInit: () => runInit,
1321
1578
  runSyncCommand: () => runSyncCommand,
1322
- runValidate: () => runValidate,
1323
- vaultRoot: () => vaultRoot
1579
+ runValidate: () => runValidate
1324
1580
  });
1325
- import { existsSync, readdirSync, rmSync } from "node:fs";
1326
- 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";
1327
1583
  import { parseArgs } from "node:util";
1328
1584
  function resolveCli(argv) {
1329
1585
  const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
@@ -1339,27 +1595,42 @@ function resolveCli(argv) {
1339
1595
  }
1340
1596
  return { kind: "error", message: `unknown command: ${command2}` };
1341
1597
  }
1342
- function vaultRoot() {
1343
- const root = process.env.WIKI_VAULT;
1344
- if (!root) {
1345
- 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);
1346
1614
  process.exit(1);
1347
1615
  }
1348
- return root;
1616
+ process.env.WIKI_VAULT = resolution.root;
1617
+ return { root: resolution.root, resolution };
1349
1618
  }
1350
1619
  function reportFindings(findings) {
1351
1620
  for (const f of findings) {
1352
1621
  console.error(`${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
1353
1622
  }
1354
1623
  }
1355
- async function runValidate() {
1356
- const findings = await validateVault(vaultRoot());
1624
+ async function runValidate(positional) {
1625
+ const { root } = requireCliVault(positional);
1626
+ const findings = await validateVault(root);
1357
1627
  reportFindings(findings);
1358
1628
  console.log(`validate: ${findings.length} finding(s)`);
1359
1629
  process.exit(hasErrors(findings) ? 1 : 0);
1360
1630
  }
1361
- async function runSyncCommand() {
1362
- const findings = await validateVault(vaultRoot());
1631
+ async function runSyncCommand(positional) {
1632
+ const { root } = requireCliVault(positional);
1633
+ const findings = await validateVault(root);
1363
1634
  reportFindings(findings);
1364
1635
  if (hasErrors(findings)) {
1365
1636
  const errors = findings.filter((f) => f.severity === "error").length;
@@ -1372,7 +1643,7 @@ function describeVault(root) {
1372
1643
  const vault = canonicalVaultRoot(root);
1373
1644
  const index = resolveIndexPath(vault);
1374
1645
  let synced = "never";
1375
- if (existsSync(index)) {
1646
+ if (existsSync4(index)) {
1376
1647
  const db = openDatabase(index);
1377
1648
  try {
1378
1649
  synced = getMeta(db, "last_synced") ?? "never";
@@ -1389,12 +1660,12 @@ function printVault(d) {
1389
1660
  }
1390
1661
  function knownVaults() {
1391
1662
  const root = indexRootDir();
1392
- if (!existsSync(root)) return [];
1663
+ if (!existsSync4(root)) return [];
1393
1664
  const known = [];
1394
1665
  for (const entry of readdirSync(root, { withFileTypes: true })) {
1395
1666
  if (!entry.isDirectory()) continue;
1396
- const index = join6(root, entry.name, "index.db");
1397
- if (!existsSync(index)) continue;
1667
+ const index = join7(root, entry.name, "index.db");
1668
+ if (!existsSync4(index)) continue;
1398
1669
  const db = openDatabase(index);
1399
1670
  try {
1400
1671
  const vault = getMeta(db, "vault_root");
@@ -1406,17 +1677,16 @@ function knownVaults() {
1406
1677
  }
1407
1678
  return known;
1408
1679
  }
1409
- async function runConfig() {
1410
- const root = process.env.WIKI_VAULT;
1411
- if (root) {
1412
- 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]}`);
1413
1685
  return;
1414
1686
  }
1415
1687
  const known = knownVaults();
1416
1688
  if (known.length === 0) {
1417
- console.error(
1418
- "no vault specified and none known \u2014 pass a vault root, set WIKI_VAULT, or run `kmd sync <vault-root>` once"
1419
- );
1689
+ console.error(NO_VAULT_HINT);
1420
1690
  process.exit(1);
1421
1691
  }
1422
1692
  known.forEach((d, i) => {
@@ -1424,14 +1694,44 @@ async function runConfig() {
1424
1694
  printVault(d);
1425
1695
  });
1426
1696
  }
1427
- async function runDbReset() {
1428
- const root = process.env.WIKI_VAULT;
1429
- 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) {
1430
1730
  console.error("usage: kmd db reset [<vault-root>] (or set WIKI_VAULT)");
1431
1731
  process.exit(2);
1432
1732
  }
1433
- const dir = dirname2(resolveIndexPath(root));
1434
- if (!existsSync(dir)) {
1733
+ const dir = dirname5(resolveIndexPath(resolution.root));
1734
+ if (!existsSync4(dir)) {
1435
1735
  console.log(`${dir} does not exist \u2014 nothing to reset`);
1436
1736
  return;
1437
1737
  }
@@ -1450,53 +1750,126 @@ async function main() {
1450
1750
  await runValidate();
1451
1751
  }
1452
1752
  }
1753
+ var SOURCE_LABELS, NO_VAULT_HINT;
1453
1754
  var init_cli = __esm({
1454
1755
  "../cli/src/cli.ts"() {
1455
1756
  "use strict";
1456
1757
  init_database();
1758
+ init_kmd_config();
1759
+ init_vault_config();
1457
1760
  init_sync();
1458
1761
  init_validate();
1459
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();
1460
1819
  }
1461
1820
  });
1462
1821
 
1463
1822
  // ../mcp/src/config.ts
1464
- import { z as z3 } from "zod";
1465
- function loadConfig(env = process.env) {
1466
- const parsed = EnvSchema2.safeParse(env);
1823
+ import { z as z4 } from "zod";
1824
+ function parseEnv(schema, env) {
1825
+ const parsed = schema.safeParse(env);
1467
1826
  if (!parsed.success) {
1468
1827
  const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1469
1828
  throw new Error(`Invalid environment configuration:
1470
1829
  ${issues}`);
1471
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);
1472
1844
  return {
1473
- wikiVault: parsed.data.WIKI_VAULT,
1474
- logLevel: parsed.data.LOG_LEVEL,
1475
- serverName: parsed.data.SERVER_NAME,
1476
- serverVersion: parsed.data.SERVER_VERSION
1845
+ logLevel: data.LOG_LEVEL,
1846
+ serverName: data.SERVER_NAME,
1847
+ serverVersion: data.SERVER_VERSION
1477
1848
  };
1478
1849
  }
1479
- var EnvSchema2;
1850
+ var BaseEnvSchema, EnvSchema2;
1480
1851
  var init_config = __esm({
1481
1852
  "../mcp/src/config.ts"() {
1482
1853
  "use strict";
1483
- EnvSchema2 = z3.object({
1484
- WIKI_VAULT: z3.string().min(1).describe("Absolute path to the Obsidian vault root"),
1485
- 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."),
1486
- SERVER_NAME: z3.string().default("wiki-mcp"),
1487
- 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")
1488
1861
  });
1489
1862
  }
1490
1863
  });
1491
1864
 
1492
1865
  // ../mcp/src/db.ts
1493
- import { mkdirSync as mkdirSync2 } from "node:fs";
1494
- import { dirname as dirname3 } from "node:path";
1495
- function createDatabase(vaultRoot2) {
1496
- const dbPath = resolveIndexPath(vaultRoot2);
1497
- 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 });
1498
1871
  const db = openDatabase(dbPath);
1499
- setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
1872
+ setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot));
1500
1873
  return db;
1501
1874
  }
1502
1875
  var init_db = __esm({
@@ -1507,9 +1880,9 @@ var init_db = __esm({
1507
1880
  });
1508
1881
 
1509
1882
  // ../mcp/src/lib/diag.ts
1510
- import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
1511
- import { homedir as homedir2 } from "node:os";
1512
- 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";
1513
1886
  function diag(msg, data) {
1514
1887
  try {
1515
1888
  const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
@@ -1523,10 +1896,10 @@ var DIAG_DIR, DIAG_LOG_PATH;
1523
1896
  var init_diag = __esm({
1524
1897
  "../mcp/src/lib/diag.ts"() {
1525
1898
  "use strict";
1526
- DIAG_DIR = join7(homedir2(), ".local", "state", "wiki-mcp");
1527
- 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");
1528
1901
  try {
1529
- mkdirSync3(DIAG_DIR, { recursive: true });
1902
+ mkdirSync4(DIAG_DIR, { recursive: true });
1530
1903
  } catch {
1531
1904
  }
1532
1905
  }
@@ -1605,7 +1978,7 @@ function buildVocabulary(config) {
1605
1978
  }
1606
1979
  return lines.join("\n");
1607
1980
  }
1608
- function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
1981
+ function registerAuthoringResource(mcp, binding) {
1609
1982
  mcp.registerResource(
1610
1983
  "Authoring guide",
1611
1984
  "wiki://authoring",
@@ -1614,10 +1987,11 @@ function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
1614
1987
  mimeType: "text/markdown"
1615
1988
  },
1616
1989
  async (uri) => {
1990
+ const { vaultRoot, vaultConfig } = await binding;
1617
1991
  const sections = [
1618
1992
  "# Wiki authoring guide",
1619
1993
  "",
1620
- `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.`,
1621
1995
  "",
1622
1996
  buildKindSelector(vaultConfig.kinds),
1623
1997
  "",
@@ -1766,7 +2140,7 @@ var init_authoring = __esm({
1766
2140
 
1767
2141
  // ../mcp/src/resources/templates.ts
1768
2142
  import { readFile as readFile5 } from "node:fs/promises";
1769
- import { join as join8 } from "node:path";
2143
+ import { join as join9 } from "node:path";
1770
2144
  function customTemplates(config) {
1771
2145
  const specs = [];
1772
2146
  for (const entry of config.kinds) {
@@ -1780,41 +2154,52 @@ function customTemplates(config) {
1780
2154
  }
1781
2155
  return specs;
1782
2156
  }
1783
- function registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
1784
- const dir = join8(vaultRoot2, "templates");
1785
- const templates = [...TEMPLATES, ...customTemplates(vaultConfig)];
1786
- for (const tmpl of templates) {
1787
- mcp.registerResource(
1788
- tmpl.name,
1789
- tmpl.uri,
1790
- { description: tmpl.description, mimeType: "text/markdown" },
1791
- async (uri) => {
1792
- let text;
1793
- try {
1794
- text = await readFile5(join8(dir, tmpl.file), "utf8");
1795
- } catch (err) {
1796
- throw new Error(`template file missing: templates/${tmpl.file} (${tmpl.uri})`, {
1797
- cause: err
1798
- });
1799
- }
1800
- return {
1801
- contents: [
1802
- {
1803
- uri: uri.toString(),
1804
- mimeType: "text/markdown",
1805
- text
1806
- }
1807
- ]
1808
- };
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
+ });
1809
2171
  }
1810
- );
1811
- }
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) {
1812
2185
  const indexLines = ["# Wiki Templates", ""];
1813
- for (const tmpl of templates) {
2186
+ for (const tmpl of [...TEMPLATES, ...customTemplates(vaultConfig)]) {
1814
2187
  indexLines.push(`- **${tmpl.name}** \u2014 \`${tmpl.uri}\` `);
1815
2188
  indexLines.push(` ${tmpl.description}`);
1816
2189
  }
1817
- 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);
1818
2203
  mcp.registerResource(
1819
2204
  "Template index",
1820
2205
  "wiki://templates",
@@ -1822,9 +2207,18 @@ function registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
1822
2207
  description: "Index of all wiki templates with URIs and descriptions. Read a specific template via its URI.",
1823
2208
  mimeType: "text/markdown"
1824
2209
  },
1825
- async (uri) => ({
1826
- contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: indexText }]
1827
- })
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
+ }
1828
2222
  );
1829
2223
  }
1830
2224
  var TEMPLATES;
@@ -1971,7 +2365,7 @@ var init_toolResponse = __esm({
1971
2365
  });
1972
2366
 
1973
2367
  // ../mcp/src/tools/search.ts
1974
- import { z as z4 } from "zod";
2368
+ import { z as z5 } from "zod";
1975
2369
  function search(deps, input) {
1976
2370
  const ftsQuery = sanitizeFtsQuery(input.query);
1977
2371
  if (!ftsQuery) return { results: [] };
@@ -2018,15 +2412,15 @@ var init_search = __esm({
2018
2412
  "use strict";
2019
2413
  init_fts();
2020
2414
  init_toolResponse();
2021
- SearchInputSchema = z4.object({
2022
- query: z4.string().min(1).describe(
2415
+ SearchInputSchema = z5.object({
2416
+ query: z5.string().min(1).describe(
2023
2417
  "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
2024
2418
  ),
2025
- scope: z4.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
2026
- 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(
2027
2421
  "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
2028
2422
  ),
2029
- 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.")
2030
2424
  });
2031
2425
  FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
2032
2426
  }
@@ -2034,33 +2428,33 @@ var init_search = __esm({
2034
2428
 
2035
2429
  // ../mcp/src/tools/prime.ts
2036
2430
  import { readFile as readFile6 } from "node:fs/promises";
2037
- import { basename as basename3, join as join9 } from "node:path";
2038
- import { z as z5 } from "zod";
2431
+ import { basename as basename3, join as join10 } from "node:path";
2432
+ import { z as z6 } from "zod";
2039
2433
  function pathSlug(p) {
2040
2434
  return basename3(p).replace(/\.md$/, "");
2041
2435
  }
2042
- async function readIndexFm(vaultRoot2, scope) {
2436
+ async function readIndexFm(vaultRoot, scope) {
2043
2437
  try {
2044
- const raw = await readFile6(join9(vaultRoot2, "projects", scope, "index.md"), "utf8");
2438
+ const raw = await readFile6(join10(vaultRoot, "projects", scope, "index.md"), "utf8");
2045
2439
  return parseFrontmatter2(raw).data;
2046
2440
  } catch {
2047
2441
  return {};
2048
2442
  }
2049
2443
  }
2050
- async function readPrimer(vaultRoot2, scope) {
2444
+ async function readPrimer(vaultRoot, scope) {
2051
2445
  try {
2052
- const raw = await readFile6(join9(vaultRoot2, "projects", scope, "primer.md"), "utf8");
2446
+ const raw = await readFile6(join10(vaultRoot, "projects", scope, "primer.md"), "utf8");
2053
2447
  return parseFrontmatter2(raw).content.trim().replace(/^#\s+[^\n]+\n+/, "");
2054
2448
  } catch {
2055
2449
  return "";
2056
2450
  }
2057
2451
  }
2058
2452
  async function prime(deps, input) {
2059
- const { db, vaultRoot: vaultRoot2, vaultConfig } = deps;
2453
+ const { db, vaultRoot, vaultConfig } = deps;
2060
2454
  const { scope, task } = input;
2061
2455
  const [fm, primer] = await Promise.all([
2062
- readIndexFm(vaultRoot2, scope),
2063
- readPrimer(vaultRoot2, scope)
2456
+ readIndexFm(vaultRoot, scope),
2457
+ readPrimer(vaultRoot, scope)
2064
2458
  ]);
2065
2459
  const counts = db.prepare("SELECT kind, count(*) AS count FROM pages WHERE scope = ? GROUP BY kind").all(scope);
2066
2460
  const adrs = db.prepare(
@@ -2114,7 +2508,7 @@ async function prime(deps, input) {
2114
2508
  for (const row of counts) countsRecord[row.kind] = Number(row.count);
2115
2509
  const data = {
2116
2510
  scope,
2117
- vault_root: canonicalVaultRoot(vaultRoot2),
2511
+ vault_root: canonicalVaultRoot(vaultRoot),
2118
2512
  title: fm.title ?? null,
2119
2513
  methodology: fm.methodology ?? null,
2120
2514
  phase: typeof fm.phase === "number" ? fm.phase : null,
@@ -2244,9 +2638,9 @@ var init_prime = __esm({
2244
2638
  init_fts();
2245
2639
  init_toolResponse();
2246
2640
  init_search();
2247
- PrimeInputSchema = z5.object({
2248
- scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
2249
- 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.")
2250
2644
  });
2251
2645
  }
2252
2646
  });
@@ -2254,15 +2648,20 @@ var init_prime = __esm({
2254
2648
  // ../mcp/src/server.ts
2255
2649
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2256
2650
  function buildServer(args) {
2257
- const { name, version, vaultRoot: vaultRoot2, db, logger, vaultConfig } = args;
2258
- 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
+ );
2259
2657
  mcp.tool(
2260
2658
  "prime",
2261
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.",
2262
2660
  PrimeInputSchema.shape,
2263
2661
  async (input) => {
2264
2662
  logger.debug({ tool: "prime", input }, "tool call");
2265
- return handlePrime({ db, vaultRoot: vaultRoot2, vaultConfig }, input);
2663
+ const { db, vaultRoot, vaultConfig } = await binding;
2664
+ return handlePrime({ db, vaultRoot, vaultConfig }, input);
2266
2665
  }
2267
2666
  );
2268
2667
  mcp.tool(
@@ -2271,11 +2670,12 @@ function buildServer(args) {
2271
2670
  SearchInputSchema.shape,
2272
2671
  async (input) => {
2273
2672
  logger.debug({ tool: "search", input }, "tool call");
2673
+ const { db } = await binding;
2274
2674
  return handleSearch({ db }, input);
2275
2675
  }
2276
2676
  );
2277
- registerTemplateResources(mcp, vaultRoot2, vaultConfig);
2278
- registerAuthoringResource(mcp, vaultRoot2, vaultConfig);
2677
+ registerTemplateResources(mcp, binding);
2678
+ registerAuthoringResource(mcp, binding);
2279
2679
  return mcp;
2280
2680
  }
2281
2681
  var init_server = __esm({
@@ -2294,8 +2694,25 @@ __export(start_exports, {
2294
2694
  startMcpServer: () => startMcpServer
2295
2695
  });
2296
2696
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2297
- 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) {
2298
2714
  diag("main entered");
2715
+ if (deferred) return startDeferred(deferred);
2299
2716
  const config = loadConfig();
2300
2717
  diag("config loaded", { vault: config.wikiVault, level: config.logLevel });
2301
2718
  const vaultConfig = await loadVaultConfig(config.wikiVault);
@@ -2314,34 +2731,89 @@ async function startMcpServer() {
2314
2731
  const mcp = buildServer({
2315
2732
  name: config.serverName,
2316
2733
  version: config.serverVersion,
2317
- vaultRoot: config.wikiVault,
2318
- db,
2319
2734
  logger,
2320
- vaultConfig
2735
+ binding: { vaultRoot: config.wikiVault, db, vaultConfig }
2321
2736
  });
2322
2737
  diag("server built");
2323
- const shutdown = async (signal) => {
2324
- logger.info({ signal }, "shutting down");
2325
- diag("shutting down", { signal });
2326
- try {
2327
- await mcp.close();
2328
- db.close();
2329
- } catch (err) {
2330
- logger.error({ err }, "error during shutdown");
2331
- }
2332
- process.exit(0);
2333
- };
2334
- process.once("SIGINT", (s) => void shutdown(s));
2335
- process.once("SIGTERM", (s) => void shutdown(s));
2738
+ installShutdown(mcp, logger, () => db);
2336
2739
  const transport = new StdioServerTransport();
2337
2740
  await mcp.connect(transport);
2338
2741
  logger.info("wiki-mcp ready");
2339
2742
  diag("ready and connected to transport");
2340
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
+ }
2341
2812
  var init_start = __esm({
2342
2813
  "../mcp/src/start.ts"() {
2343
2814
  "use strict";
2344
2815
  init_vault_config();
2816
+ init_binding();
2345
2817
  init_config();
2346
2818
  init_db();
2347
2819
  init_diag();
@@ -2366,25 +2838,28 @@ __export(hook_exports, {
2366
2838
  matchPromptTriggers: () => matchPromptTriggers,
2367
2839
  parsePretoolEvent: () => parsePretoolEvent,
2368
2840
  parsePromptEvent: () => parsePromptEvent,
2841
+ parseSessionStartEvent: () => parseSessionStartEvent,
2369
2842
  parseStopEvent: () => parseStopEvent,
2370
2843
  renderPosttool: () => renderPosttool,
2371
2844
  renderPretool: () => renderPretool,
2372
2845
  renderPrompt: () => renderPrompt,
2846
+ renderSessionStart: () => renderSessionStart,
2373
2847
  renderStop: () => renderStop,
2374
2848
  resolveScope: () => resolveScope,
2375
2849
  runHookPosttool: () => runHookPosttool,
2376
2850
  runHookPretool: () => runHookPretool,
2377
2851
  runHookPrompt: () => runHookPrompt,
2852
+ runHookSessionStart: () => runHookSessionStart,
2378
2853
  runHookStop: () => runHookStop,
2379
2854
  vaultPathTouched: () => vaultPathTouched
2380
2855
  });
2381
- import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
2382
- import { homedir as homedir3 } from "node:os";
2383
- 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";
2384
2859
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
2385
2860
  import { parseArgs as parseArgs2 } from "node:util";
2386
2861
  import { parse as parseYaml3 } from "yaml";
2387
- import { z as z6 } from "zod";
2862
+ import { z as z7 } from "zod";
2388
2863
  function eventFields(raw) {
2389
2864
  let data;
2390
2865
  try {
@@ -2410,7 +2885,7 @@ function kiroIdePromptEvent(now = Date.now()) {
2410
2885
  }
2411
2886
  function loadTriggerFile(path) {
2412
2887
  try {
2413
- const result = z6.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
2888
+ const result = z7.array(TriggerSchema).safeParse(parseYaml3(readFileSync2(path, "utf8")));
2414
2889
  return result.success ? result.data : null;
2415
2890
  } catch {
2416
2891
  return null;
@@ -2434,9 +2909,9 @@ function effectiveTriggers(config, scope, fileTriggers = []) {
2434
2909
  }
2435
2910
  return { triggers, duplicates };
2436
2911
  }
2437
- function expandHome(path) {
2438
- if (path === "~") return homedir3();
2439
- 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;
2440
2915
  }
2441
2916
  function resolveScope(config, cwd) {
2442
2917
  if (cwd === void 0 || cwd === "") return void 0;
@@ -2444,7 +2919,7 @@ function resolveScope(config, cwd) {
2444
2919
  let bestLength = -1;
2445
2920
  for (const [name, scope] of Object.entries(config.scopes)) {
2446
2921
  if (scope.repo === void 0) continue;
2447
- const repo = expandHome(scope.repo).replace(/\/+$/, "");
2922
+ const repo = expandHome2(scope.repo).replace(/\/+$/, "");
2448
2923
  if (!repo.startsWith("/")) continue;
2449
2924
  if (cwd !== repo && !cwd.startsWith(`${repo}/`)) continue;
2450
2925
  if (repo.length > bestLength) {
@@ -2593,7 +3068,7 @@ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
2593
3068
  }
2594
3069
  return matches;
2595
3070
  }
2596
- function evaluateMatches(matches, vaultRoot2) {
3071
+ function evaluateMatches(matches, vaultRoot) {
2597
3072
  const fired = [];
2598
3073
  const skipped = [];
2599
3074
  for (const match of matches) {
@@ -2601,38 +3076,38 @@ function evaluateMatches(matches, vaultRoot2) {
2601
3076
  fired.push(match);
2602
3077
  continue;
2603
3078
  }
2604
- const verdict = evaluateWhen(match.when, vaultRoot2);
3079
+ const verdict = evaluateWhen(match.when, vaultRoot);
2605
3080
  if (verdict === null) skipped.push(match.id);
2606
3081
  else if (!verdict) fired.push(match);
2607
3082
  }
2608
3083
  return { fired, skipped };
2609
3084
  }
2610
- function evaluateWhenVerdict(when, vaultRoot2) {
3085
+ function evaluateWhenVerdict(when, vaultRoot) {
2611
3086
  if (typeof when === "string") return "unknown";
2612
3087
  try {
2613
- const than = newestUpdated(vaultRoot2, when.than);
3088
+ const than = newestUpdated(vaultRoot, when.than);
2614
3089
  if (than === null) return "vacuous";
2615
- const fresh = newestUpdated(vaultRoot2, when.fresh);
3090
+ const fresh = newestUpdated(vaultRoot, when.fresh);
2616
3091
  if (fresh === null) return "unmet";
2617
3092
  return fresh >= than ? "satisfied" : "unmet";
2618
3093
  } catch {
2619
3094
  return "unknown";
2620
3095
  }
2621
3096
  }
2622
- function evaluateWhen(when, vaultRoot2) {
2623
- const verdict = evaluateWhenVerdict(when, vaultRoot2);
3097
+ function evaluateWhen(when, vaultRoot) {
3098
+ const verdict = evaluateWhenVerdict(when, vaultRoot);
2624
3099
  if (verdict === "unknown") return null;
2625
3100
  return verdict !== "unmet";
2626
3101
  }
2627
- function newestUpdated(vaultRoot2, globs) {
3102
+ function newestUpdated(vaultRoot, globs) {
2628
3103
  const regexes = globs.map(globToRegExp);
2629
3104
  let newest = null;
2630
- for (const entry of readdirSync2(vaultRoot2, { recursive: true })) {
3105
+ for (const entry of readdirSync2(vaultRoot, { recursive: true })) {
2631
3106
  const rel = entry.split(sep2).join("/");
2632
3107
  if (!rel.endsWith(".md")) continue;
2633
3108
  if (rel.startsWith(".") || rel.includes("/.")) continue;
2634
3109
  if (!regexes.some((regex) => regex.test(rel))) continue;
2635
- const updated = readUpdated(join10(vaultRoot2, entry));
3110
+ const updated = readUpdated(join11(vaultRoot, entry));
2636
3111
  if (updated !== null && (newest === null || updated > newest)) {
2637
3112
  newest = updated;
2638
3113
  }
@@ -2641,7 +3116,7 @@ function newestUpdated(vaultRoot2, globs) {
2641
3116
  }
2642
3117
  function readUpdated(path) {
2643
3118
  try {
2644
- const { data } = parseFrontmatter(readFileSync(path, "utf8"));
3119
+ const { data } = parseFrontmatter(readFileSync2(path, "utf8"));
2645
3120
  const updated = data.updated;
2646
3121
  if (typeof updated === "string") return updated;
2647
3122
  if (updated instanceof Date) return updated.toISOString().slice(0, 10);
@@ -2704,15 +3179,15 @@ function commandPaths(toolInput) {
2704
3179
  }
2705
3180
  return paths;
2706
3181
  }
2707
- function vaultPathTouched(toolInput, vaultRoot2, cwd) {
2708
- const root = resolve3(vaultRoot2);
3182
+ function vaultPathTouched(toolInput, vaultRoot, cwd) {
3183
+ const root = resolve5(vaultRoot);
2709
3184
  const candidates = [
2710
3185
  ...pathCandidates(toolInput, cwd),
2711
3186
  ...patchPaths(toolInput),
2712
3187
  ...commandPaths(toolInput)
2713
3188
  ];
2714
3189
  return candidates.some((candidate) => {
2715
- const absolute = resolve3(cwd ?? ".", candidate);
3190
+ const absolute = resolve5(cwd ?? ".", candidate);
2716
3191
  return absolute === root || absolute.startsWith(`${root}/`);
2717
3192
  });
2718
3193
  }
@@ -2762,12 +3237,12 @@ function dedupePretoolMatches(stateDir, sessionId, matches, persist = true) {
2762
3237
  const fresh = dedupeMatches(stateDir, sessionId, rest, Date.now(), persist);
2763
3238
  return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
2764
3239
  }
2765
- function hookStateDir() {
2766
- return join10(kmdHome(), "state", "hook");
3240
+ function hookStateDir(vaultRoot) {
3241
+ return resolveStateDir(vaultRoot);
2767
3242
  }
2768
3243
  function explainPrompt(options) {
2769
3244
  const now = options.now ?? Date.now();
2770
- const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
3245
+ const fired = readFired(join11(options.stateDir, safeName(options.sessionId)));
2771
3246
  const entries = [];
2772
3247
  const rendered = [];
2773
3248
  const state = { db: null };
@@ -2804,7 +3279,7 @@ function explainPrompt(options) {
2804
3279
  }
2805
3280
  function explainPretool(options) {
2806
3281
  const now = options.now ?? Date.now();
2807
- const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
3282
+ const fired = readFired(join11(options.stateDir, safeName(options.sessionId)));
2808
3283
  const entries = [];
2809
3284
  const rendered = [];
2810
3285
  for (const trigger of options.triggers) {
@@ -2845,7 +3320,7 @@ function explainPretool(options) {
2845
3320
  }
2846
3321
  function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist = true) {
2847
3322
  if (matches.length === 0) return [];
2848
- const dir = join10(stateDir, safeName(sessionId));
3323
+ const dir = join11(stateDir, safeName(sessionId));
2849
3324
  const fired = readFired(dir);
2850
3325
  const fresh = [];
2851
3326
  const record = [];
@@ -2861,10 +3336,10 @@ function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist =
2861
3336
  }
2862
3337
  if (persist && record.length > 0) {
2863
3338
  try {
2864
- mkdirSync4(dir, { recursive: true });
3339
+ mkdirSync5(dir, { recursive: true });
2865
3340
  for (const key of record) {
2866
3341
  try {
2867
- writeFileSync(join10(dir, key), "", { flag: "wx" });
3342
+ writeFileSync2(join11(dir, key), "", { flag: "wx" });
2868
3343
  } catch (err) {
2869
3344
  if (err.code !== "EEXIST") throw err;
2870
3345
  }
@@ -2896,7 +3371,7 @@ function pruneStale(stateDir, keep) {
2896
3371
  try {
2897
3372
  const cutoff = Date.now() - SESSION_STATE_MAX_AGE_MS;
2898
3373
  for (const entry of readdirSync2(stateDir)) {
2899
- const path = join10(stateDir, entry);
3374
+ const path = join11(stateDir, entry);
2900
3375
  if (path !== keep && statSync(path).mtimeMs < cutoff) {
2901
3376
  rmSync2(path, { recursive: true, force: true });
2902
3377
  }
@@ -2913,17 +3388,14 @@ function hookInvocation() {
2913
3388
  scope: { type: "string" },
2914
3389
  harness: { type: "string" },
2915
3390
  triggers: { type: "string" },
3391
+ "default-root": { type: "string" },
2916
3392
  "dry-run": { type: "boolean" },
2917
3393
  explain: { type: "boolean" }
2918
3394
  }
2919
3395
  });
2920
- const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
2921
- if (vaultRoot2 === void 0 || vaultRoot2 === "") {
2922
- diag2("no vault root (positional or $WIKI_VAULT)");
2923
- return null;
2924
- }
2925
3396
  return {
2926
- vaultRoot: vaultRoot2,
3397
+ positional: positionals2[2],
3398
+ defaultRoot: typeof values2["default-root"] === "string" ? values2["default-root"] : void 0,
2927
3399
  scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
2928
3400
  harness: values2.harness,
2929
3401
  triggersFile: values2.triggers,
@@ -2931,6 +3403,28 @@ function hookInvocation() {
2931
3403
  explain: values2.explain === true
2932
3404
  };
2933
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
+ }
2934
3428
  function resolveFileTriggers(invocation) {
2935
3429
  if (typeof invocation.triggersFile !== "string") return [];
2936
3430
  const loaded = loadTriggerFile(invocation.triggersFile);
@@ -2943,8 +3437,6 @@ function resolveFileTriggers(invocation) {
2943
3437
  async function runHookPrompt() {
2944
3438
  try {
2945
3439
  const invocation = hookInvocation();
2946
- if (invocation === null) return;
2947
- const { vaultRoot: vaultRoot2 } = invocation;
2948
3440
  let event = null;
2949
3441
  if (invocation.harness === "kiro-ide") {
2950
3442
  event = kiroIdePromptEvent();
@@ -2956,7 +3448,9 @@ async function runHookPrompt() {
2956
3448
  diag2("stdin is not a prompt event ({session_id, prompt})");
2957
3449
  return;
2958
3450
  }
2959
- const config = await loadVaultConfig(vaultRoot2);
3451
+ const vaultRoot = resolveHookVault(invocation, event.cwd);
3452
+ if (vaultRoot === null) return;
3453
+ const config = await loadVaultConfig(vaultRoot);
2960
3454
  const scope = invocation.scope ?? resolveScope(config, event.cwd);
2961
3455
  const { triggers, duplicates } = effectiveTriggers(
2962
3456
  config,
@@ -2970,7 +3464,7 @@ async function runHookPrompt() {
2970
3464
  const trace = explainPrompt({
2971
3465
  prompt: event.prompt,
2972
3466
  triggers,
2973
- stateDir: hookStateDir(),
3467
+ stateDir: hookStateDir(vaultRoot),
2974
3468
  sessionId: event.session_id
2975
3469
  });
2976
3470
  console.log(JSON.stringify({ event: "prompt", scope: scope ?? null, duplicates, ...trace }));
@@ -2978,7 +3472,7 @@ async function runHookPrompt() {
2978
3472
  }
2979
3473
  const matches = matchPromptTriggers(event.prompt, triggers);
2980
3474
  const fresh = dedupeMatches(
2981
- hookStateDir(),
3475
+ hookStateDir(vaultRoot),
2982
3476
  event.session_id,
2983
3477
  matches,
2984
3478
  Date.now(),
@@ -2994,8 +3488,6 @@ async function runHookPrompt() {
2994
3488
  async function runHookPretool() {
2995
3489
  try {
2996
3490
  const invocation = hookInvocation();
2997
- if (invocation === null) return;
2998
- const { vaultRoot: vaultRoot2 } = invocation;
2999
3491
  let format = "neutral";
3000
3492
  if (invocation.harness === "claude") {
3001
3493
  format = "claude";
@@ -3007,7 +3499,9 @@ async function runHookPretool() {
3007
3499
  diag2("stdin is not a pretool event ({session_id, tool_name})");
3008
3500
  return;
3009
3501
  }
3010
- const config = await loadVaultConfig(vaultRoot2);
3502
+ const vaultRoot = resolveHookVault(invocation, event.cwd);
3503
+ if (vaultRoot === null) return;
3504
+ const config = await loadVaultConfig(vaultRoot);
3011
3505
  const scope = invocation.scope ?? resolveScope(config, event.cwd);
3012
3506
  const { triggers, duplicates } = effectiveTriggers(
3013
3507
  config,
@@ -3022,8 +3516,8 @@ async function runHookPretool() {
3022
3516
  toolName: event.tool_name,
3023
3517
  toolInput: event.tool_input,
3024
3518
  triggers,
3025
- vaultRoot: vaultRoot2,
3026
- stateDir: hookStateDir(),
3519
+ vaultRoot,
3520
+ stateDir: hookStateDir(vaultRoot),
3027
3521
  sessionId: event.session_id,
3028
3522
  ...event.cwd !== void 0 && { cwd: event.cwd },
3029
3523
  format
@@ -3032,12 +3526,12 @@ async function runHookPretool() {
3032
3526
  return;
3033
3527
  }
3034
3528
  const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
3035
- const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
3529
+ const { fired, skipped } = evaluateMatches(matches, vaultRoot);
3036
3530
  for (const id of skipped) {
3037
3531
  diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
3038
3532
  }
3039
3533
  const rendered = renderPretool(
3040
- dedupePretoolMatches(hookStateDir(), event.session_id, fired, !invocation.dryRun),
3534
+ dedupePretoolMatches(hookStateDir(vaultRoot), event.session_id, fired, !invocation.dryRun),
3041
3535
  format
3042
3536
  );
3043
3537
  for (const line of rendered.stderr) {
@@ -3053,7 +3547,6 @@ async function runHookPretool() {
3053
3547
  async function runHookPosttool() {
3054
3548
  try {
3055
3549
  const invocation = hookInvocation();
3056
- if (invocation === null) return;
3057
3550
  if (invocation.dryRun) {
3058
3551
  diag2("--dry-run/--explain support prompt and pretool events only");
3059
3552
  return;
@@ -3069,13 +3562,15 @@ async function runHookPosttool() {
3069
3562
  diag2("stdin is not a posttool event ({session_id, tool_name})");
3070
3563
  return;
3071
3564
  }
3072
- if (!vaultPathTouched(event.tool_input, invocation.vaultRoot, event.cwd)) return;
3073
- const config = await loadVaultConfig(invocation.vaultRoot);
3074
- 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);
3075
3570
  let synced = false;
3076
3571
  if (!hasErrors(findings)) {
3077
3572
  try {
3078
- await syncVault(invocation.vaultRoot);
3573
+ await syncVault(vaultRoot);
3079
3574
  synced = true;
3080
3575
  } catch (err) {
3081
3576
  diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -3092,7 +3587,6 @@ async function runHookPosttool() {
3092
3587
  async function runHookStop() {
3093
3588
  try {
3094
3589
  const invocation = hookInvocation();
3095
- if (invocation === null) return;
3096
3590
  if (invocation.dryRun) {
3097
3591
  diag2("--dry-run/--explain support prompt and pretool events only");
3098
3592
  return;
@@ -3103,21 +3597,62 @@ async function runHookStop() {
3103
3597
  return;
3104
3598
  }
3105
3599
  if (event.stop_hook_active === true) return;
3106
- const config = await loadVaultConfig(invocation.vaultRoot);
3600
+ const vaultRoot = resolveHookVault(invocation, event.cwd);
3601
+ if (vaultRoot === null) return;
3602
+ const config = await loadVaultConfig(vaultRoot);
3107
3603
  const scope = invocation.scope ?? resolveScope(config, event.cwd);
3108
3604
  if (scope === void 0) return;
3109
3605
  const rendered = renderStop(
3110
- await validateVault(invocation.vaultRoot),
3606
+ await validateVault(vaultRoot),
3111
3607
  config.builtin_hooks?.["handoff-gate"]?.reason
3112
3608
  );
3113
3609
  if (rendered === null) return;
3114
- const fired = dedupeMatches(hookStateDir(), event.session_id, [{ id: "handoff-gate" }]);
3610
+ const fired = dedupeMatches(hookStateDir(vaultRoot), event.session_id, [
3611
+ { id: "handoff-gate" }
3612
+ ]);
3115
3613
  if (fired.length === 0) return;
3116
3614
  console.log(rendered);
3117
3615
  } catch (err) {
3118
3616
  diag2(err instanceof Error ? err.message : String(err));
3119
3617
  }
3120
3618
  }
3619
+ function parseSessionStartEvent(raw) {
3620
+ const fields = eventFields(raw);
3621
+ if (fields === null) return null;
3622
+ const { session_id, cwd, source } = fields;
3623
+ if (typeof session_id !== "string") return null;
3624
+ return {
3625
+ session_id,
3626
+ ...typeof cwd === "string" && { cwd },
3627
+ ...typeof source === "string" && { source }
3628
+ };
3629
+ }
3630
+ function renderSessionStart(scope, source, messages = {}) {
3631
+ const text = source === "compact" ? messages.reorient?.text ?? REORIENT_TEXT : messages.orient?.text ?? ORIENT_TEXT;
3632
+ return `Wiki scope "${scope}": ${text}`;
3633
+ }
3634
+ async function runHookSessionStart() {
3635
+ try {
3636
+ const invocation = hookInvocation();
3637
+ if (invocation.dryRun) {
3638
+ diag2("--dry-run/--explain support prompt and pretool events only");
3639
+ return;
3640
+ }
3641
+ const event = parseSessionStartEvent(await readStdin());
3642
+ if (event === null) {
3643
+ diag2("stdin is not a session-start event ({session_id})");
3644
+ return;
3645
+ }
3646
+ const vaultRoot = resolveHookVault(invocation, event.cwd);
3647
+ if (vaultRoot === null) return;
3648
+ const config = await loadVaultConfig(vaultRoot);
3649
+ const scope = invocation.scope ?? resolveScope(config, event.cwd);
3650
+ if (scope === void 0) return;
3651
+ console.log(renderSessionStart(scope, event.source, config.builtin_hooks ?? {}));
3652
+ } catch (err) {
3653
+ diag2(err instanceof Error ? err.message : String(err));
3654
+ }
3655
+ }
3121
3656
  async function readStdin() {
3122
3657
  process.stdin.setEncoding("utf8");
3123
3658
  let input = "";
@@ -3129,11 +3664,12 @@ async function readStdin() {
3129
3664
  function diag2(message) {
3130
3665
  console.error(`kmd hook: ${message}`);
3131
3666
  }
3132
- var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE, COMMAND_TOKEN_RE, PATHISH_RE, RESYNC_REASON, RESYNC_TEXT, HANDOFF_GATE_REASON;
3667
+ var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE, COMMAND_TOKEN_RE, PATHISH_RE, RESYNC_REASON, RESYNC_TEXT, HANDOFF_GATE_REASON, ORIENT_TEXT, REORIENT_TEXT;
3133
3668
  var init_hook = __esm({
3134
3669
  "../cli/src/hook.ts"() {
3135
3670
  "use strict";
3136
3671
  init_database();
3672
+ init_kmd_config();
3137
3673
  init_vault_config();
3138
3674
  init_frontmatter();
3139
3675
  init_sync();
@@ -3148,6 +3684,8 @@ var init_hook = __esm({
3148
3684
  RESYNC_REASON = "Edit landed; the index sync is held until these validate errors are fixed";
3149
3685
  RESYNC_TEXT = "kmd sync failed \u2014 index not updated; see hook stderr";
3150
3686
  HANDOFF_GATE_REASON = "Validate errors are outstanding and the index sync is held \u2014 fix them, let the resync run, then finish";
3687
+ ORIENT_TEXT = "prime via the wiki MCP prime tool before substantive work \u2014 the primer carries current focus, book of work, and invariants.";
3688
+ REORIENT_TEXT = "context was compacted and transcript detail is lost \u2014 re-read the primer via the wiki MCP prime tool and route uncaptured findings into the wiki before continuing.";
3151
3689
  }
3152
3690
  });
3153
3691
 
@@ -3161,16 +3699,33 @@ process.on("warning", (warning) => {
3161
3699
  });
3162
3700
  var USAGE = `usage: kmd <command> [options]
3163
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
+
3164
3707
  commands:
3165
- init [<dir>] [-y] scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y)
3166
- sync vault \u2192 index sync (runs validate first)
3167
- validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
3168
- mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
3169
- config [<vault-root>] print vault + index resolution; with no vault, list known vaults
3170
- db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
3171
- hook <prompt|pretool|posttool|stop> [<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>]
3172
3726
  harness gate engine: JSON event on stdin, decision/context on stdout;
3173
- 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;
3174
3729
  stop blocks the handoff once while validate errors hold the sync
3175
3730
 
3176
3731
  options:
@@ -3183,52 +3738,79 @@ var { positionals, values } = parseArgs3({
3183
3738
  options: {
3184
3739
  version: { type: "boolean", short: "v" },
3185
3740
  help: { type: "boolean", short: "h" },
3186
- 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" }
3187
3745
  }
3188
3746
  });
3189
3747
  var command = values.version ? "--version" : values.help ? "--help" : positionals[0];
3190
- function applyVaultRoot(positionalIndex) {
3191
- const arg = positionals[positionalIndex];
3192
- if (arg) {
3193
- process.env.WIKI_VAULT = arg;
3194
- }
3195
- }
3196
3748
  async function run() {
3197
3749
  switch (command) {
3198
3750
  case "init": {
3199
3751
  const { runInit: runInit2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
3200
- 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
+ );
3201
3758
  break;
3202
3759
  }
3203
3760
  case "sync": {
3204
3761
  const { runSyncCommand: runSyncCommand2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
3205
- await runSyncCommand2();
3762
+ await runSyncCommand2(positionals[1]);
3206
3763
  break;
3207
3764
  }
3208
3765
  case "validate": {
3209
- applyVaultRoot(1);
3210
3766
  const { runValidate: runValidate2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
3211
- await runValidate2();
3767
+ await runValidate2(positionals[1]);
3212
3768
  break;
3213
3769
  }
3214
3770
  case "mcp": {
3215
- 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
+ }
3216
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;
3217
3797
  await startMcpServer2();
3218
3798
  break;
3219
3799
  }
3220
3800
  case "config": {
3221
- applyVaultRoot(1);
3222
- const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
3223
- 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);
3224
3807
  break;
3225
3808
  }
3226
3809
  case "db": {
3227
3810
  const sub = positionals[1];
3228
3811
  if (sub === "reset") {
3229
- applyVaultRoot(2);
3230
3812
  const { runDbReset: runDbReset2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
3231
- await runDbReset2();
3813
+ await runDbReset2(positionals[2]);
3232
3814
  } else {
3233
3815
  console.error(sub ? `unknown db subcommand: ${sub}` : "usage: kmd db reset [<vault-root>]");
3234
3816
  process.exit(2);
@@ -3249,11 +3831,14 @@ async function run() {
3249
3831
  } else if (sub === "stop") {
3250
3832
  const { runHookStop: runHookStop2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
3251
3833
  await runHookStop2();
3834
+ } else if (sub === "session-start") {
3835
+ const { runHookSessionStart: runHookSessionStart2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
3836
+ await runHookSessionStart2();
3252
3837
  } else if (sub) {
3253
3838
  console.error(`kmd hook: unknown event: ${sub}`);
3254
3839
  } else {
3255
3840
  console.error(
3256
- "usage: kmd hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
3841
+ "usage: kmd hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
3257
3842
  );
3258
3843
  process.exit(2);
3259
3844
  }
@@ -3261,11 +3846,11 @@ async function run() {
3261
3846
  }
3262
3847
  case "--version":
3263
3848
  case "-v": {
3264
- const { readFileSync: readFileSync2 } = await import("node:fs");
3265
- const { join: join11, dirname: dirname4 } = await import("node:path");
3266
- const { fileURLToPath } = await import("node:url");
3267
- const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
3268
- 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"));
3269
3854
  console.log(pkg.version);
3270
3855
  break;
3271
3856
  }