@bartolli/kmd 0.5.0 → 0.6.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 {
@@ -119,6 +121,13 @@ 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
  }
@@ -138,7 +147,7 @@ ${issues}`);
138
147
  }
139
148
  return parsed.data;
140
149
  }
141
- var ScopeSchema, KindEntrySchema, VaultConfigSchema, BUILT_IN_KINDS;
150
+ var ScopeSchema, KindEntrySchema, WhenSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
142
151
  var init_config = __esm({
143
152
  "../cli/src/config.ts"() {
144
153
  "use strict";
@@ -155,6 +164,63 @@ var init_config = __esm({
155
164
  where: z.string()
156
165
  })
157
166
  ]);
167
+ WhenSchema = z.union([
168
+ z.string(),
169
+ z.object({
170
+ name: z.enum(["newer-than"]),
171
+ fresh: z.array(z.string().min(1)).min(1),
172
+ than: z.array(z.string().min(1)).min(1)
173
+ })
174
+ ]);
175
+ TriggerSchema = z.object({
176
+ id: z.string().min(1),
177
+ on: z.enum(["prompt", "pretool"]),
178
+ enforce: z.enum(["inject", "warn", "block"]),
179
+ keywords: z.array(z.string().min(1)).optional(),
180
+ intent: z.array(z.string()).optional(),
181
+ tool: z.string().optional(),
182
+ args_match: z.string().optional(),
183
+ files: z.array(z.string().min(1)).optional(),
184
+ when: WhenSchema.optional(),
185
+ text: z.string().optional(),
186
+ reason: z.string().optional()
187
+ }).superRefine((trigger, ctx) => {
188
+ if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
189
+ ctx.addIssue({
190
+ code: "custom",
191
+ message: `prompt trigger "${trigger.id}" needs keywords or intent`
192
+ });
193
+ }
194
+ if (trigger.on === "pretool" && trigger.tool === void 0 && trigger.args_match === void 0 && !trigger.files?.length) {
195
+ ctx.addIssue({
196
+ code: "custom",
197
+ message: `pretool trigger "${trigger.id}" needs a tool, args_match, or files matcher`
198
+ });
199
+ }
200
+ if (trigger.on === "prompt" && trigger.files !== void 0) {
201
+ ctx.addIssue({
202
+ code: "custom",
203
+ message: `trigger "${trigger.id}": files applies to pretool triggers only`
204
+ });
205
+ }
206
+ if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
207
+ ctx.addIssue({
208
+ code: "custom",
209
+ message: trigger.enforce === "block" ? `block trigger "${trigger.id}" needs a reason` : `${trigger.enforce} trigger "${trigger.id}" needs a text`
210
+ });
211
+ }
212
+ const patterns = [...trigger.intent ?? []];
213
+ if (trigger.args_match !== void 0) patterns.push(trigger.args_match);
214
+ for (const pattern of patterns) {
215
+ if (!isValidRegex(pattern)) {
216
+ ctx.addIssue({
217
+ code: "custom",
218
+ message: `trigger "${trigger.id}" has an invalid regex: ${pattern}`
219
+ });
220
+ }
221
+ }
222
+ });
223
+ TriggersSchema = z.record(z.string(), z.array(TriggerSchema));
158
224
  VaultConfigSchema = z.object({
159
225
  scopes: z.record(z.string(), ScopeSchema),
160
226
  kinds: z.array(KindEntrySchema),
@@ -167,7 +233,9 @@ var init_config = __esm({
167
233
  authoring_rules: z.string().optional(),
168
234
  authoring_rules_extra: z.string().optional(),
169
235
  sync_protocol: z.string().optional(),
170
- sync_protocol_extra: z.string().optional()
236
+ sync_protocol_extra: z.string().optional(),
237
+ triggers: TriggersSchema.optional(),
238
+ triggers_extra: TriggersSchema.optional()
171
239
  }).superRefine((config, ctx) => {
172
240
  for (const [name, scope] of Object.entries(config.scopes)) {
173
241
  if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
@@ -178,6 +246,28 @@ var init_config = __esm({
178
246
  });
179
247
  }
180
248
  }
249
+ if (config.triggers?._all !== void 0) {
250
+ ctx.addIssue({
251
+ code: "custom",
252
+ path: ["triggers", "_all"],
253
+ message: '"_all" is reserved for triggers_extra'
254
+ });
255
+ }
256
+ for (const field of ["triggers", "triggers_extra"]) {
257
+ for (const [scope, list] of Object.entries(config[field] ?? {})) {
258
+ const seen = /* @__PURE__ */ new Set();
259
+ for (const trigger of list) {
260
+ if (seen.has(trigger.id)) {
261
+ ctx.addIssue({
262
+ code: "custom",
263
+ path: [field, scope],
264
+ message: `duplicate trigger id "${trigger.id}"`
265
+ });
266
+ }
267
+ seen.add(trigger.id);
268
+ }
269
+ }
270
+ }
181
271
  });
182
272
  BUILT_IN_KINDS = /* @__PURE__ */ new Set([
183
273
  "project",
@@ -373,25 +463,23 @@ function syncPage(db, fields) {
373
463
  }
374
464
  return "changed";
375
465
  }
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);
466
+ async function syncVault(vaultRoot2) {
467
+ const dbPath = resolveIndexPath(vaultRoot2);
468
+ const vaultConfig = await loadVaultConfig(vaultRoot2);
381
469
  const scopes = new Set(Object.keys(vaultConfig.scopes));
382
470
  mkdirSync(dirname(dbPath), { recursive: true });
383
471
  const db = openDatabase(dbPath);
384
472
  try {
385
473
  const files = [];
386
474
  for (const domain of SCAN_DOMAINS) {
387
- files.push(...await walkMarkdown(env.WIKI_VAULT, domain));
475
+ files.push(...await walkMarkdown(vaultRoot2, domain));
388
476
  }
389
477
  const indexedPaths = [];
390
478
  let changed = 0;
391
479
  let unchanged = 0;
392
480
  let skipped = 0;
393
481
  for (const file of files) {
394
- const path = toRelativePath(env.WIKI_VAULT, file);
482
+ const path = toRelativePath(vaultRoot2, file);
395
483
  const raw = await readFile2(file, "utf8");
396
484
  const parsed = parseFrontmatter(raw);
397
485
  const fields = buildPageFields(path, raw, parsed, scopes);
@@ -415,19 +503,33 @@ async function runSync() {
415
503
  pagesDeleted = Number(pageResult.changes);
416
504
  const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
417
505
  linksDeleted = Number(linkResult.changes);
418
- } else {
419
- console.warn("no indexable pages found; skipping orphan deletion (safety)");
420
506
  }
421
507
  db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
422
- setMeta(db, "vault_root", canonicalVaultRoot(env.WIKI_VAULT));
508
+ setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
423
509
  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
- );
510
+ return {
511
+ changed,
512
+ unchanged,
513
+ skipped,
514
+ pagesDeleted,
515
+ linksDeleted,
516
+ noPages: indexedPaths.length === 0
517
+ };
427
518
  } finally {
428
519
  db.close();
429
520
  }
430
521
  }
522
+ async function runSync() {
523
+ const env = loadEnv();
524
+ console.log(`sync: ${env.WIKI_VAULT} \u2192 ${resolveIndexPath(env.WIKI_VAULT)}`);
525
+ const stats = await syncVault(env.WIKI_VAULT);
526
+ if (stats.noPages) {
527
+ console.warn("no indexable pages found; skipping orphan deletion (safety)");
528
+ }
529
+ console.log(
530
+ `done: ${stats.changed} changed, ${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.pagesDeleted} pages deleted, ${stats.linksDeleted} link orphans cleared`
531
+ );
532
+ }
431
533
  var EnvSchema, SCAN_DOMAINS, WIKILINK_RE, INDEXED_FRONTMATTER_KEYS;
432
534
  var init_sync = __esm({
433
535
  "../cli/src/sync.ts"() {
@@ -1138,6 +1240,13 @@ import { readFile as readFile4 } from "node:fs/promises";
1138
1240
  import { join as join7 } from "node:path";
1139
1241
  import { parse as parse2 } from "yaml";
1140
1242
  import { z as z4 } from "zod";
1243
+ function isValidRegex2(pattern) {
1244
+ try {
1245
+ return Boolean(new RegExp(pattern));
1246
+ } catch {
1247
+ return false;
1248
+ }
1249
+ }
1141
1250
  function kindName2(entry) {
1142
1251
  return typeof entry === "string" ? entry : entry.name;
1143
1252
  }
@@ -1157,7 +1266,7 @@ ${issues}`);
1157
1266
  }
