@bartolli/kmd 0.5.1 → 0.7.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
@@ -27,9 +27,11 @@ function openDatabase(dbPath) {
27
27
  db.exec(SCHEMA);
28
28
  return db;
29
29
  }
30
+ function kmdHome() {
31
+ return process.env.KMD_HOME ?? join(homedir(), ".kmd");
32
+ }
30
33
  function indexRootDir() {
31
- const home = process.env.KMD_HOME ?? join(homedir(), ".kmd");
32
- return join(home, "db");
34
+ return join(kmdHome(), "db");
33
35
  }
34
36
  function canonicalVaultRoot(vaultRoot2) {
35
37
  try {
@@ -114,14 +116,24 @@ CREATE TABLE IF NOT EXISTS meta (
114
116
  }
115
117
  });
116
118
 
117
- // ../cli/src/config.ts
119
+ // ../db/src/vault-config.ts
118
120
  import { readFile } from "node:fs/promises";
119
121
  import { join as join2 } from "node:path";
120
122
  import { parse } from "yaml";
121
123
  import { z } from "zod";
124
+ function isValidRegex(pattern) {
125
+ try {
126
+ return Boolean(new RegExp(pattern));
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
122
131
  function kindName(entry) {
123
132
  return typeof entry === "string" ? entry : entry.name;
124
133
  }
134
+ function configJsonSchema() {
135
+ return z.toJSONSchema(VaultConfigSchema, { target: "draft-7" });
136
+ }
125
137
  async function loadVaultConfig(vaultRoot2) {
126
138
  const path = join2(vaultRoot2, "vault.yaml");
127
139
  let raw;
@@ -138,37 +150,106 @@ ${issues}`);
138
150
  }
139
151
  return parsed.data;
140
152
  }
141
- var ScopeSchema, KindEntrySchema, VaultConfigSchema, BUILT_IN_KINDS;
142
- var init_config = __esm({
143
- "../cli/src/config.ts"() {
153
+ var ScopeSchema, KindEntrySchema, WhenSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
154
+ var init_vault_config = __esm({
155
+ "../db/src/vault-config.ts"() {
144
156
  "use strict";
145
- ScopeSchema = z.object({
146
- repo: z.string().optional(),
147
- methodology: z.string().optional(),
148
- status: z.string()
157
+ ScopeSchema = z.strictObject({
158
+ repo: z.string().optional().describe(
159
+ "Consumer repo path (~ expands). Load-bearing for kmd hook: the active scope resolves by matching the session cwd against it."
160
+ ),
161
+ methodology: z.string().optional().describe("Must appear in the methodologies list."),
162
+ status: z.string().describe("Free string; keep within statuses by convention.")
149
163
  });
150
164
  KindEntrySchema = z.union([
151
165
  z.string(),
152
- z.object({
166
+ z.strictObject({
153
167
  name: z.string(),
154
- signal: z.string(),
155
- where: z.string()
168
+ signal: z.string().describe("When to pick this kind."),
169
+ where: z.string().describe("Path pattern pages of this kind follow.")
170
+ })
171
+ ]);
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)
156
178
  })
157
179
  ]);
158
- VaultConfigSchema = z.object({
159
- scopes: z.record(z.string(), ScopeSchema),
160
- kinds: z.array(KindEntrySchema),
161
- statuses: z.array(z.string()),
162
- methodologies: z.array(z.string()),
163
- tags: z.object({
164
- canonical: z.array(z.string()),
165
- aliases: z.record(z.string(), z.string())
180
+ TriggerSchema = z.strictObject({
181
+ id: z.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
182
+ on: z.enum(["prompt", "pretool"]),
183
+ enforce: z.enum(["inject", "warn", "block"]).describe("inject: context line \xB7 warn: stderr \xB7 block: deny with reason."),
184
+ keywords: z.array(z.string().min(1)).optional().describe("Word-boundary, porter-stemmed match. Prompt triggers need keywords or intent."),
185
+ intent: z.array(z.string()).optional().describe("Case-insensitive regexes over the raw prompt \u2014 the stemming escape hatch."),
186
+ tool: z.string().optional().describe("Exact tool name; pretool matchers AND-compose."),
187
+ args_match: z.string().optional().describe("Regex over the serialized tool input."),
188
+ files: z.array(z.string().min(1)).optional().describe("Globs against the paths the tool touches; pretool triggers only."),
189
+ when: WhenSchema.optional().describe(
190
+ "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."
191
+ ),
192
+ text: z.string().optional().describe("Required for inject and warn \u2014 the line emitted."),
193
+ reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads.")
194
+ }).superRefine((trigger, ctx) => {
195
+ if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
196
+ ctx.addIssue({
197
+ code: "custom",
198
+ message: `prompt trigger "${trigger.id}" needs keywords or intent`
199
+ });
200
+ }
201
+ if (trigger.on === "pretool" && trigger.tool === void 0 && trigger.args_match === void 0 && !trigger.files?.length) {
202
+ ctx.addIssue({
203
+ code: "custom",
204
+ message: `pretool trigger "${trigger.id}" needs a tool, args_match, or files matcher`
205
+ });
206
+ }
207
+ if (trigger.on === "prompt" && trigger.files !== void 0) {
208
+ ctx.addIssue({
209
+ code: "custom",
210
+ message: `trigger "${trigger.id}": files applies to pretool triggers only`
211
+ });
212
+ }
213
+ if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
214
+ ctx.addIssue({
215
+ code: "custom",
216
+ message: trigger.enforce === "block" ? `block trigger "${trigger.id}" needs a reason` : `${trigger.enforce} trigger "${trigger.id}" needs a text`
217
+ });
218
+ }
219
+ const patterns = [...trigger.intent ?? []];
220
+ if (trigger.args_match !== void 0) patterns.push(trigger.args_match);
221
+ for (const pattern of patterns) {
222
+ if (!isValidRegex(pattern)) {
223
+ ctx.addIssue({
224
+ code: "custom",
225
+ message: `trigger "${trigger.id}" has an invalid regex: ${pattern}`
226
+ });
227
+ }
228
+ }
229
+ });
230
+ TriggersSchema = z.record(z.string(), z.array(TriggerSchema));
231
+ VaultConfigSchema = z.strictObject({
232
+ scopes: z.record(z.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
233
+ kinds: z.array(KindEntrySchema).describe(
234
+ "Page kind vocabulary; validate-enforced. Object form adds a kind-selector row to wiki://authoring."
235
+ ),
236
+ statuses: z.array(z.string()).describe("Page status vocabulary; validate-enforced."),
237
+ methodologies: z.array(z.string()).describe("Methodology vocabulary for pages and scope entries."),
238
+ tags: z.strictObject({
239
+ canonical: z.array(z.string()).describe("Approved tags."),
240
+ aliases: z.record(z.string(), z.string()).describe("Alias \u2192 canonical; validate warns on alias use.")
166
241
  }),
167
- authoring_rules: z.string().optional(),
168
- authoring_rules_extra: z.string().optional(),
169
- sync_protocol: z.string().optional(),
170
- sync_protocol_extra: z.string().optional()
171
- }).superRefine((config, ctx) => {
242
+ authoring_rules: z.string().optional().describe("Replaces the served \xA7 Authoring rules entirely \u2014 escape hatch."),
243
+ authoring_rules_extra: z.string().optional().describe("Appended after the served \xA7 Authoring rules."),
244
+ sync_protocol: z.string().optional().describe("Replaces the served \xA7 Resync protocol entirely \u2014 escape hatch."),
245
+ sync_protocol_extra: z.string().optional().describe("Appended after the served \xA7 Resync protocol."),
246
+ triggers: TriggersSchema.optional().describe(
247
+ 'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
248
+ ),
249
+ triggers_extra: TriggersSchema.optional().describe(
250
+ 'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
251
+ )
252
+ }).describe("kmd vault.yaml \u2014 controlled vocabulary and gate triggers.").superRefine((config, ctx) => {
172
253
  for (const [name, scope] of Object.entries(config.scopes)) {
173
254
  if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
174
255
  ctx.addIssue({
@@ -178,6 +259,28 @@ var init_config = __esm({
178
259
  });
179
260
  }
180
261
  }
262
+ if (config.triggers?._all !== void 0) {
263
+ ctx.addIssue({
264
+ code: "custom",
265
+ path: ["triggers", "_all"],
266
+ message: '"_all" is reserved for triggers_extra'
267
+ });
268
+ }
269
+ for (const field of ["triggers", "triggers_extra"]) {
270
+ for (const [scope, list] of Object.entries(config[field] ?? {})) {
271
+ const seen = /* @__PURE__ */ new Set();
272
+ for (const trigger of list) {
273
+ if (seen.has(trigger.id)) {
274
+ ctx.addIssue({
275
+ code: "custom",
276
+ path: [field, scope],
277
+ message: `duplicate trigger id "${trigger.id}"`
278
+ });
279
+ }
280
+ seen.add(trigger.id);
281
+ }
282
+ }
283
+ }
181
284
  });
182
285
  BUILT_IN_KINDS = /* @__PURE__ */ new Set([
183
286
  "project",
@@ -220,11 +323,287 @@ var init_frontmatter = __esm({
220
323
  }
221
324
  });
222
325
 
326
+ // ../cli/src/init-templates.ts
327
+ var VAULT_TEMPLATES;
328
+ var init_init_templates = __esm({
329
+ "../cli/src/init-templates.ts"() {
330
+ "use strict";
331
+ VAULT_TEMPLATES = {
332
+ "note.md": '---\ntitle: {{title}}\ntags: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n',
333
+ "project-adr.md": '---\ntitle: {{title}}\nkind: adr\nscope:\nstatus: active\nsummary:\ntags: []\nsupersedes:\nsuperseded_by:\nsources: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Status\n\n> [!info] Active\n\n## Context\n\nThe forces in tension \u2014 what made this a decision rather than an\nobvious step. Name the constraints; skip the history recap.\n\n## Decision\n\nWhat was decided, precisely. Predicates only \u2014 falsifiable, not\naspirational.\n\n## Rationale\n\nWhy this side won: which alternatives were live, and what killed each.\nAn ADR with no rejected alternative is a spec in disguise.\n\n## Consequences\n\nWhat this commits the project to going forward \u2014 including the costs\naccepted, not just the benefits claimed.\n\n## Links\n\n-\n',
334
+ "project-index.md": `---
335
+ title: {{title}}
336
+ kind: project
337
+ methodology: sdd
338
+ phase: 1
339
+ repo:
340
+ scope:
341
+ status: active
342
+ tags: []
343
+ summary:
344
+ created: "{{date}}"
345
+ updated: {{date}}
346
+ ---
347
+
348
+ # {{title}}
349
+
350
+ ## Summary
351
+
352
+ One paragraph. What this project is and what it delivers.
353
+
354
+ ## Current Phase
355
+
356
+ What's in flight right now. 1\u20133 sentences.
357
+
358
+ ## Links
359
+
360
+ - [[projects/{{title}}/primer]]
361
+
362
+ ## Sources
363
+
364
+ -
365
+ `,
366
+ "project-ops.md": `---
367
+ title: {{title}}
368
+ kind: ops
369
+ scope:
370
+ status: active
371
+ summary:
372
+ tags: []
373
+ sources: []
374
+ created: "{{date}}"
375
+ updated: {{date}}
376
+ ---
377
+
378
+ # {{title}}
379
+
380
+ ## Summary
381
+
382
+ What this runbook operates and when it runs.
383
+
384
+ ## Context
385
+
386
+ What triggers this flow; what breaks when it's skipped.
387
+
388
+ ## Details
389
+
390
+ Numbered steps executable without asking: exact commands, expected
391
+ output, failure modes and their recovery. Predicates only \u2014 if a step
392
+ needs judgment, say whose.
393
+
394
+ ## Links
395
+
396
+ -
397
+
398
+ ## Sources
399
+
400
+ -
401
+ `,
402
+ "project-plan.md": `---
403
+ title: {{title}}
404
+ kind: plan
405
+ scope:
406
+ status: active
407
+ summary:
408
+ tags: []
409
+ created: "{{date}}"
410
+ updated: {{date}}
411
+ ---
412
+
413
+ # {{title}}
414
+
415
+ ## Goal
416
+
417
+ One sentence: what this phase delivers. If it needs two, it's two
418
+ plans.
419
+
420
+ ## Scope
421
+
422
+ What's in and \u2014 more important \u2014 what's out. Out-of-scope lines stop
423
+ scope creep better than in-scope lines define it.
424
+
425
+ ## Milestones
426
+
427
+ Checkable outcomes, not activities. Tick here; don't cascade ticks to
428
+ index.md unless phase or status changed.
429
+
430
+ 1.
431
+ 2.
432
+
433
+ ## Dependencies
434
+
435
+ -
436
+
437
+ ## Status Log
438
+
439
+ Dated one-liners, newest first. This is the only history surface \u2014
440
+ primer and index stay clean.
441
+
442
+ - {{date}}: Phase started.
443
+ `,
444
+ "project-primer.md": "---\ncreated: \"{{date}}\"\nupdated: {{date}}\n---\n\n# Primer\n\n## Current Focus\n\nWhat we're working on right now. 1\u20133 sentences. Not a history log \u2014\nprogress lives in plan checkboxes; history lives in git.\n\n## Open Questions\n\nCurrently-open questions only. Strip on resolution; reflect the\nresolution in the relevant ADR or spec, not here.\n\n-\n\n## Blocked On\n\nNothing currently. List only items genuinely blocking forward\nprogress.\n\n## Load-bearing invariants (optional)\n\nFacts not derivable from code or commits \u2014 pipeline orderings, sync\nconstraints, file-location rationales. The kind of fact a fresh-context\nagent would otherwise re-derive incorrectly. Durable across sessions.\n\n## Read Order (optional)\n\n\u22645 entries. Wiki/doc pointers \u2014 what to read to orient. Each entry\none line: `[[wikilink]] \u2014 why`.\n\n1.\n\n## Working set (optional)\n\n\u226410 entries. File pointers (code, fixtures, configs) for the\nimmediate Next Steps, with one-line context. Lets a fresh-context\nagent skip the discovery loop. Task-scoped \u2014 refresh when focus\nshifts. Most valuable when transitioning phases or starting a new\nfeature; can be skipped mid-slice when the immediate context already\ncarries it.\n\n- `path/to/file` \u2014 why it's relevant for the next steps.\n",
445
+ "project-spec.md": `---
446
+ title: {{title}}
447
+ kind: spec
448
+ scope:
449
+ status: active
450
+ summary:
451
+ tags: []
452
+ sources: []
453
+ created: "{{date}}"
454
+ updated: {{date}}
455
+ ---
456
+
457
+ # {{title}}
458
+
459
+ ## Summary
460
+
461
+ One paragraph, in predicates \u2014 the system's current shape, not its
462
+ history. If you're recording a *choice* between alternatives, stop:
463
+ that's an ADR.
464
+
465
+ ## Context
466
+
467
+ The question this spec answers and who asks it. One paragraph, no
468
+ narrative arc.
469
+
470
+ ## Details
471
+
472
+ The actual contract: inputs, outputs, invariants, failure modes. Use
473
+ subsections per concern. Present tense \u2014 the spec describes what IS
474
+ and must match current code at every commit.
475
+
476
+ ## Links
477
+
478
+ -
479
+
480
+ ## Sources
481
+
482
+ -
483
+ `,
484
+ "project-story.md": '---\ntitle: {{title}}\nkind: story\nscope:\nparent:\nstatus: active\ntriage_state: needs-triage\ncategory: enhancement\nblocked_by: []\ntags: []\nsources: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## User Story\n\nAs a {{actor}}, I want {{capability}}, so that {{benefit}}.\n\n## Scenarios\n\nScenarios are the test specification \u2014 each becomes a failing test\nbefore implementation. Write observable outcomes, not implementation\nsteps.\n\n**Scenario: {{scenario name}}**\n- Given {{precondition}}\n- When {{action}}\n- Then {{expected outcome}}\n\n**Scenario: {{edge case name}}**\n- Given {{precondition}}\n- When {{action}}\n- Then {{expected outcome}}\n\n## Slices\n\nVertical tracer bullets: each slice ships signature + implementation +\ntests + wiring, independently committable. `AFK` = agent works alone;\n`HITL` = human in the loop.\n\n- [ ] **Slice 1** \u2014 {{description}} \xB7 `AFK` \xB7 [[spec-{{topic}}]]\n- [ ] **Slice 2** \u2014 {{description}} \xB7 `HITL` \xB7 [[adr-{{decision}}]]\n\n## References\n\n- [[spec-{{related-spec}}]]\n- [[adr-{{related-adr}}]]\n',
485
+ "research-article.md": '---\ntitle: {{title}}\nkind: article\ntopic:\nstatus: active\nsummary:\ntags: []\nsources: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Summary\n\nOne paragraph. What this page covers and why it matters.\n\n## Context\n\nWhy this page exists. The problem or question it addresses.\n\n## Details\n\nThe actual content. Use subsections as needed.\n\n## Links\n\n-\n\n## Sources\n\n-\n',
486
+ "research-index.md": '---\ntitle: {{title}}\nkind: topic\nstatus: active\nsummary:\nconfidence: medium\nsource_count: 0\ntags: []\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Summary\n\nOne paragraph. What this research topic covers.\n\n## Confidence\n\nWhy the confidence level is what it is. What would raise or lower it.\n\n## Pages\n\n-\n\n## Sources\n\n-\n',
487
+ "research-src.md": '---\ntitle: {{title}}\nkind: src\ntopic:\nstatus: active\nsummary:\ntags: []\nsource_url:\ncreated: "{{date}}"\nupdated: {{date}}\n---\n\n# {{title}}\n\n## Summary\n\nWhat this source is. Author, origin, year.\n\n## Key Points\n\n-\n\n## Quotes\n\n>\n\n## Relevance\n\nWhy this source matters to the topic.\n\n## Citation\n\n`{author, year, title, url}`\n'
488
+ };
489
+ }
490
+ });
491
+
492
+ // ../cli/src/init.ts
493
+ import { mkdir, readdir, readFile as readFile2, writeFile } from "node:fs/promises";
494
+ import { join as join3, resolve as resolve2 } from "node:path";
495
+ import { stringify } from "yaml";
496
+ async function refreshSchemaFile(root) {
497
+ const path = join3(root, SCHEMA_FILE);
498
+ const next = `${JSON.stringify(configJsonSchema(), null, 2)}
499
+ `;
500
+ try {
501
+ if (await readFile2(path, "utf8") === next) return false;
502
+ } catch {
503
+ }
504
+ await writeFile(path, next);
505
+ return true;
506
+ }
507
+ async function scaffoldVault(dir) {
508
+ const root = resolve2(dir);
509
+ let entries = [];
510
+ try {
511
+ entries = await readdir(root);
512
+ } catch (err) {
513
+ if (err.code !== "ENOENT") throw err;
514
+ }
515
+ if (entries.includes("vault.yaml")) {
516
+ throw new Error(`already a vault: ${root} (vault.yaml exists)`);
517
+ }
518
+ if (entries.length > 0) {
519
+ throw new Error(
520
+ `target is not empty: ${root}
521
+ found: ${entries.join(", ")}
522
+ delete it or pick another directory`
523
+ );
524
+ }
525
+ for (const domain of DOMAIN_DIRS) {
526
+ await mkdir(join3(root, domain), { recursive: true });
527
+ }
528
+ await mkdir(join3(root, "templates"), { recursive: true });
529
+ for (const [file, content] of Object.entries(VAULT_TEMPLATES)) {
530
+ await writeFile(join3(root, "templates", file), content);
531
+ }
532
+ await refreshSchemaFile(root);
533
+ await writeFile(join3(root, "vault.yaml"), SCHEMA_MODELINE + stringify(STARTER_CONFIG));
534
+ return root;
535
+ }
536
+ async function promptYesNo(question, input = process.stdin, output = process.stderr) {
537
+ const { createInterface } = await import("node:readline/promises");
538
+ const rl = createInterface({ input, output });
539
+ try {
540
+ const answer = await rl.question(question);
541
+ return /^y(es)?$/i.test(answer.trim());
542
+ } finally {
543
+ rl.close();
544
+ }
545
+ }
546
+ async function runInit(dir, yes = false) {
547
+ let target = dir;
548
+ if (!target) {
549
+ if (yes) {
550
+ target = ".";
551
+ } else if (process.stdin.isTTY) {
552
+ const ok = await promptYesNo(`initialize a vault in ${resolve2(".")}? [y/N] `);
553
+ if (!ok) {
554
+ console.error("init: aborted");
555
+ process.exit(1);
556
+ }
557
+ target = ".";
558
+ } else {
559
+ console.error("usage: kmd init <dir> (or --yes to scaffold the current directory)");
560
+ process.exit(2);
561
+ }
562
+ }
563
+ let root;
564
+ try {
565
+ root = await scaffoldVault(target);
566
+ } catch (err) {
567
+ console.error(`init: ${err instanceof Error ? err.message : err}`);
568
+ process.exit(1);
569
+ }
570
+ const templateCount = Object.keys(VAULT_TEMPLATES).length;
571
+ console.log(`initialized empty vault at ${root}
572
+
573
+ vault.yaml starter vocabulary \u2014 add your first scope under scopes:
574
+ vault.schema.json IDE validation via the yaml-language-server modeline
575
+ templates/ ${templateCount} built-in templates (served at wiki://template/...)
576
+ projects/ research/ notes/
577
+
578
+ next steps:
579
+ export WIKI_VAULT=${root}
580
+ kmd mcp ${root} # stdio MCP server (prime, search)`);
581
+ }
582
+ var SCHEMA_FILE, SCHEMA_MODELINE, STARTER_CONFIG, DOMAIN_DIRS;
583
+ var init_init = __esm({
584
+ "../cli/src/init.ts"() {
585
+ "use strict";
586
+ init_vault_config();
587
+ init_init_templates();
588
+ SCHEMA_FILE = "vault.schema.json";
589
+ SCHEMA_MODELINE = `# yaml-language-server: $schema=./${SCHEMA_FILE}
590
+ `;
591
+ STARTER_CONFIG = {
592
+ scopes: {},
593
+ kinds: ["project", "spec", "adr", "plan", "story", "ops", "topic", "article", "src", "note"],
594
+ statuses: ["draft", "active", "superseded", "archived"],
595
+ methodologies: ["sdd", "tdd", "hybrid"],
596
+ tags: { canonical: [], aliases: {} }
597
+ };
598
+ DOMAIN_DIRS = ["projects", "research", "notes"];
599
+ }
600
+ });
601
+
223
602
  // ../cli/src/sync.ts
224
603
  import { createHash as createHash2 } from "node:crypto";
225
604
  import { mkdirSync } from "node:fs";
226
- import { readdir, readFile as readFile2 } from "node:fs/promises";
227
- import { dirname, join as join3, relative, sep } from "node:path";
605
+ import { readdir as readdir2, readFile as readFile3 } from "node:fs/promises";
606
+ import { dirname, join as join4, relative, sep } from "node:path";
228
607
  import { z as z2 } from "zod";
229
608
  function loadEnv() {
230
609
  const parsed = EnvSchema.safeParse({
@@ -240,18 +619,18 @@ function loadEnv() {
240
619
  async function walkMarkdown(root, domain) {
241
620
  const out = [];
242
621
  async function recurse(dir) {
243
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
622
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
244
623
  if (!entries) return;
245
624
  for (const entry of entries) {
246
625
  if (entry.name.startsWith(".")) continue;
247
626
  if (entry.isDirectory()) {
248
- await recurse(join3(dir, entry.name));
627
+ await recurse(join4(dir, entry.name));
249
628
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
250
- out.push(join3(dir, entry.name));
629
+ out.push(join4(dir, entry.name));
251
630
  }
252
631
  }
253
632
  }
254
- await recurse(join3(root, domain));
633
+ await recurse(join4(root, domain));
255
634
  return out;
256
635
  }
257
636
  function toRelativePath(root, absolute) {
@@ -373,26 +752,24 @@ function syncPage(db, fields) {
373
752
  }
374
753
  return "changed";
375
754
  }
376
- async function runSync() {
377
- const env = loadEnv();
378
- const dbPath = resolveIndexPath(env.WIKI_VAULT);
379
- console.log(`sync: ${env.WIKI_VAULT} \u2192 ${dbPath}`);
380
- const vaultConfig = await loadVaultConfig(env.WIKI_VAULT);
755
+ async function syncVault(vaultRoot2) {
756
+ const dbPath = resolveIndexPath(vaultRoot2);
757
+ const vaultConfig = await loadVaultConfig(vaultRoot2);
381
758
  const scopes = new Set(Object.keys(vaultConfig.scopes));
382
759
  mkdirSync(dirname(dbPath), { recursive: true });
383
760
  const db = openDatabase(dbPath);
384
761
  try {
385
762
  const files = [];
386
763
  for (const domain of SCAN_DOMAINS) {
387
- files.push(...await walkMarkdown(env.WIKI_VAULT, domain));
764
+ files.push(...await walkMarkdown(vaultRoot2, domain));
388
765
  }
389
766
  const indexedPaths = [];
390
767
  let changed = 0;
391
768
  let unchanged = 0;
392
769
  let skipped = 0;
393
770
  for (const file of files) {
394
- const path = toRelativePath(env.WIKI_VAULT, file);
395
- const raw = await readFile2(file, "utf8");
771
+ const path = toRelativePath(vaultRoot2, file);
772
+ const raw = await readFile3(file, "utf8");
396
773
  const parsed = parseFrontmatter(raw);
397
774
  const fields = buildPageFields(path, raw, parsed, scopes);
398
775
  if (!fields) {
@@ -415,26 +792,44 @@ async function runSync() {
415
792
  pagesDeleted = Number(pageResult.changes);
416
793
  const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
417
794
  linksDeleted = Number(linkResult.changes);
418
- } else {
419
- console.warn("no indexable pages found; skipping orphan deletion (safety)");
420
795
  }
421
796
  db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
422
- setMeta(db, "vault_root", canonicalVaultRoot(env.WIKI_VAULT));
797
+ setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
423
798
  setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
424
- console.log(
425
- `done: ${changed} changed, ${unchanged} unchanged, ${skipped} skipped, ${pagesDeleted} pages deleted, ${linksDeleted} link orphans cleared`
426
- );
799
+ if (await refreshSchemaFile(vaultRoot2)) {
800
+ console.error("sync: vault.schema.json refreshed to the running engine");
801
+ }
802
+ return {
803
+ changed,
804
+ unchanged,
805
+ skipped,
806
+ pagesDeleted,
807
+ linksDeleted,
808
+ noPages: indexedPaths.length === 0
809
+ };
427
810
  } finally {
428
811
  db.close();
429
812
  }
430
813
  }
814
+ async function runSync() {
815
+ const env = loadEnv();
816
+ console.log(`sync: ${env.WIKI_VAULT} \u2192 ${resolveIndexPath(env.WIKI_VAULT)}`);
817
+ const stats = await syncVault(env.WIKI_VAULT);
818
+ if (stats.noPages) {
819
+ console.warn("no indexable pages found; skipping orphan deletion (safety)");
820
+ }
821
+ console.log(
822
+ `done: ${stats.changed} changed, ${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.pagesDeleted} pages deleted, ${stats.linksDeleted} link orphans cleared`
823
+ );
824
+ }
431
825
  var EnvSchema, SCAN_DOMAINS, WIKILINK_RE, INDEXED_FRONTMATTER_KEYS;
432
826
  var init_sync = __esm({
433
827
  "../cli/src/sync.ts"() {
434
828
  "use strict";
435
829
  init_database();
436
- init_config();
830
+ init_vault_config();
437
831
  init_frontmatter();
832
+ init_init();
438
833
  EnvSchema = z2.object({
439
834
  WIKI_VAULT: z2.string().min(1)
440
835
  });
@@ -454,8 +849,8 @@ var init_sync = __esm({
454
849
  });
455
850
 
456
851
  // ../cli/src/validate.ts
457
- import { readFile as readFile3, stat } from "node:fs/promises";
458
- import { join as join4 } from "node:path";
852
+ import { readFile as readFile4, stat } from "node:fs/promises";
853
+ import { join as join5 } from "node:path";
459
854
  function hasIndexableTitle(data) {
460
855
  return typeof data.title === "string" && data.title.trim() !== "";
461
856
  }
@@ -813,7 +1208,7 @@ async function validateVault(root) {
813
1208
  const pages = [];
814
1209
  const linkPages = [];
815
1210
  for (const { relPath, abs } of files) {
816
- const raw = await readFile3(abs, "utf8");
1211
+ const raw = await readFile4(abs, "utf8");
817
1212
  findings.push(...validatePage(relPath, raw, cfg, refIndex));
818
1213
  try {
819
1214
  const parsed = parseFrontmatter(raw);
@@ -827,7 +1222,7 @@ async function validateVault(root) {
827
1222
  for (const name of customKindNames(cfg)) {
828
1223
  const file = `templates/${name}.md`;
829
1224
  try {
830
- await stat(join4(root, file));
1225
+ await stat(join5(root, file));
831
1226
  } catch {
832
1227
  findings.push({
833
1228
  path: file,
@@ -843,7 +1238,7 @@ var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS, UNIVERSAL_FLOOR;
843
1238
  var init_validate = __esm({
844
1239
  "../cli/src/validate.ts"() {
845
1240
  "use strict";
846
- init_config();
1241
+ init_vault_config();
847
1242
  init_frontmatter();
848
1243
  init_sync();
849
1244
  REQUIRED_FIELDS = {
@@ -901,12 +1296,13 @@ __export(cli_exports, {
901
1296
  resolveCli: () => resolveCli,
902
1297
  runConfig: () => runConfig,
903
1298
  runDbReset: () => runDbReset,
1299
+ runInit: () => runInit,
904
1300
  runSyncCommand: () => runSyncCommand,
905
1301
  runValidate: () => runValidate,
906
1302
  vaultRoot: () => vaultRoot
907
1303
  });
908
1304
  import { existsSync, readdirSync, rmSync } from "node:fs";
909
- import { dirname as dirname2, join as join5 } from "node:path";
1305
+ import { dirname as dirname2, join as join6 } from "node:path";
910
1306
  import { parseArgs } from "node:util";
911
1307
  function resolveCli(argv) {
912
1308
  const { positionals: positionals2 } = parseArgs({ args: argv, allowPositionals: true, strict: false });
@@ -976,7 +1372,7 @@ function knownVaults() {
976
1372
  const known = [];
977
1373
  for (const entry of readdirSync(root, { withFileTypes: true })) {
978
1374
  if (!entry.isDirectory()) continue;
979
- const index = join5(root, entry.name, "index.db");
1375
+ const index = join6(root, entry.name, "index.db");
980
1376
  if (!existsSync(index)) continue;
981
1377
  const db = openDatabase(index);
982
1378
  try {
@@ -1039,6 +1435,7 @@ var init_cli = __esm({
1039
1435
  init_database();
1040
1436
  init_sync();
1041
1437
  init_validate();
1438
+ init_init();
1042
1439
  }
1043
1440
  });
1044
1441
 
@@ -1059,7 +1456,7 @@ ${issues}`);
1059
1456
  };
1060
1457
  }
1061
1458
  var EnvSchema2;
1062
- var init_config2 = __esm({
1459
+ var init_config = __esm({
1063
1460
  "../mcp/src/config.ts"() {
1064
1461
  "use strict";
1065
1462
  EnvSchema2 = z3.object({
@@ -1091,7 +1488,7 @@ var init_db = __esm({
1091
1488
  // ../mcp/src/lib/diag.ts
1092
1489
  import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
1093
1490
  import { homedir as homedir2 } from "node:os";
1094
- import { join as join6 } from "node:path";
1491
+ import { join as join7 } from "node:path";
1095
1492
  function diag(msg, data) {
1096
1493
  try {
1097
1494
  const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
@@ -1105,8 +1502,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
1105
1502
  var init_diag = __esm({
1106
1503
  "../mcp/src/lib/diag.ts"() {
1107
1504
  "use strict";
1108
- DIAG_DIR = join6(homedir2(), ".local", "state", "wiki-mcp");
1109
- DIAG_LOG_PATH = join6(DIAG_DIR, "server.log");
1505
+ DIAG_DIR = join7(homedir2(), ".local", "state", "wiki-mcp");
1506
+ DIAG_LOG_PATH = join7(DIAG_DIR, "server.log");
1110
1507
  try {
1111
1508
  mkdirSync3(DIAG_DIR, { recursive: true });
1112
1509
  } catch {
@@ -1133,88 +1530,6 @@ var init_logger = __esm({
1133
1530
  }
1134
1531
  });
1135
1532
 
1136
- // ../mcp/src/vault-config.ts
1137
- import { readFile as readFile4 } from "node:fs/promises";
1138
- import { join as join7 } from "node:path";
1139
- import { parse as parse2 } from "yaml";
1140
- import { z as z4 } from "zod";
1141
- function kindName2(entry) {
1142
- return typeof entry === "string" ? entry : entry.name;
1143
- }
1144
- async function loadVaultConfig2(vaultRoot2) {
1145
- const path = join7(vaultRoot2, "vault.yaml");
1146
- let raw;
1147
- try {
1148
- raw = await readFile4(path, "utf8");
1149
- } catch (err) {
1150
- throw new Error(`vault.yaml not found at ${path}`, { cause: err });
1151
- }
1152
- const parsed = VaultConfigSchema2.safeParse(parse2(raw));
1153
- if (!parsed.success) {
1154
- const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1155
- throw new Error(`Invalid vault.yaml at ${path}:
1156
- ${issues}`);
1157
- }
1158
- return parsed.data;
1159
- }
1160
- var ScopeSchema2, KindEntrySchema2, VaultConfigSchema2, BUILT_IN_KINDS2;
1161
- var init_vault_config = __esm({
1162
- "../mcp/src/vault-config.ts"() {
1163
- "use strict";
1164
- ScopeSchema2 = z4.object({
1165
- repo: z4.string().optional(),
1166
- methodology: z4.string().optional(),
1167
- status: z4.string()
1168
- });
1169
- KindEntrySchema2 = z4.union([
1170
- z4.string(),
1171
- z4.object({
1172
- name: z4.string(),
1173
- signal: z4.string(),
1174
- where: z4.string()
1175
- })
1176
- ]);
1177
- VaultConfigSchema2 = z4.object({
1178
- scopes: z4.record(z4.string(), ScopeSchema2),
1179
- kinds: z4.array(KindEntrySchema2),
1180
- statuses: z4.array(z4.string()),
1181
- methodologies: z4.array(z4.string()),
1182
- tags: z4.object({
1183
- canonical: z4.array(z4.string()),
1184
- aliases: z4.record(z4.string(), z4.string())
1185
- }),
1186
- authoring_rules: z4.string().optional(),
1187
- authoring_rules_extra: z4.string().optional(),
1188
- sync_protocol: z4.string().optional(),
1189
- sync_protocol_extra: z4.string().optional()
1190
- }).superRefine((config, ctx) => {
1191
- for (const [name, scope] of Object.entries(config.scopes)) {
1192
- if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
1193
- ctx.addIssue({
1194
- code: "custom",
1195
- path: ["scopes", name, "methodology"],
1196
- message: `"${scope.methodology}" is not in the methodologies list`
1197
- });
1198
- }
1199
- }
1200
- });
1201
- BUILT_IN_KINDS2 = /* @__PURE__ */ new Set([
1202
- "project",
1203
- "spec",
1204
- "adr",
1205
- "plan",
1206
- "story",
1207
- "ops",
1208
- "topic",
1209
- "article",
1210
- "src",
1211
- "note",
1212
- "artifact",
1213
- "prompt"
1214
- ]);
1215
- }
1216
- });
1217
-
1218
1533
  // ../mcp/src/resources/authoring.ts
1219
1534
  function buildAuthoringRules(config) {
1220
1535
  const parts = [(config.authoring_rules ?? DEFAULT_AUTHORING_RULES).trim()];
@@ -1229,13 +1544,13 @@ function buildSyncProtocol(config) {
1229
1544
  function buildKindSelector(kinds) {
1230
1545
  const lines = ["## Kind selector", "", "| Signal | Kind | Where |", "|---|---|---|"];
1231
1546
  for (const entry of kinds) {
1232
- const name = kindName2(entry);
1547
+ const name = kindName(entry);
1233
1548
  const pedagogy = typeof entry === "string" ? KIND_PEDAGOGY.get(entry) : entry;
1234
1549
  const signal = pedagogy?.signal ?? "\u2014";
1235
1550
  const where = pedagogy?.where ?? "\u2014";
1236
1551
  lines.push(`| ${signal} | **${name}** | ${where} |`);
1237
1552
  }
1238
- const names = kinds.map(kindName2);
1553
+ const names = kinds.map(kindName);
1239
1554
  const hasNote = names.includes("note");
1240
1555
  const hasAdrAndSpec = names.includes("adr") && names.includes("spec");
1241
1556
  if (hasNote || hasAdrAndSpec) {
@@ -1258,7 +1573,7 @@ function buildVocabulary(config) {
1258
1573
  const lines = [
1259
1574
  "## Controlled vocabulary",
1260
1575
  "",
1261
- `**Kinds:** ${config.kinds.map(kindName2).join(", ")}`,
1576
+ `**Kinds:** ${config.kinds.map(kindName).join(", ")}`,
1262
1577
  buildStatusLine(config.statuses),
1263
1578
  `**Methodologies:** ${config.methodologies.join(", ")}`,
1264
1579
  `**Canonical tags:** ${config.tags.canonical.join(", ")}`
@@ -1434,7 +1749,7 @@ import { join as join8 } from "node:path";
1434
1749
  function customTemplates(config) {
1435
1750
  const specs = [];
1436
1751
  for (const entry of config.kinds) {
1437
- if (typeof entry === "string" || BUILT_IN_KINDS2.has(entry.name)) continue;
1752
+ if (typeof entry === "string" || BUILT_IN_KINDS.has(entry.name)) continue;
1438
1753
  specs.push({
1439
1754
  uri: `wiki://template/${entry.name}`,
1440
1755
  name: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
@@ -1635,7 +1950,7 @@ var init_toolResponse = __esm({
1635
1950
  });
1636
1951
 
1637
1952
  // ../mcp/src/tools/search.ts
1638
- import { z as z5 } from "zod";
1953
+ import { z as z4 } from "zod";
1639
1954
  function search(deps, input) {
1640
1955
  const ftsQuery = sanitizeFtsQuery(input.query);
1641
1956
  if (!ftsQuery) return { results: [] };
@@ -1682,15 +1997,15 @@ var init_search = __esm({
1682
1997
  "use strict";
1683
1998
  init_fts();
1684
1999
  init_toolResponse();
1685
- SearchInputSchema = z5.object({
1686
- query: z5.string().min(1).describe(
2000
+ SearchInputSchema = z4.object({
2001
+ query: z4.string().min(1).describe(
1687
2002
  "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1688
2003
  ),
1689
- scope: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1690
- kind: z5.string().optional().describe(
2004
+ scope: z4.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
2005
+ kind: z4.string().optional().describe(
1691
2006
  "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1692
2007
  ),
1693
- limit: z5.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
2008
+ limit: z4.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1694
2009
  });
1695
2010
  FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
1696
2011
  }
@@ -1699,7 +2014,7 @@ var init_search = __esm({
1699
2014
  // ../mcp/src/tools/prime.ts
1700
2015
  import { readFile as readFile6 } from "node:fs/promises";
1701
2016
  import { basename as basename3, join as join9 } from "node:path";
1702
- import { z as z6 } from "zod";
2017
+ import { z as z5 } from "zod";
1703
2018
  function pathSlug(p) {
1704
2019
  return basename3(p).replace(/\.md$/, "");
1705
2020
  }
@@ -1842,7 +2157,7 @@ function renderMarkdown(d, config, task) {
1842
2157
  lines.push(countEntries.map(([k, n]) => `${k}: ${n}`).join(" | "));
1843
2158
  }
1844
2159
  lines.push("", "## Vocabulary");
1845
- lines.push(`kinds: ${config.kinds.map(kindName2).join(", ")}`);
2160
+ lines.push(`kinds: ${config.kinds.map(kindName).join(", ")}`);
1846
2161
  lines.push(`statuses: ${config.statuses.join(", ")}`);
1847
2162
  lines.push(`tags: ${config.tags.canonical.join(", ")}`);
1848
2163
  if (d.top_tags.length > 0) {
@@ -1903,14 +2218,14 @@ var init_prime = __esm({
1903
2218
  "../mcp/src/tools/prime.ts"() {
1904
2219
  "use strict";
1905
2220
  init_database();
2221
+ init_vault_config();
1906
2222
  init_frontmatter2();
1907
2223
  init_fts();
1908
2224
  init_toolResponse();
1909
- init_vault_config();
1910
2225
  init_search();
1911
- PrimeInputSchema = z6.object({
1912
- scope: z6.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1913
- task: z6.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
2226
+ PrimeInputSchema = z5.object({
2227
+ scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
2228
+ task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
1914
2229
  });
1915
2230
  }
1916
2231
  });
@@ -1962,7 +2277,7 @@ async function startMcpServer() {
1962
2277
  diag("main entered");
1963
2278
  const config = loadConfig();
1964
2279
  diag("config loaded", { vault: config.wikiVault, level: config.logLevel });
1965
- const vaultConfig = await loadVaultConfig2(config.wikiVault);
2280
+ const vaultConfig = await loadVaultConfig(config.wikiVault);
1966
2281
  diag("vault config loaded", {
1967
2282
  scopes: Object.keys(vaultConfig.scopes).length,
1968
2283
  kinds: vaultConfig.kinds.length,
@@ -2005,36 +2320,594 @@ async function startMcpServer() {
2005
2320
  var init_start = __esm({
2006
2321
  "../mcp/src/start.ts"() {
2007
2322
  "use strict";
2008
- init_config2();
2323
+ init_vault_config();
2324
+ init_config();
2009
2325
  init_db();
2010
2326
  init_diag();
2011
2327
  init_logger();
2012
2328
  init_server();
2329
+ }
2330
+ });
2331
+
2332
+ // ../cli/src/hook.ts
2333
+ var hook_exports = {};
2334
+ __export(hook_exports, {
2335
+ dedupeMatches: () => dedupeMatches,
2336
+ dedupePretoolMatches: () => dedupePretoolMatches,
2337
+ effectiveTriggers: () => effectiveTriggers,
2338
+ evaluateMatches: () => evaluateMatches,
2339
+ hookStateDir: () => hookStateDir,
2340
+ kiroIdePromptEvent: () => kiroIdePromptEvent,
2341
+ loadTriggerFile: () => loadTriggerFile,
2342
+ matchPretoolTriggers: () => matchPretoolTriggers,
2343
+ matchPromptTriggers: () => matchPromptTriggers,
2344
+ parsePretoolEvent: () => parsePretoolEvent,
2345
+ parsePromptEvent: () => parsePromptEvent,
2346
+ renderPosttool: () => renderPosttool,
2347
+ renderPretool: () => renderPretool,
2348
+ resolveScope: () => resolveScope,
2349
+ runHookPosttool: () => runHookPosttool,
2350
+ runHookPretool: () => runHookPretool,
2351
+ runHookPrompt: () => runHookPrompt,
2352
+ vaultPathTouched: () => vaultPathTouched
2353
+ });
2354
+ import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
2355
+ import { homedir as homedir3 } from "node:os";
2356
+ import { join as join10, resolve as resolve3, sep as sep2 } from "node:path";
2357
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
2358
+ import { parseArgs as parseArgs2 } from "node:util";
2359
+ import { parse as parseYaml3 } from "yaml";
2360
+ import { z as z6 } from "zod";
2361
+ function eventFields(raw) {
2362
+ let data;
2363
+ try {
2364
+ data = JSON.parse(raw);
2365
+ } catch {
2366
+ return null;
2367
+ }
2368
+ if (typeof data !== "object" || data === null) return null;
2369
+ return data;
2370
+ }
2371
+ function parsePromptEvent(raw) {
2372
+ const fields = eventFields(raw);
2373
+ if (fields === null) return null;
2374
+ const { session_id, prompt, cwd } = fields;
2375
+ if (typeof session_id !== "string" || typeof prompt !== "string") return null;
2376
+ return { session_id, prompt, ...typeof cwd === "string" && { cwd } };
2377
+ }
2378
+ function kiroIdePromptEvent(now = Date.now()) {
2379
+ const prompt = process.env.USER_PROMPT;
2380
+ if (prompt === void 0 || prompt === "") return null;
2381
+ const cwd = process.cwd();
2382
+ return { session_id: `kiro:${cwd}:${Math.floor(now / KIRO_IDE_BUCKET_MS)}`, prompt, cwd };
2383
+ }
2384
+ function loadTriggerFile(path) {
2385
+ try {
2386
+ const result = z6.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
2387
+ return result.success ? result.data : null;
2388
+ } catch {
2389
+ return null;
2390
+ }
2391
+ }
2392
+ function effectiveTriggers(config, scope, fileTriggers = []) {
2393
+ const replace = scope === void 0 ? void 0 : config.triggers?.[scope];
2394
+ const base = replace ?? [...DEFAULT_TRIGGERS, ...fileTriggers];
2395
+ const allExtras = config.triggers_extra?.[ALL_SCOPES_KEY] ?? [];
2396
+ const scopeExtras = scope === void 0 || scope === ALL_SCOPES_KEY ? [] : config.triggers_extra?.[scope] ?? [];
2397
+ const seen = /* @__PURE__ */ new Set();
2398
+ const triggers = [];
2399
+ const duplicates = [];
2400
+ for (const trigger of [...base, ...allExtras, ...scopeExtras]) {
2401
+ if (seen.has(trigger.id)) {
2402
+ duplicates.push(trigger.id);
2403
+ continue;
2404
+ }
2405
+ seen.add(trigger.id);
2406
+ triggers.push(trigger);
2407
+ }
2408
+ return { triggers, duplicates };
2409
+ }
2410
+ function expandHome(path) {
2411
+ if (path === "~") return homedir3();
2412
+ return path.startsWith("~/") ? join10(homedir3(), path.slice(2)) : path;
2413
+ }
2414
+ function resolveScope(config, cwd) {
2415
+ if (cwd === void 0 || cwd === "") return void 0;
2416
+ let best;
2417
+ let bestLength = -1;
2418
+ for (const [name, scope] of Object.entries(config.scopes)) {
2419
+ if (scope.repo === void 0) continue;
2420
+ const repo = expandHome(scope.repo).replace(/\/+$/, "");
2421
+ if (!repo.startsWith("/")) continue;
2422
+ if (cwd !== repo && !cwd.startsWith(`${repo}/`)) continue;
2423
+ if (repo.length > bestLength) {
2424
+ best = name;
2425
+ bestLength = repo.length;
2426
+ }
2427
+ }
2428
+ return best;
2429
+ }
2430
+ function keywordQuery(keywords) {
2431
+ return keywords.map((keyword) => `"${keyword.replaceAll('"', '""')}"`).join(" OR ");
2432
+ }
2433
+ function openPromptIndex(prompt) {
2434
+ const db = new DatabaseSync2(":memory:");
2435
+ db.exec(`CREATE VIRTUAL TABLE prompt_doc USING fts5(text, tokenize = 'porter unicode61')`);
2436
+ db.prepare("INSERT INTO prompt_doc (text) VALUES (?)").run(prompt);
2437
+ return db;
2438
+ }
2439
+ function matchPromptTriggers(prompt, triggers) {
2440
+ const candidates = triggers.filter(
2441
+ (trigger) => trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0
2442
+ );
2443
+ if (candidates.length === 0) return [];
2444
+ const matches = [];
2445
+ let db = null;
2446
+ try {
2447
+ for (const trigger of candidates) {
2448
+ let hit = false;
2449
+ if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
2450
+ db ??= openPromptIndex(prompt);
2451
+ const row = db.prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
2452
+ hit = row.n > 0;
2453
+ }
2454
+ if (!hit && trigger.intent !== void 0) {
2455
+ hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
2456
+ }
2457
+ if (hit) {
2458
+ matches.push({ id: trigger.id, text: trigger.text });
2459
+ }
2460
+ }
2461
+ } finally {
2462
+ db?.close();
2463
+ }
2464
+ return matches;
2465
+ }
2466
+ function parsePretoolEvent(raw) {
2467
+ const fields = eventFields(raw);
2468
+ if (fields === null) return null;
2469
+ const { session_id, tool_name, tool_input, cwd } = fields;
2470
+ if (typeof session_id !== "string" || typeof tool_name !== "string") return null;
2471
+ return { session_id, tool_name, tool_input, ...typeof cwd === "string" && { cwd } };
2472
+ }
2473
+ function globToRegExp(glob) {
2474
+ let source = "^";
2475
+ let i = 0;
2476
+ while (i < glob.length) {
2477
+ const char = glob[i];
2478
+ if (char === "*") {
2479
+ if (glob.startsWith("**/", i)) {
2480
+ source += "(?:.*/)?";
2481
+ i += 3;
2482
+ } else if (glob.startsWith("**", i)) {
2483
+ source += ".*";
2484
+ i += 2;
2485
+ } else {
2486
+ source += "[^/]*";
2487
+ i += 1;
2488
+ }
2489
+ } else if (char === "?") {
2490
+ source += "[^/]";
2491
+ i += 1;
2492
+ } else {
2493
+ source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2494
+ i += 1;
2495
+ }
2496
+ }
2497
+ return new RegExp(`${source}$`);
2498
+ }
2499
+ function pathCandidates(toolInput, cwd) {
2500
+ if (typeof toolInput !== "object" || toolInput === null) return [];
2501
+ const fields = toolInput;
2502
+ const candidates = [];
2503
+ for (const key of ["file_path", "notebook_path", "path"]) {
2504
+ const value = fields[key];
2505
+ if (typeof value === "string" && value !== "") {
2506
+ candidates.push(value);
2507
+ if (cwd !== void 0 && value.startsWith(`${cwd}/`)) {
2508
+ candidates.push(value.slice(cwd.length + 1));
2509
+ }
2510
+ }
2511
+ }
2512
+ return candidates;
2513
+ }
2514
+ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
2515
+ const matches = [];
2516
+ for (const trigger of triggers) {
2517
+ if (trigger.on !== "pretool") continue;
2518
+ if (trigger.tool !== void 0 && trigger.tool !== toolName) continue;
2519
+ if (trigger.args_match !== void 0) {
2520
+ const serialized = JSON.stringify(toolInput ?? {});
2521
+ if (!new RegExp(trigger.args_match).test(serialized)) continue;
2522
+ }
2523
+ if (trigger.files !== void 0 && trigger.files.length > 0) {
2524
+ const candidates = pathCandidates(toolInput, cwd);
2525
+ const hit = trigger.files.some((glob) => {
2526
+ const regex = globToRegExp(glob);
2527
+ return candidates.some((candidate) => regex.test(candidate));
2528
+ });
2529
+ if (!hit) continue;
2530
+ }
2531
+ const text = trigger.enforce === "block" ? trigger.reason : trigger.text;
2532
+ if (text === void 0) continue;
2533
+ matches.push({
2534
+ id: trigger.id,
2535
+ enforce: trigger.enforce,
2536
+ text,
2537
+ ...trigger.when !== void 0 && { when: trigger.when }
2538
+ });
2539
+ }
2540
+ return matches;
2541
+ }
2542
+ function evaluateMatches(matches, vaultRoot2) {
2543
+ const fired = [];
2544
+ const skipped = [];
2545
+ for (const match of matches) {
2546
+ if (match.when === void 0) {
2547
+ fired.push(match);
2548
+ continue;
2549
+ }
2550
+ const verdict = evaluateWhen(match.when, vaultRoot2);
2551
+ if (verdict === null) skipped.push(match.id);
2552
+ else if (!verdict) fired.push(match);
2553
+ }
2554
+ return { fired, skipped };
2555
+ }
2556
+ function evaluateWhen(when, vaultRoot2) {
2557
+ if (typeof when === "string") return null;
2558
+ try {
2559
+ const than = newestUpdated(vaultRoot2, when.than);
2560
+ if (than === null) return true;
2561
+ const fresh = newestUpdated(vaultRoot2, when.fresh);
2562
+ if (fresh === null) return false;
2563
+ return fresh >= than;
2564
+ } catch {
2565
+ return null;
2566
+ }
2567
+ }
2568
+ function newestUpdated(vaultRoot2, globs) {
2569
+ const regexes = globs.map(globToRegExp);
2570
+ let newest = null;
2571
+ for (const entry of readdirSync2(vaultRoot2, { recursive: true })) {
2572
+ const rel = entry.split(sep2).join("/");
2573
+ if (!rel.endsWith(".md")) continue;
2574
+ if (rel.startsWith(".") || rel.includes("/.")) continue;
2575
+ if (!regexes.some((regex) => regex.test(rel))) continue;
2576
+ const updated = readUpdated(join10(vaultRoot2, entry));
2577
+ if (updated !== null && (newest === null || updated > newest)) {
2578
+ newest = updated;
2579
+ }
2580
+ }
2581
+ return newest;
2582
+ }
2583
+ function readUpdated(path) {
2584
+ try {
2585
+ const { data } = parseFrontmatter(readFileSync(path, "utf8"));
2586
+ const updated = data.updated;
2587
+ if (typeof updated === "string") return updated;
2588
+ if (updated instanceof Date) return updated.toISOString().slice(0, 10);
2589
+ } catch {
2590
+ }
2591
+ return null;
2592
+ }
2593
+ function renderPretool(matches, format) {
2594
+ const block = matches.find((match) => match.enforce === "block");
2595
+ const context = matches.filter((match) => match.enforce === "inject").map((match) => match.text);
2596
+ const warnings = matches.filter((match) => match.enforce === "warn").map((match) => match.text);
2597
+ if (format === "claude") {
2598
+ const hookSpecificOutput = { hookEventName: "PreToolUse" };
2599
+ if (block !== void 0) {
2600
+ hookSpecificOutput.permissionDecision = "deny";
2601
+ hookSpecificOutput.permissionDecisionReason = block.text;
2602
+ }
2603
+ if (context.length > 0) {
2604
+ hookSpecificOutput.additionalContext = context.join("\n");
2605
+ }
2606
+ const decided = block !== void 0 || context.length > 0;
2607
+ return { stdout: decided ? JSON.stringify({ hookSpecificOutput }) : null, stderr: warnings };
2608
+ }
2609
+ if (matches.length === 0) return { stdout: null, stderr: [] };
2610
+ return {
2611
+ stdout: JSON.stringify({
2612
+ decision: block !== void 0 ? "deny" : "none",
2613
+ ...block !== void 0 && { reason: block.text },
2614
+ context,
2615
+ warnings
2616
+ }),
2617
+ stderr: []
2618
+ };
2619
+ }
2620
+ function patchPaths(toolInput) {
2621
+ const fields = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
2622
+ const sources = [toolInput, fields.patch, fields.input, fields.command].filter(
2623
+ (value) => typeof value === "string"
2624
+ );
2625
+ const paths = [];
2626
+ for (const source of sources) {
2627
+ for (const match of source.matchAll(PATCH_FILE_RE)) {
2628
+ paths.push(match[1].trim());
2629
+ }
2630
+ }
2631
+ return paths;
2632
+ }
2633
+ function vaultPathTouched(toolInput, vaultRoot2, cwd) {
2634
+ const root = resolve3(vaultRoot2);
2635
+ const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
2636
+ return candidates.some((candidate) => {
2637
+ const absolute = resolve3(cwd ?? ".", candidate);
2638
+ return absolute === root || absolute.startsWith(`${root}/`);
2639
+ });
2640
+ }
2641
+ function renderPosttool(findings, synced, format) {
2642
+ if (findings.length === 0 && synced) return null;
2643
+ const lines = findings.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
2644
+ if (format === "claude") {
2645
+ if (hasErrors(findings)) {
2646
+ return JSON.stringify({
2647
+ decision: "block",
2648
+ reason: `kmd validate failed \u2014 fix before the index syncs:
2649
+ ${lines.join("\n")}`
2650
+ });
2651
+ }
2652
+ const hookSpecificOutput = { hookEventName: "PostToolUse" };
2653
+ const notes = [...lines];
2654
+ if (!synced) notes.push("kmd sync failed \u2014 index not updated; see hook stderr");
2655
+ hookSpecificOutput.additionalContext = notes.join("\n");
2656
+ return JSON.stringify({ hookSpecificOutput });
2657
+ }
2658
+ return JSON.stringify({ findings, synced });
2659
+ }
2660
+ function dedupePretoolMatches(stateDir, sessionId, matches) {
2661
+ const blocks = matches.filter((match) => match.enforce === "block");
2662
+ const rest = matches.filter((match) => match.enforce !== "block");
2663
+ const fresh = dedupeMatches(stateDir, sessionId, rest);
2664
+ return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
2665
+ }
2666
+ function hookStateDir() {
2667
+ return join10(kmdHome(), "state", "hook");
2668
+ }
2669
+ function dedupeMatches(stateDir, sessionId, matches) {
2670
+ if (matches.length === 0) return [];
2671
+ const file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
2672
+ const fired = readFired(file);
2673
+ const fresh = matches.filter((match) => !fired.has(match.id));
2674
+ if (fresh.length > 0) {
2675
+ mkdirSync4(stateDir, { recursive: true });
2676
+ for (const match of fresh) {
2677
+ fired.add(match.id);
2678
+ }
2679
+ writeFileSync(file, JSON.stringify([...fired]));
2680
+ pruneStale(stateDir, file);
2681
+ }
2682
+ return fresh;
2683
+ }
2684
+ function readFired(file) {
2685
+ try {
2686
+ const data = JSON.parse(readFileSync(file, "utf8"));
2687
+ if (Array.isArray(data)) {
2688
+ return new Set(data.filter((entry) => typeof entry === "string"));
2689
+ }
2690
+ } catch {
2691
+ }
2692
+ return /* @__PURE__ */ new Set();
2693
+ }
2694
+ function pruneStale(stateDir, keep) {
2695
+ try {
2696
+ const cutoff = Date.now() - SESSION_STATE_MAX_AGE_MS;
2697
+ for (const entry of readdirSync2(stateDir)) {
2698
+ const path = join10(stateDir, entry);
2699
+ if (path !== keep && statSync(path).mtimeMs < cutoff) {
2700
+ rmSync2(path, { force: true });
2701
+ }
2702
+ }
2703
+ } catch {
2704
+ }
2705
+ }
2706
+ function hookInvocation() {
2707
+ const { values: values2, positionals: positionals2 } = parseArgs2({
2708
+ args: process.argv.slice(2),
2709
+ allowPositionals: true,
2710
+ strict: false,
2711
+ options: {
2712
+ scope: { type: "string" },
2713
+ harness: { type: "string" },
2714
+ triggers: { type: "string" }
2715
+ }
2716
+ });
2717
+ const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
2718
+ if (vaultRoot2 === void 0 || vaultRoot2 === "") {
2719
+ diag2("no vault root (positional or $WIKI_VAULT)");
2720
+ return null;
2721
+ }
2722
+ return {
2723
+ vaultRoot: vaultRoot2,
2724
+ scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
2725
+ harness: values2.harness,
2726
+ triggersFile: values2.triggers
2727
+ };
2728
+ }
2729
+ function resolveFileTriggers(invocation) {
2730
+ if (typeof invocation.triggersFile !== "string") return [];
2731
+ const loaded = loadTriggerFile(invocation.triggersFile);
2732
+ if (loaded === null) {
2733
+ diag2(`triggers file unreadable or invalid: ${invocation.triggersFile}`);
2734
+ return [];
2735
+ }
2736
+ return loaded;
2737
+ }
2738
+ async function runHookPrompt() {
2739
+ try {
2740
+ const invocation = hookInvocation();
2741
+ if (invocation === null) return;
2742
+ const { vaultRoot: vaultRoot2 } = invocation;
2743
+ let event = null;
2744
+ if (invocation.harness === "kiro-ide") {
2745
+ event = kiroIdePromptEvent();
2746
+ } else if (invocation.harness !== void 0) {
2747
+ diag2(`unknown harness "${String(invocation.harness)}" \u2014 reading the neutral stdin event`);
2748
+ }
2749
+ event ??= parsePromptEvent(await readStdin());
2750
+ if (event === null) {
2751
+ diag2("stdin is not a prompt event ({session_id, prompt})");
2752
+ return;
2753
+ }
2754
+ const config = await loadVaultConfig(vaultRoot2);
2755
+ const scope = invocation.scope ?? resolveScope(config, event.cwd);
2756
+ const { triggers, duplicates } = effectiveTriggers(
2757
+ config,
2758
+ scope,
2759
+ resolveFileTriggers(invocation)
2760
+ );
2761
+ for (const id of duplicates) {
2762
+ diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
2763
+ }
2764
+ const matches = matchPromptTriggers(event.prompt, triggers);
2765
+ for (const match of dedupeMatches(hookStateDir(), event.session_id, matches)) {
2766
+ console.log(match.text);
2767
+ }
2768
+ } catch (err) {
2769
+ diag2(err instanceof Error ? err.message : String(err));
2770
+ }
2771
+ }
2772
+ async function runHookPretool() {
2773
+ try {
2774
+ const invocation = hookInvocation();
2775
+ if (invocation === null) return;
2776
+ const { vaultRoot: vaultRoot2 } = invocation;
2777
+ let format = "neutral";
2778
+ if (invocation.harness === "claude") {
2779
+ format = "claude";
2780
+ } else if (invocation.harness !== void 0) {
2781
+ diag2(`unknown harness "${String(invocation.harness)}" \u2014 emitting the neutral contract`);
2782
+ }
2783
+ const event = parsePretoolEvent(await readStdin());
2784
+ if (event === null) {
2785
+ diag2("stdin is not a pretool event ({session_id, tool_name})");
2786
+ return;
2787
+ }
2788
+ const config = await loadVaultConfig(vaultRoot2);
2789
+ const scope = invocation.scope ?? resolveScope(config, event.cwd);
2790
+ const { triggers, duplicates } = effectiveTriggers(
2791
+ config,
2792
+ scope,
2793
+ resolveFileTriggers(invocation)
2794
+ );
2795
+ for (const id of duplicates) {
2796
+ diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
2797
+ }
2798
+ const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
2799
+ const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
2800
+ for (const id of skipped) {
2801
+ diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
2802
+ }
2803
+ const rendered = renderPretool(
2804
+ dedupePretoolMatches(hookStateDir(), event.session_id, fired),
2805
+ format
2806
+ );
2807
+ for (const line of rendered.stderr) {
2808
+ console.error(line);
2809
+ }
2810
+ if (rendered.stdout !== null) {
2811
+ console.log(rendered.stdout);
2812
+ }
2813
+ } catch (err) {
2814
+ diag2(err instanceof Error ? err.message : String(err));
2815
+ }
2816
+ }
2817
+ async function runHookPosttool() {
2818
+ try {
2819
+ const invocation = hookInvocation();
2820
+ if (invocation === null) return;
2821
+ let format = "neutral";
2822
+ if (invocation.harness === "claude") {
2823
+ format = "claude";
2824
+ } else if (invocation.harness !== void 0) {
2825
+ diag2(`unknown harness "${String(invocation.harness)}" \u2014 emitting the neutral contract`);
2826
+ }
2827
+ const event = parsePretoolEvent(await readStdin());
2828
+ if (event === null) {
2829
+ diag2("stdin is not a posttool event ({session_id, tool_name})");
2830
+ return;
2831
+ }
2832
+ if (!vaultPathTouched(event.tool_input, invocation.vaultRoot, event.cwd)) return;
2833
+ const findings = await validateVault(invocation.vaultRoot);
2834
+ let synced = false;
2835
+ if (!hasErrors(findings)) {
2836
+ try {
2837
+ await syncVault(invocation.vaultRoot);
2838
+ synced = true;
2839
+ } catch (err) {
2840
+ diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
2841
+ }
2842
+ }
2843
+ const rendered = renderPosttool(findings, synced, format);
2844
+ if (rendered !== null) {
2845
+ console.log(rendered);
2846
+ }
2847
+ } catch (err) {
2848
+ diag2(err instanceof Error ? err.message : String(err));
2849
+ }
2850
+ }
2851
+ async function readStdin() {
2852
+ process.stdin.setEncoding("utf8");
2853
+ let input = "";
2854
+ for await (const chunk of process.stdin) {
2855
+ input += chunk;
2856
+ }
2857
+ return input;
2858
+ }
2859
+ function diag2(message) {
2860
+ console.error(`kmd hook: ${message}`);
2861
+ }
2862
+ var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE;
2863
+ var init_hook = __esm({
2864
+ "../cli/src/hook.ts"() {
2865
+ "use strict";
2866
+ init_database();
2013
2867
  init_vault_config();
2868
+ init_frontmatter();
2869
+ init_sync();
2870
+ init_validate();
2871
+ DEFAULT_TRIGGERS = [];
2872
+ SESSION_STATE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2873
+ KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
2874
+ ALL_SCOPES_KEY = "_all";
2875
+ PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
2014
2876
  }
2015
2877
  });
2016
2878
 
2017
2879
  // bin/kmd.ts
2018
- import { parseArgs as parseArgs2 } from "node:util";
2880
+ import { parseArgs as parseArgs3 } from "node:util";
2881
+ process.removeAllListeners("warning");
2882
+ process.on("warning", (warning) => {
2883
+ if (warning.name !== "ExperimentalWarning") {
2884
+ console.error(warning.stack ?? `${warning.name}: ${warning.message}`);
2885
+ }
2886
+ });
2019
2887
  var USAGE = `usage: kmd <command> [options]
2020
2888
 
2021
2889
  commands:
2890
+ init [<dir>] [-y] scaffold a fresh vault (no dir: current directory \u2014 TTY prompt, or -y)
2022
2891
  sync vault \u2192 index sync (runs validate first)
2023
2892
  validate [<path>] deterministic vault checker (default: $WIKI_VAULT)
2024
2893
  mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
2025
2894
  config [<vault-root>] print vault + index resolution; with no vault, list known vaults
2026
2895
  db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
2896
+ hook <prompt|pretool|posttool> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
2897
+ harness gate engine: JSON event on stdin, decision/context on stdout;
2898
+ posttool auto-runs validate + sync after a vault write
2027
2899
 
2028
2900
  options:
2029
2901
  --version print version
2030
2902
  --help show this help`;
2031
- var { positionals, values } = parseArgs2({
2903
+ var { positionals, values } = parseArgs3({
2032
2904
  args: process.argv.slice(2),
2033
2905
  allowPositionals: true,
2034
2906
  strict: false,
2035
2907
  options: {
2036
2908
  version: { type: "boolean", short: "v" },
2037
- help: { type: "boolean", short: "h" }
2909
+ help: { type: "boolean", short: "h" },
2910
+ yes: { type: "boolean", short: "y" }
2038
2911
  }
2039
2912
  });
2040
2913
  var command = values.version ? "--version" : values.help ? "--help" : positionals[0];
@@ -2046,6 +2919,11 @@ function applyVaultRoot(positionalIndex) {
2046
2919
  }
2047
2920
  async function run() {
2048
2921
  switch (command) {
2922
+ case "init": {
2923
+ const { runInit: runInit2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2924
+ await runInit2(positionals[1], Boolean(values.yes));
2925
+ break;
2926
+ }
2049
2927
  case "sync": {
2050
2928
  const { runSyncCommand: runSyncCommand2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
2051
2929
  await runSyncCommand2();
@@ -2081,13 +2959,34 @@ async function run() {
2081
2959
  }
2082
2960
  break;
2083
2961
  }
2962
+ case "hook": {
2963
+ const sub = positionals[1];
2964
+ if (sub === "prompt") {
2965
+ const { runHookPrompt: runHookPrompt2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
2966
+ await runHookPrompt2();
2967
+ } else if (sub === "pretool") {
2968
+ const { runHookPretool: runHookPretool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
2969
+ await runHookPretool2();
2970
+ } else if (sub === "posttool") {
2971
+ const { runHookPosttool: runHookPosttool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
2972
+ await runHookPosttool2();
2973
+ } else if (sub) {
2974
+ console.error(`kmd hook: unknown event: ${sub}`);
2975
+ } else {
2976
+ console.error(
2977
+ "usage: kmd hook <prompt|pretool|posttool> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
2978
+ );
2979
+ process.exit(2);
2980
+ }
2981
+ break;
2982
+ }
2084
2983
  case "--version":
2085
2984
  case "-v": {
2086
- const { readFileSync } = await import("node:fs");
2087
- const { join: join10, dirname: dirname4 } = await import("node:path");
2985
+ const { readFileSync: readFileSync2 } = await import("node:fs");
2986
+ const { join: join11, dirname: dirname4 } = await import("node:path");
2088
2987
  const { fileURLToPath } = await import("node:url");
2089
2988
  const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
2090
- const pkg = JSON.parse(readFileSync(join10(pkgDir, "package.json"), "utf8"));
2989
+ const pkg = JSON.parse(readFileSync2(join11(pkgDir, "package.json"), "utf8"));
2091
2990
  console.log(pkg.version);
2092
2991
  break;
2093
2992
  }
@@ -2099,6 +2998,11 @@ async function run() {
2099
2998
  break;
2100
2999
  }
2101
3000
  default: {
3001
+ const tail = positionals[1];
3002
+ if (tail === "prompt" || tail === "pretool" || tail === "posttool") {
3003
+ console.error(`kmd: unknown command: ${command}`);
3004
+ break;
3005
+ }
2102
3006
  console.error(`unknown command: ${command}
2103
3007
 
2104
3008
  ${USAGE}`);