@bartolli/kmd 0.5.1 → 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 +789 -24
- package/dist/kmd.mjs.map +4 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
|
377
|
-
const
|
|
378
|
-
const
|
|
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(
|
|
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(
|
|
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(
|
|
508
|
+
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
|
|
423
509
|
setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
|
|
424
|
-
|
|
425
|
-
|
|
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",
|
|
@@ -2014,8 +2204,561 @@ var init_start = __esm({
|
|
|
2014
2204
|
}
|
|
2015
2205
|
});
|
|
2016
2206
|
|
|
2017
|
-
//
|
|
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";
|
|
2018
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
|
+
});
|
|
2019
2762
|
var USAGE = `usage: kmd <command> [options]
|
|
2020
2763
|
|
|
2021
2764
|
commands:
|
|
@@ -2024,11 +2767,14 @@ commands:
|
|
|
2024
2767
|
mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
|
|
2025
2768
|
config [<vault-root>] print vault + index resolution; with no vault, list known vaults
|
|
2026
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
|
|
2027
2773
|
|
|
2028
2774
|
options:
|
|
2029
2775
|
--version print version
|
|
2030
2776
|
--help show this help`;
|
|
2031
|
-
var { positionals, values } =
|
|
2777
|
+
var { positionals, values } = parseArgs3({
|
|
2032
2778
|
args: process.argv.slice(2),
|
|
2033
2779
|
allowPositionals: true,
|
|
2034
2780
|
strict: false,
|
|
@@ -2081,13 +2827,32 @@ async function run() {
|
|
|
2081
2827
|
}
|
|
2082
2828
|
break;
|
|
2083
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
|
+
}
|
|
2084
2849
|
case "--version":
|
|
2085
2850
|
case "-v": {
|
|
2086
|
-
const { readFileSync } = await import("node:fs");
|
|
2087
|
-
const { join:
|
|
2851
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
2852
|
+
const { join: join11, dirname: dirname4 } = await import("node:path");
|
|
2088
2853
|
const { fileURLToPath } = await import("node:url");
|
|
2089
2854
|
const pkgDir = dirname4(dirname4(fileURLToPath(import.meta.url)));
|
|
2090
|
-
const pkg = JSON.parse(
|
|
2855
|
+
const pkg = JSON.parse(readFileSync2(join11(pkgDir, "package.json"), "utf8"));
|
|
2091
2856
|
console.log(pkg.version);
|
|
2092
2857
|
break;
|
|
2093
2858
|
}
|