1158
1267
  return parsed.data;
1159
1268
  }
1160
- var ScopeSchema2, KindEntrySchema2, VaultConfigSchema2, BUILT_IN_KINDS2;
1269
+ var ScopeSchema2, KindEntrySchema2, WhenSchema2, TriggerSchema2, TriggersSchema2, VaultConfigSchema2, BUILT_IN_KINDS2;
1161
1270
  var init_vault_config = __esm({
1162
1271
  "../mcp/src/vault-config.ts"() {
1163
1272
  "use strict";
@@ -1174,6 +1283,63 @@ var init_vault_config = __esm({
1174
1283
  where: z4.string()
1175
1284
  })
1176
1285
  ]);
1286
+ WhenSchema2 = z4.union([
1287
+ z4.string(),
1288
+ z4.object({
1289
+ name: z4.enum(["newer-than"]),
1290
+ fresh: z4.array(z4.string().min(1)).min(1),
1291
+ than: z4.array(z4.string().min(1)).min(1)
1292
+ })
1293
+ ]);
1294
+ TriggerSchema2 = z4.object({
1295
+ id: z4.string().min(1),
1296
+ on: z4.enum(["prompt", "pretool"]),
1297
+ enforce: z4.enum(["inject", "warn", "block"]),
1298
+ keywords: z4.array(z4.string().min(1)).optional(),
1299
+ intent: z4.array(z4.string()).optional(),
1300
+ tool: z4.string().optional(),
1301
+ args_match: z4.string().optional(),
1302
+ files: z4.array(z4.string().min(1)).optional(),
1303
+ when: WhenSchema2.optional(),
1304
+ text: z4.string().optional(),
1305
+ reason: z4.string().optional()
1306
+ }).superRefine((trigger, ctx) => {
1307
+ if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
1308
+ ctx.addIssue({
1309
+ code: "custom",
1310
+ message: `prompt trigger "${trigger.id}" needs keywords or intent`
1311
+ });
1312
+ }
1313
+ if (trigger.on === "pretool" && trigger.tool === void 0 && trigger.args_match === void 0 && !trigger.files?.length) {
1314
+ ctx.addIssue({
1315
+ code: "custom",
1316
+ message: `pretool trigger "${trigger.id}" needs a tool, args_match, or files matcher`
1317
+ });
1318
+ }
1319
+ if (trigger.on === "prompt" && trigger.files !== void 0) {
1320
+ ctx.addIssue({
1321
+ code: "custom",
1322
+ message: `trigger "${trigger.id}": files applies to pretool triggers only`
1323
+ });
1324
+ }
1325
+ if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
1326
+ ctx.addIssue({
1327
+ code: "custom",
1328
+ message: trigger.enforce === "block" ? `block trigger "${trigger.id}" needs a reason` : `${trigger.enforce} trigger "${trigger.id}" needs a text`
1329
+ });
1330
+ }
1331
+ const patterns = [...trigger.intent ?? []];
1332
+ if (trigger.args_match !== void 0) patterns.push(trigger.args_match);
1333
+ for (const pattern of patterns) {
1334
+ if (!isValidRegex2(pattern)) {
1335
+ ctx.addIssue({
1336
+ code: "custom",
1337
+ message: `trigger "${trigger.id}" has an invalid regex: ${pattern}`
1338
+ });
1339
+ }
1340
+ }
1341
+ });
1342
+ TriggersSchema2 = z4.record(z4.string(), z4.array(TriggerSchema2));
1177
1343
  VaultConfigSchema2 = z4.object({
1178
1344
  scopes: z4.record(z4.string(), ScopeSchema2),
1179
1345
  kinds: z4.array(KindEntrySchema2),
@@ -1186,7 +1352,9 @@ var init_vault_config = __esm({
1186
1352
  authoring_rules: z4.string().optional(),
1187
1353
  authoring_rules_extra: z4.string().optional(),
1188
1354
  sync_protocol: z4.string().optional(),
1189
- sync_protocol_extra: z4.string().optional()
1355
+ sync_protocol_extra: z4.string().optional(),
1356
+ triggers: TriggersSchema2.optional(),
1357
+ triggers_extra: TriggersSchema2.optional()
1190
1358
  }).superRefine((config, ctx) => {
1191
1359
  for (const [name, scope] of Object.entries(config.scopes)) {
1192
1360
  if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
@@ -1197,6 +1365,28 @@ var init_vault_config = __esm({
1197
1365
  });
1198
1366
  }
1199
1367
  }
1368
+ if (config.triggers?._all !== void 0) {
1369
+ ctx.addIssue({
1370
+ code: "custom",
1371
+ path: ["triggers", "_all"],
1372
+ message: '"_all" is reserved for triggers_extra'
1373
+ });
1374
+ }
1375
+ for (const field of ["triggers", "triggers_extra"]) {
1376
+ for (const [scope, list] of Object.entries(config[field] ?? {})) {
1377
+ const seen = /* @__PURE__ */ new Set();
1378
+ for (const trigger of list) {
1379
+ if (seen.has(trigger.id)) {
1380
+ ctx.addIssue({
1381
+ code: "custom",
1382
+ path: [field, scope],
1383
+ message: `duplicate trigger id "${trigger.id}"`
1384
+ });
1385
+ }
1386
+ seen.add(trigger.id);
1387
+ }
1388
+ }
1389
+ }
1200
1390
  });
1201
1391
  BUILT_IN_KINDS2 = /* @__PURE__ */ new Set([
1202
1392
  "project",
@@ -1634,10 +1824,72 @@ var init_toolResponse = __esm({
1634
1824
  }
1635
1825
  });
1636
1826
 
1827
+ // ../mcp/src/tools/search.ts
1828
+ import { z as z5 } from "zod";
1829
+ function search(deps, input) {
1830
+ const ftsQuery = sanitizeFtsQuery(input.query);
1831
+ if (!ftsQuery) return { results: [] };
1832
+ let sql = `SELECT p.path, p.title, p.kind, p.summary, p.scope, ${FTS_RANK} AS score
1833
+ FROM pages_fts
1834
+ JOIN pages p ON p.id = pages_fts.rowid
1835
+ WHERE pages_fts MATCH ?`;
1836
+ const params = [ftsQuery];
1837
+ if (input.scope) {
1838
+ sql += " AND p.scope = ?";
1839
+ params.push(input.scope);
1840
+ }
1841
+ if (input.kind) {
1842
+ sql += " AND p.kind = ?";
1843
+ params.push(input.kind);
1844
+ }
1845
+ sql += ` ORDER BY ${FTS_RANK} LIMIT ?`;
1846
+ params.push(input.limit);
1847
+ const rows = deps.db.prepare(sql).all(...params);
1848
+ return {
1849
+ results: rows.map((r) => ({
1850
+ path: r.path,
1851
+ title: r.title,
1852
+ kind: r.kind,
1853
+ summary: r.summary,
1854
+ scope: r.scope,
1855
+ score: r.score
1856
+ }))
1857
+ };
1858
+ }
1859
+ function handleSearch(deps, input) {
1860
+ try {
1861
+ return textJson(search(deps, input));
1862
+ } catch (err) {
1863
+ return textError({
1864
+ code: "SEARCH_FAILED",
1865
+ message: err instanceof Error ? err.message : String(err)
1866
+ });
1867
+ }
1868
+ }
1869
+ var SearchInputSchema, FTS_RANK;
1870
+ var init_search = __esm({
1871
+ "../mcp/src/tools/search.ts"() {
1872
+ "use strict";
1873
+ init_fts();
1874
+ init_toolResponse();
1875
+ SearchInputSchema = z5.object({
1876
+ query: z5.string().min(1).describe(
1877
+ "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1878
+ ),
1879
+ scope: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1880
+ kind: z5.string().optional().describe(
1881
+ "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1882
+ ),
1883
+ limit: z5.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1884
+ });
1885
+ FTS_RANK = "bm25(pages_fts, 10.0, 5.0, 1.0)";
1886
+ }
1887
+ });
1888
+
1637
1889
  // ../mcp/src/tools/prime.ts
1638
1890
  import { readFile as readFile6 } from "node:fs/promises";
1639
1891
  import { basename as basename3, join as join9 } from "node:path";
1640
- import { z as z5 } from "zod";
1892
+ import { z as z6 } from "zod";
1641
1893
  function pathSlug(p) {
1642
1894
  return basename3(p).replace(/\.md$/, "");
1643
1895
  }
@@ -1700,11 +1952,11 @@ async function prime(deps, input) {
1700
1952
  const ftsQuery = sanitizeFtsQuery(task);
1701
1953
  if (ftsQuery) {
1702
1954
  relevant = db.prepare(
1703
- `SELECT p.path, p.title, bm25(pages_fts) AS score
1955
+ `SELECT p.path, p.title, ${FTS_RANK} AS score
1704
1956
  FROM pages_fts
1705
1957
  JOIN pages p ON p.id = pages_fts.rowid
1706
1958
  WHERE pages_fts MATCH ? AND p.scope = ?
1707
- ORDER BY bm25(pages_fts) LIMIT 3`
1959
+ ORDER BY ${FTS_RANK} LIMIT 3`
1708
1960
  ).all(ftsQuery, scope).map((row) => ({
1709
1961
  path: row.path,
1710
1962
  title: row.title,
@@ -1845,70 +2097,10 @@ var init_prime = __esm({
1845
2097
  init_fts();
1846
2098
  init_toolResponse();
1847
2099
  init_vault_config();
1848
- PrimeInputSchema = z5.object({
1849
- scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1850
- task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
1851
- });
1852
- }
1853
- });
1854
-
1855
- // ../mcp/src/tools/search.ts
1856
- import { z as z6 } from "zod";
1857
- function search(deps, input) {
1858
- const ftsQuery = sanitizeFtsQuery(input.query);
1859
- if (!ftsQuery) return { results: [] };
1860
- let sql = `SELECT p.path, p.title, p.kind, p.summary, p.scope, bm25(pages_fts) AS score
1861
- FROM pages_fts
1862
- JOIN pages p ON p.id = pages_fts.rowid
1863
- WHERE pages_fts MATCH ?`;
1864
- const params = [ftsQuery];
1865
- if (input.scope) {
1866
- sql += " AND p.scope = ?";
1867
- params.push(input.scope);
1868
- }
1869
- if (input.kind) {
1870
- sql += " AND p.kind = ?";
1871
- params.push(input.kind);
1872
- }
1873
- sql += " ORDER BY bm25(pages_fts) LIMIT ?";
1874
- params.push(input.limit);
1875
- const rows = deps.db.prepare(sql).all(...params);
1876
- return {
1877
- results: rows.map((r) => ({
1878
- path: r.path,
1879
- title: r.title,
1880
- kind: r.kind,
1881
- summary: r.summary,
1882
- scope: r.scope,
1883
- score: r.score
1884
- }))
1885
- };
1886
- }
1887
- function handleSearch(deps, input) {
1888
- try {
1889
- return textJson(search(deps, input));
1890
- } catch (err) {
1891
- return textError({
1892
- code: "SEARCH_FAILED",
1893
- message: err instanceof Error ? err.message : String(err)
1894
- });
1895
- }
1896
- }
1897
- var SearchInputSchema;
1898
- var init_search = __esm({
1899
- "../mcp/src/tools/search.ts"() {
1900
- "use strict";
1901
- init_fts();
1902
- init_toolResponse();
1903
- SearchInputSchema = z6.object({
1904
- query: z6.string().min(1).describe(
1905
- "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1906
- ),
1907
- scope: z6.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1908
- kind: z6.string().optional().describe(
1909
- "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1910
- ),
1911
- limit: z6.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
2100
+ init_search();
2101
+ PrimeInputSchema = z6.object({
2102
+ scope: z6.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
2103
+ task: z6.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
1912
2104
  });
1913
2105
  }
1914
2106
  });
@@ -2012,8 +2204,561 @@ var init_start = __esm({
2012
2204
  }
2013
2205
  });
2014
2206
 
2015
- // bin/kmd.ts
2207
+ // ../cli/src/hook.ts
2208
+ var hook_exports = {};
2209
+ __export(hook_exports, {
2210
+ dedupeMatches: () => dedupeMatches,
2211
+ dedupePretoolMatches: () => dedupePretoolMatches,
2212
+ effectiveTriggers: () => effectiveTriggers,
2213
+ evaluateMatches: () => evaluateMatches,
2214
+ hookStateDir: () => hookStateDir,
2215
+ kiroIdePromptEvent: () => kiroIdePromptEvent,
2216
+ loadTriggerFile: () => loadTriggerFile,
2217
+ matchPretoolTriggers: () => matchPretoolTriggers,
2218
+ matchPromptTriggers: () => matchPromptTriggers,
2219
+ parsePretoolEvent: () => parsePretoolEvent,
2220
+ parsePromptEvent: () => parsePromptEvent,
2221
+ renderPosttool: () => renderPosttool,
2222
+ renderPretool: () => renderPretool,
2223
+ resolveScope: () => resolveScope,
2224
+ runHookPosttool: () => runHookPosttool,
2225
+ runHookPretool: () => runHookPretool,
2226
+ runHookPrompt: () => runHookPrompt,
2227
+ vaultPathTouched: () => vaultPathTouched
2228
+ });
2229
+ import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
2230
+ import { homedir as homedir3 } from "node:os";
2231
+ import { join as join10, resolve as resolve2, sep as sep2 } from "node:path";
2232
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
2016
2233
  import { parseArgs as parseArgs2 } from "node:util";
2234
+ import { parse as parseYaml3 } from "yaml";
2235
+ import { z as z7 } from "zod";
2236
+ function eventFields(raw) {
2237
+ let data;
2238
+ try {
2239
+ data = JSON.parse(raw);
2240
+ } catch {
2241
+ return null;
2242
+ }
2243
+ if (typeof data !== "object" || data === null) return null;
2244
+ return data;
2245
+ }
2246
+ function parsePromptEvent(raw) {
2247
+ const fields = eventFields(raw);
2248
+ if (fields === null) return null;
2249
+ const { session_id, prompt, cwd } = fields;
2250
+ if (typeof session_id !== "string" || typeof prompt !== "string") return null;
2251
+ return { session_id, prompt, ...typeof cwd === "string" && { cwd } };
2252
+ }
2253
+ function kiroIdePromptEvent(now = Date.now()) {
2254
+ const prompt = process.env.USER_PROMPT;
2255
+ if (prompt === void 0 || prompt === "") return null;
2256
+ const cwd = process.cwd();
2257
+ return { session_id: `kiro:${cwd}:${Math.floor(now / KIRO_IDE_BUCKET_MS)}`, prompt, cwd };
2258
+ }
2259
+ function loadTriggerFile(path) {
2260
+ try {
2261
+ const result = z7.array(TriggerSchema).safeParse(parseYaml3(readFileSync(path, "utf8")));
2262
+ return result.success ? result.data : null;
2263
+ } catch {
2264
+ return null;
2265
+ }
2266
+ }
2267
+ function effectiveTriggers(config, scope, fileTriggers = []) {
2268
+ const replace = scope === void 0 ? void 0 : config.triggers?.[scope];
2269
+ const base = replace ?? [...DEFAULT_TRIGGERS, ...fileTriggers];
2270
+ const allExtras = config.triggers_extra?.[ALL_SCOPES_KEY] ?? [];
2271
+ const scopeExtras = scope === void 0 || scope === ALL_SCOPES_KEY ? [] : config.triggers_extra?.[scope] ?? [];
2272
+ const seen = /* @__PURE__ */ new Set();
2273
+ const triggers = [];
2274
+ const duplicates = [];
2275
+ for (const trigger of [...base, ...allExtras, ...scopeExtras]) {
2276
+ if (seen.has(trigger.id)) {
2277
+ duplicates.push(trigger.id);
2278
+ continue;
2279
+ }
2280
+ seen.add(trigger.id);
2281
+ triggers.push(trigger);
2282
+ }
2283
+ return { triggers, duplicates };
2284
+ }
2285
+ function expandHome(path) {
2286
+ if (path === "~") return homedir3();
2287
+ return path.startsWith("~/") ? join10(homedir3(), path.slice(2)) : path;
2288
+ }
2289
+ function resolveScope(config, cwd) {
2290
+ if (cwd === void 0 || cwd === "") return void 0;
2291
+ let best;
2292
+ let bestLength = -1;
2293
+ for (const [name, scope] of Object.entries(config.scopes)) {
2294
+ if (scope.repo === void 0) continue;
2295
+ const repo = expandHome(scope.repo).replace(/\/+$/, "");
2296
+ if (!repo.startsWith("/")) continue;
2297
+ if (cwd !== repo && !cwd.startsWith(`${repo}/`)) continue;
2298
+ if (repo.length > bestLength) {
2299
+ best = name;
2300
+ bestLength = repo.length;
2301
+ }
2302
+ }
2303
+ return best;
2304
+ }
2305
+ function keywordQuery(keywords) {
2306
+ return keywords.map((keyword) => `"${keyword.replaceAll('"', '""')}"`).join(" OR ");
2307
+ }
2308
+ function openPromptIndex(prompt) {
2309
+ const db = new DatabaseSync2(":memory:");
2310
+ db.exec(`CREATE VIRTUAL TABLE prompt_doc USING fts5(text, tokenize = 'porter unicode61')`);
2311
+ db.prepare("INSERT INTO prompt_doc (text) VALUES (?)").run(prompt);
2312
+ return db;
2313
+ }
2314
+ function matchPromptTriggers(prompt, triggers) {
2315
+ const candidates = triggers.filter(
2316
+ (trigger) => trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0
2317
+ );
2318
+ if (candidates.length === 0) return [];
2319
+ const matches = [];
2320
+ let db = null;
2321
+ try {
2322
+ for (const trigger of candidates) {
2323
+ let hit = false;
2324
+ if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
2325
+ db ??= openPromptIndex(prompt);
2326
+ const row = db.prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
2327
+ hit = row.n > 0;
2328
+ }
2329
+ if (!hit && trigger.intent !== void 0) {
2330
+ hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
2331
+ }
2332
+ if (hit) {
2333
+ matches.push({ id: trigger.id, text: trigger.text });
2334
+ }
2335
+ }
2336
+ } finally {
2337
+ db?.close();
2338
+ }
2339
+ return matches;
2340
+ }
2341
+ function parsePretoolEvent(raw) {
2342
+ const fields = eventFields(raw);
2343
+ if (fields === null) return null;
2344
+ const { session_id, tool_name, tool_input, cwd } = fields;
2345
+ if (typeof session_id !== "string" || typeof tool_name !== "string") return null;
2346
+ return { session_id, tool_name, tool_input, ...typeof cwd === "string" && { cwd } };
2347
+ }
2348
+ function globToRegExp(glob) {
2349
+ let source = "^";
2350
+ let i = 0;
2351
+ while (i < glob.length) {
2352
+ const char = glob[i];
2353
+ if (char === "*") {
2354
+ if (glob.startsWith("**/", i)) {
2355
+ source += "(?:.*/)?";
2356
+ i += 3;
2357
+ } else if (glob.startsWith("**", i)) {
2358
+ source += ".*";
2359
+ i += 2;
2360
+ } else {
2361
+ source += "[^/]*";
2362
+ i += 1;
2363
+ }
2364
+ } else if (char === "?") {
2365
+ source += "[^/]";
2366
+ i += 1;
2367
+ } else {
2368
+ source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2369
+ i += 1;
2370
+ }
2371
+ }
2372
+ return new RegExp(`${source}$`);
2373
+ }
2374
+ function pathCandidates(toolInput, cwd) {
2375
+ if (typeof toolInput !== "object" || toolInput === null) return [];
2376
+ const fields = toolInput;
2377
+ const candidates = [];
2378
+ for (const key of ["file_path", "notebook_path", "path"]) {
2379
+ const value = fields[key];
2380
+ if (typeof value === "string" && value !== "") {
2381
+ candidates.push(value);
2382
+ if (cwd !== void 0 && value.startsWith(`${cwd}/`)) {
2383
+ candidates.push(value.slice(cwd.length + 1));
2384
+ }
2385
+ }
2386
+ }
2387
+ return candidates;
2388
+ }
2389
+ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
2390
+ const matches = [];
2391
+ for (const trigger of triggers) {
2392
+ if (trigger.on !== "pretool") continue;
2393
+ if (trigger.tool !== void 0 && trigger.tool !== toolName) continue;
2394
+ if (trigger.args_match !== void 0) {
2395
+ const serialized = JSON.stringify(toolInput ?? {});
2396
+ if (!new RegExp(trigger.args_match).test(serialized)) continue;
2397
+ }
2398
+ if (trigger.files !== void 0 && trigger.files.length > 0) {
2399
+ const candidates = pathCandidates(toolInput, cwd);
2400
+ const hit = trigger.files.some((glob) => {
2401
+ const regex = globToRegExp(glob);
2402
+ return candidates.some((candidate) => regex.test(candidate));
2403
+ });
2404
+ if (!hit) continue;
2405
+ }
2406
+ const text = trigger.enforce === "block" ? trigger.reason : trigger.text;
2407
+ if (text === void 0) continue;
2408
+ matches.push({
2409
+ id: trigger.id,
2410
+ enforce: trigger.enforce,
2411
+ text,
2412
+ ...trigger.when !== void 0 && { when: trigger.when }
2413
+ });
2414
+ }
2415
+ return matches;
2416
+ }
2417
+ function evaluateMatches(matches, vaultRoot2) {
2418
+ const fired = [];
2419
+ const skipped = [];
2420
+ for (const match of matches) {
2421
+ if (match.when === void 0) {
2422
+ fired.push(match);
2423
+ continue;
2424
+ }
2425
+ const verdict = evaluateWhen(match.when, vaultRoot2);
2426
+ if (verdict === null) skipped.push(match.id);
2427
+ else if (!verdict) fired.push(match);
2428
+ }
2429
+ return { fired, skipped };
2430
+ }
2431
+ function evaluateWhen(when, vaultRoot2) {
2432
+ if (typeof when === "string") return null;
2433
+ try {
2434
+ const than = newestUpdated(vaultRoot2, when.than);
2435
+ if (than === null) return true;
2436
+ const fresh = newestUpdated(vaultRoot2, when.fresh);
2437
+ if (fresh === null) return false;
2438
+ return fresh >= than;
2439
+ } catch {
2440
+ return null;
2441
+ }
2442
+ }
2443
+ function newestUpdated(vaultRoot2, globs) {
2444
+ const regexes = globs.map(globToRegExp);
2445
+ let newest = null;
2446
+ for (const entry of readdirSync2(vaultRoot2, { recursive: true })) {
2447
+ const rel = entry.split(sep2).join("/");
2448
+ if (!rel.endsWith(".md")) continue;
2449
+ if (rel.startsWith(".") || rel.includes("/.")) continue;
2450
+ if (!regexes.some((regex) => regex.test(rel))) continue;
2451
+ const updated = readUpdated(join10(vaultRoot2, entry));
2452
+ if (updated !== null && (newest === null || updated > newest)) {
2453
+ newest = updated;
2454
+ }
2455
+ }
2456
+ return newest;
2457
+ }
2458
+ function readUpdated(path) {
2459
+ try {
2460
+ const { data } = parseFrontmatter(readFileSync(path, "utf8"));
2461
+ const updated = data.updated;
2462
+ if (typeof updated === "string") return updated;
2463
+ if (updated instanceof Date) return updated.toISOString().slice(0, 10);
2464
+ } catch {
2465
+ }
2466
+ return null;
2467
+ }
2468
+ function renderPretool(matches, format) {
2469
+ const block = matches.find((match) => match.enforce === "block");
2470
+ const context = matches.filter((match) => match.enforce === "inject").map((match) => match.text);
2471
+ const warnings = matches.filter((match) => match.enforce === "warn").map((match) => match.text);
2472
+ if (format === "claude") {
2473
+ const hookSpecificOutput = { hookEventName: "PreToolUse" };
2474
+ if (block !== void 0) {
2475
+ hookSpecificOutput.permissionDecision = "deny";
2476
+ hookSpecificOutput.permissionDecisionReason = block.text;
2477
+ }
2478
+ if (context.length > 0) {
2479
+ hookSpecificOutput.additionalContext = context.join("\n");
2480
+ }
2481
+ const decided = block !== void 0 || context.length > 0;
2482
+ return { stdout: decided ? JSON.stringify({ hookSpecificOutput }) : null, stderr: warnings };
2483
+ }
2484
+ if (matches.length === 0) return { stdout: null, stderr: [] };
2485
+ return {
2486
+ stdout: JSON.stringify({
2487
+ decision: block !== void 0 ? "deny" : "none",
2488
+ ...block !== void 0 && { reason: block.text },
2489
+ context,
2490
+ warnings
2491
+ }),
2492
+ stderr: []
2493
+ };
2494
+ }
2495
+ function patchPaths(toolInput) {
2496
+ const fields = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
2497
+ const sources = [toolInput, fields.patch, fields.input, fields.command].filter(
2498
+ (value) => typeof value === "string"
2499
+ );
2500
+ const paths = [];
2501
+ for (const source of sources) {
2502
+ for (const match of source.matchAll(PATCH_FILE_RE)) {
2503
+ paths.push(match[1].trim());
2504
+ }
2505
+ }
2506
+ return paths;
2507
+ }
2508
+ function vaultPathTouched(toolInput, vaultRoot2, cwd) {
2509
+ const root = resolve2(vaultRoot2);
2510
+ const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
2511
+ return candidates.some((candidate) => {
2512
+ const absolute = resolve2(cwd ?? ".", candidate);
2513
+ return absolute === root || absolute.startsWith(`${root}/`);
2514
+ });
2515
+ }
2516
+ function renderPosttool(findings, synced, format) {
2517
+ if (findings.length === 0 && synced) return null;
2518
+ const lines = findings.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
2519
+ if (format === "claude") {
2520
+ if (hasErrors(findings)) {
2521
+ return JSON.stringify({
2522
+ decision: "block",
2523
+ reason: `kmd validate failed \u2014 fix before the index syncs:
2524
+ ${lines.join("\n")}`
2525
+ });
2526
+ }
2527
+ const hookSpecificOutput = { hookEventName: "PostToolUse" };
2528
+ const notes = [...lines];
2529
+ if (!synced) notes.push("kmd sync failed \u2014 index not updated; see hook stderr");
2530
+ hookSpecificOutput.additionalContext = notes.join("\n");
2531
+ return JSON.stringify({ hookSpecificOutput });
2532
+ }
2533
+ return JSON.stringify({ findings, synced });
2534
+ }
2535
+ function dedupePretoolMatches(stateDir, sessionId, matches) {
2536
+ const blocks = matches.filter((match) => match.enforce === "block");
2537
+ const rest = matches.filter((match) => match.enforce !== "block");
2538
+ const fresh = dedupeMatches(stateDir, sessionId, rest);
2539
+ return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
2540
+ }
2541
+ function hookStateDir() {
2542
+ return join10(kmdHome(), "state", "hook");
2543
+ }
2544
+ function dedupeMatches(stateDir, sessionId, matches) {
2545
+ if (matches.length === 0) return [];
2546
+ const file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
2547
+ const fired = readFired(file);
2548
+ const fresh = matches.filter((match) => !fired.has(match.id));
2549
+ if (fresh.length > 0) {
2550
+ mkdirSync4(stateDir, { recursive: true });
2551
+ for (const match of fresh) {
2552
+ fired.add(match.id);
2553
+ }
2554
+ writeFileSync(file, JSON.stringify([...fired]));
2555
+ pruneStale(stateDir, file);
2556
+ }
2557
+ return fresh;
2558
+ }
2559
+ function readFired(file) {
2560
+ try {
2561
+ const data = JSON.parse(readFileSync(file, "utf8"));
2562
+ if (Array.isArray(data)) {
2563
+ return new Set(data.filter((entry) => typeof entry === "string"));
2564
+ }
2565
+ } catch {
2566
+ }
2567
+ return /* @__PURE__ */ new Set();
2568
+ }
2569
+ function pruneStale(stateDir, keep) {
2570
+ try {
2571
+ const cutoff = Date.now() - SESSION_STATE_MAX_AGE_MS;
2572
+ for (const entry of readdirSync2(stateDir)) {
2573
+ const path = join10(stateDir, entry);
2574
+ if (path !== keep && statSync(path).mtimeMs < cutoff) {
2575
+ rmSync2(path, { force: true });
2576
+ }
2577
+ }
2578
+ } catch {
2579
+ }
2580
+ }
2581
+ function hookInvocation() {
2582
+ const { values: values2, positionals: positionals2 } = parseArgs2({
2583
+ args: process.argv.slice(2),
2584
+ allowPositionals: true,
2585
+ strict: false,
2586
+ options: {
2587
+ scope: { type: "string" },
2588
+ harness: { type: "string" },
2589
+ triggers: { type: "string" }
2590
+ }
2591
+ });
2592
+ const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
2593
+ if (vaultRoot2 === void 0 || vaultRoot2 === "") {
2594
+ diag2("no vault root (positional or $WIKI_VAULT)");
2595
+ return null;
2596
+ }
2597
+ return {
2598
+ vaultRoot: vaultRoot2,
2599
+ scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
2600
+ harness: values2.harness,
2601
+ triggersFile: values2.triggers
2602
+ };
2603
+ }
2604
+ function resolveFileTriggers(invocation) {
2605
+ if (typeof invocation.triggersFile !== "string") return [];
2606
+ const loaded = loadTriggerFile(invocation.triggersFile);
2607
+ if (loaded === null) {
2608
+ diag2(`triggers file unreadable or invalid: ${invocation.triggersFile}`);
2609
+ return [];
2610
+ }
2611
+ return loaded;
2612
+ }
2613
+ async function runHookPrompt() {
2614
+ try {
2615
+ const invocation = hookInvocation();
2616
+ if (invocation === null) return;
2617
+ const { vaultRoot: vaultRoot2 } = invocation;
2618
+ let event = null;
2619
+ if (invocation.harness === "kiro-ide") {
2620
+ event = kiroIdePromptEvent();
2621
+ } else if (invocation.harness !== void 0) {
2622
+ diag2(`unknown harness "${String(invocation.harness)}" \u2014 reading the neutral stdin event`);
2623
+ }
2624
+ event ??= parsePromptEvent(await readStdin());
2625
+ if (event === null) {
2626
+ diag2("stdin is not a prompt event ({session_id, prompt})");
2627
+ return;
2628
+ }
2629
+ const config = await loadVaultConfig(vaultRoot2);
2630
+ const scope = invocation.scope ?? resolveScope(config, event.cwd);
2631
+ const { triggers, duplicates } = effectiveTriggers(
2632
+ config,
2633
+ scope,
2634
+ resolveFileTriggers(invocation)
2635
+ );
2636
+ for (const id of duplicates) {
2637
+ diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
2638
+ }
2639
+ const matches = matchPromptTriggers(event.prompt, triggers);
2640
+ for (const match of dedupeMatches(hookStateDir(), event.session_id, matches)) {
2641
+ console.log(match.text);
2642
+ }
2643
+ } catch (err) {
2644
+ diag2(err instanceof Error ? err.message : String(err));
2645
+ }
2646
+ }
2647
+ async function runHookPretool() {
2648
+ try {
2649
+ const invocation = hookInvocation();
2650
+ if (invocation === null) return;
2651
+ const { vaultRoot: vaultRoot2 } = invocation;
2652
+ let format = "neutral";
2653
+ if (invocation.harness === "claude") {
2654
+ format = "claude";
2655
+ } else if (invocation.harness !== void 0) {
2656
+ diag2(`unknown harness "${String(invocation.harness)}" \u2014 emitting the neutral contract`);
2657
+ }
2658
+ const event = parsePretoolEvent(await readStdin());
2659
+ if (event === null) {
2660
+ diag2("stdin is not a pretool event ({session_id, tool_name})");
2661
+ return;
2662
+ }
2663
+ const config = await loadVaultConfig(vaultRoot2);
2664
+ const scope = invocation.scope ?? resolveScope(config, event.cwd);
2665
+ const { triggers, duplicates } = effectiveTriggers(
2666
+ config,
2667
+ scope,
2668
+ resolveFileTriggers(invocation)
2669
+ );
2670
+ for (const id of duplicates) {
2671
+ diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
2672
+ }
2673
+ const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
2674
+ const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
2675
+ for (const id of skipped) {
2676
+ diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
2677
+ }
2678
+ const rendered = renderPretool(
2679
+ dedupePretoolMatches(hookStateDir(), event.session_id, fired),
2680
+ format
2681
+ );
2682
+ for (const line of rendered.stderr) {
2683
+ console.error(line);
2684
+ }
2685
+ if (rendered.stdout !== null) {
2686
+ console.log(rendered.stdout);
2687
+ }
2688
+ } catch (err) {
2689
+ diag2(err instanceof Error ? err.message : String(err));
2690
+ }
2691
+ }
2692
+ async function runHookPosttool() {
2693
+ try {
2694
+ const invocation = hookInvocation();
2695
+ if (invocation === null) return;
2696
+ let format = "neutral";
2697
+ if (invocation.harness === "claude") {
2698
+ format = "claude";
2699
+ } else if (invocation.harness !== void 0) {
2700
+ diag2(`unknown harness "${String(invocation.harness)}" \u2014 emitting the neutral contract`);
2701
+ }
2702
+ const event = parsePretoolEvent(await readStdin());
2703
+ if (event === null) {
2704
+ diag2("stdin is not a posttool event ({session_id, tool_name})");
2705
+ return;
2706
+ }
2707
+ if (!vaultPathTouched(event.tool_input, invocation.vaultRoot, event.cwd)) return;
2708
+ const findings = await validateVault(invocation.vaultRoot);
2709
+ let synced = false;
2710
+ if (!hasErrors(findings)) {
2711
+ try {
2712
+ await syncVault(invocation.vaultRoot);
2713
+ synced = true;
2714
+ } catch (err) {
2715
+ diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
2716
+ }
2717
+ }
2718
+ const rendered = renderPosttool(findings, synced, format);
2719
+ if (rendered !== null) {
2720
+ console.log(rendered);
2721
+ }
2722
+ } catch (err) {
2723
+ diag2(err instanceof Error ? err.message : String(err));
2724
+ }
2725
+ }
2726
+ async function readStdin() {
2727
+ process.stdin.setEncoding("utf8");
2728
+ let input = "";
2729
+ for await (const chunk of process.stdin) {
2730
+ input += chunk;
2731
+ }
2732
+ return input;
2733
+ }
2734
+ function diag2(message) {
2735
+ console.error(`kmd hook: ${message}`);
2736
+ }
2737
+ var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE;
2738
+ var init_hook = __esm({
2739
+ "../cli/src/hook.ts"() {
2740
+ "use strict";
2741
+ init_database();
2742
+ init_config();
2743
+ init_frontmatter();
2744
+ init_sync();
2745
+ init_validate();
2746
+ DEFAULT_TRIGGERS = [];
2747
+ SESSION_STATE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2748
+ KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
2749
+ ALL_SCOPES_KEY = "_all";
2750
+ PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
2751
+ }
2752
+ });
2753
+
2754
+ // bin/kmd.ts
2755
+ import { parseArgs as parseArgs3 } from "node:util";
2756
+ process.removeAllListeners("warning");
2757
+ process.on("warning", (warning) => {
2758
+ if (warning.name !== "ExperimentalWarning") {
2759
+ console.error(warning.stack ?? `${warning.name}: ${warning.message}`);
2760
+ }
2761
+ });
2017
2762
  var USAGE = `usage: kmd <command> [options]
2018
2763
 
2019
2764
  commands:
@@ -2022,11 +2767,14 @@ commands:
2022
2767
  mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
2023
2768
  config [<vault-root>] print vault + index resolution; with no vault, list known vaults
2024
2769
  db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
2770
+ hook <prompt|pretool|posttool> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
2771
+ harness gate engine: JSON event on stdin, decision/context on stdout;
2772
+ posttool auto-runs validate + sync after a vault write
2025
2773
 
2026
2774
  options:
2027
2775
  --version print version
2028
2776
  --help show this help`;
2029
- var { positionals, values } = parseArgs2({
2777
+ var { positionals, values } = parseArgs3({
2030
2778
  args: process.argv.slice(2),
2031
2779
  allowPositionals: true,
2032
2780
  strict: false,
@@ -2079,13 +2827,32 @@ async function run() {
2079
2827
  }
2080
2828
  break;
2081
2829
  }
2830
+ case "hook": {
2831
+ const sub = positionals[1];
2832
+ if (sub === "prompt") {
2833
+ const { runHookPrompt: runHookPrompt2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
2834
+ await runHookPrompt2();
2835
+ } else if (sub === "pretool") {
2836
+ const { runHookPretool: runHookPretool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
2837
+ await runHookPretool2();
2838
+ } else if (sub === "posttool") {
2839
+ const { runHookPosttool: runHookPosttool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
2840
+ await runHookPosttool2();
2841
+ } else {
2842
+ console.error(
2843
+ sub ? `unknown hook event: ${sub}` : "usage: kmd hook <prompt|pretool|posttool> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
2844
+ );
2845
+ process.exit(2);
2846
+ }
2847
+ break;
2848
+ }
2082
2849
  case "--version":
2083
2850
  case "-v": {
2084
- const { readFileSync } = await import("node:fs");
2085
- const { join: join10, dirname: dirname4 } = await import("node:path");
2851
+ const { readFileSync: readFileSync2 } = await import("node:fs");
2852
+ const { join: join11, dirname: dirname4 } = await import("node:path");
2086
2853
  const { fileURLToPath } = await import("node:url");
2087
2854
  const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
2088
- const pkg = JSON.parse(readFileSync(join10(pkgDir, "package.json"), "utf8"));
2855
+ const pkg = JSON.parse(readFileSync2(join11(pkgDir, "package.json"), "utf8"));
2089
2856
  console.log(pkg.version);
2090
2857
  break;
2091
2858
  }