@bartolli/kmd 0.8.0 → 0.10.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 +303 -103
- package/dist/kmd.mjs.map +3 -3
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -150,7 +150,7 @@ ${issues}`);
|
|
|
150
150
|
}
|
|
151
151
|
return parsed.data;
|
|
152
152
|
}
|
|
153
|
-
var ScopeSchema, KindEntrySchema, WhenSchema, DedupSchema, TriggerSchema, TriggersSchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
153
|
+
var ScopeSchema, KindEntrySchema, WhenSchema, DedupSchema, TriggerSchema, TriggersSchema, BuiltinHooksSchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
154
154
|
var init_vault_config = __esm({
|
|
155
155
|
"../db/src/vault-config.ts"() {
|
|
156
156
|
"use strict";
|
|
@@ -241,6 +241,15 @@ var init_vault_config = __esm({
|
|
|
241
241
|
}
|
|
242
242
|
});
|
|
243
243
|
TriggersSchema = z.record(z.string(), z.array(TriggerSchema));
|
|
244
|
+
BuiltinHooksSchema = z.strictObject({
|
|
245
|
+
resync: z.strictObject({
|
|
246
|
+
reason: z.string().min(1).optional().describe("Validate-errors preamble; the engine appends the error lines."),
|
|
247
|
+
text: z.string().min(1).optional().describe("Sync-failed note.")
|
|
248
|
+
}).optional(),
|
|
249
|
+
"handoff-gate": z.strictObject({
|
|
250
|
+
reason: z.string().min(1).optional().describe("Stop-block preamble; the engine appends the error lines.")
|
|
251
|
+
}).optional()
|
|
252
|
+
});
|
|
244
253
|
VaultConfigSchema = z.strictObject({
|
|
245
254
|
scopes: z.record(z.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
|
|
246
255
|
kinds: z.array(KindEntrySchema).describe(
|
|
@@ -259,6 +268,9 @@ var init_vault_config = __esm({
|
|
|
259
268
|
triggers: TriggersSchema.optional().describe(
|
|
260
269
|
'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
|
|
261
270
|
),
|
|
271
|
+
builtin_hooks: BuiltinHooksSchema.optional().describe(
|
|
272
|
+
"Message overrides for the fixed-function hooks (resync, handoff-gate) by public id."
|
|
273
|
+
),
|
|
262
274
|
triggers_extra: TriggersSchema.optional().describe(
|
|
263
275
|
'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
|
|
264
276
|
)
|
|
@@ -797,15 +809,11 @@ async function syncVault(vaultRoot2) {
|
|
|
797
809
|
}
|
|
798
810
|
indexedPaths.push(path);
|
|
799
811
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
pagesDeleted = Number(pageResult.changes);
|
|
806
|
-
const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
|
|
807
|
-
linksDeleted = Number(linkResult.changes);
|
|
808
|
-
}
|
|
812
|
+
const placeholders = indexedPaths.map(() => "?").join(", ");
|
|
813
|
+
const pageResult = indexedPaths.length > 0 ? db.prepare(`DELETE FROM pages WHERE path NOT IN (${placeholders})`).run(...indexedPaths) : db.prepare("DELETE FROM pages").run();
|
|
814
|
+
const pagesDeleted = Number(pageResult.changes);
|
|
815
|
+
const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
|
|
816
|
+
const linksDeleted = Number(linkResult.changes);
|
|
809
817
|
db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
|
|
810
818
|
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
|
|
811
819
|
setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -829,7 +837,7 @@ async function runSync() {
|
|
|
829
837
|
console.log(`sync: ${env.WIKI_VAULT} \u2192 ${resolveIndexPath(env.WIKI_VAULT)}`);
|
|
830
838
|
const stats = await syncVault(env.WIKI_VAULT);
|
|
831
839
|
if (stats.noPages) {
|
|
832
|
-
console.warn("no indexable pages found;
|
|
840
|
+
console.warn("no indexable pages found; index swept empty");
|
|
833
841
|
}
|
|
834
842
|
console.log(
|
|
835
843
|
`done: ${stats.changed} changed, ${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.pagesDeleted} pages deleted, ${stats.linksDeleted} link orphans cleared`
|
|
@@ -2349,6 +2357,8 @@ __export(hook_exports, {
|
|
|
2349
2357
|
dedupePretoolMatches: () => dedupePretoolMatches,
|
|
2350
2358
|
effectiveTriggers: () => effectiveTriggers,
|
|
2351
2359
|
evaluateMatches: () => evaluateMatches,
|
|
2360
|
+
explainPretool: () => explainPretool,
|
|
2361
|
+
explainPrompt: () => explainPrompt,
|
|
2352
2362
|
hookStateDir: () => hookStateDir,
|
|
2353
2363
|
kiroIdePromptEvent: () => kiroIdePromptEvent,
|
|
2354
2364
|
loadTriggerFile: () => loadTriggerFile,
|
|
@@ -2359,6 +2369,7 @@ __export(hook_exports, {
|
|
|
2359
2369
|
parseStopEvent: () => parseStopEvent,
|
|
2360
2370
|
renderPosttool: () => renderPosttool,
|
|
2361
2371
|
renderPretool: () => renderPretool,
|
|
2372
|
+
renderPrompt: () => renderPrompt,
|
|
2362
2373
|
renderStop: () => renderStop,
|
|
2363
2374
|
resolveScope: () => resolveScope,
|
|
2364
2375
|
runHookPosttool: () => runHookPosttool,
|
|
@@ -2452,37 +2463,50 @@ function openPromptIndex(prompt) {
|
|
|
2452
2463
|
db.prepare("INSERT INTO prompt_doc (text) VALUES (?)").run(prompt);
|
|
2453
2464
|
return db;
|
|
2454
2465
|
}
|
|
2466
|
+
function promptProbe(trigger, prompt, getDb) {
|
|
2467
|
+
let keywords = "unset";
|
|
2468
|
+
if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
|
|
2469
|
+
const row = getDb().prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
|
|
2470
|
+
keywords = row.n > 0 ? "hit" : "miss";
|
|
2471
|
+
}
|
|
2472
|
+
let intent = "unset";
|
|
2473
|
+
if (trigger.intent !== void 0) {
|
|
2474
|
+
intent = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt)) ? "hit" : "miss";
|
|
2475
|
+
}
|
|
2476
|
+
return { keywords, intent };
|
|
2477
|
+
}
|
|
2478
|
+
function promptHit(probe) {
|
|
2479
|
+
return probe.keywords === "hit" || probe.intent === "hit";
|
|
2480
|
+
}
|
|
2481
|
+
function isInjectTrigger(trigger) {
|
|
2482
|
+
return trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0;
|
|
2483
|
+
}
|
|
2455
2484
|
function matchPromptTriggers(prompt, triggers) {
|
|
2456
|
-
const candidates = triggers.filter(
|
|
2457
|
-
(trigger) => trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0
|
|
2458
|
-
);
|
|
2485
|
+
const candidates = triggers.filter(isInjectTrigger);
|
|
2459
2486
|
if (candidates.length === 0) return [];
|
|
2460
2487
|
const matches = [];
|
|
2461
|
-
|
|
2488
|
+
const state = { db: null };
|
|
2489
|
+
const getDb = () => state.db ??= openPromptIndex(prompt);
|
|
2462
2490
|
try {
|
|
2463
2491
|
for (const trigger of candidates) {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
}
|
|
2470
|
-
if (!hit && trigger.intent !== void 0) {
|
|
2471
|
-
hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
|
|
2472
|
-
}
|
|
2473
|
-
if (hit) {
|
|
2474
|
-
matches.push({
|
|
2475
|
-
id: trigger.id,
|
|
2476
|
-
text: trigger.text,
|
|
2477
|
-
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2478
|
-
});
|
|
2479
|
-
}
|
|
2492
|
+
if (!promptHit(promptProbe(trigger, prompt, getDb))) continue;
|
|
2493
|
+
matches.push({
|
|
2494
|
+
id: trigger.id,
|
|
2495
|
+
text: trigger.text,
|
|
2496
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2497
|
+
});
|
|
2480
2498
|
}
|
|
2481
2499
|
} finally {
|
|
2482
|
-
db?.close();
|
|
2500
|
+
state.db?.close();
|
|
2483
2501
|
}
|
|
2484
2502
|
return matches;
|
|
2485
2503
|
}
|
|
2504
|
+
function uniqueTexts(texts) {
|
|
2505
|
+
return [...new Set(texts)];
|
|
2506
|
+
}
|
|
2507
|
+
function renderPrompt(matches) {
|
|
2508
|
+
return uniqueTexts(matches.map((match) => match.text));
|
|
2509
|
+
}
|
|
2486
2510
|
function parsePretoolEvent(raw) {
|
|
2487
2511
|
const fields = eventFields(raw);
|
|
2488
2512
|
if (fields === null) return null;
|
|
@@ -2531,32 +2555,41 @@ function pathCandidates(toolInput, cwd) {
|
|
|
2531
2555
|
}
|
|
2532
2556
|
return candidates;
|
|
2533
2557
|
}
|
|
2558
|
+
function pretoolStage(trigger, toolName, toolInput, cwd) {
|
|
2559
|
+
if (trigger.tool !== void 0 && trigger.tool !== toolName) return "tool";
|
|
2560
|
+
if (trigger.args_match !== void 0) {
|
|
2561
|
+
const serialized = JSON.stringify(toolInput ?? {});
|
|
2562
|
+
if (!new RegExp(trigger.args_match).test(serialized)) return "args";
|
|
2563
|
+
}
|
|
2564
|
+
if (trigger.files !== void 0 && trigger.files.length > 0) {
|
|
2565
|
+
const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
|
|
2566
|
+
const hit = trigger.files.some((glob) => {
|
|
2567
|
+
const regex = globToRegExp(glob);
|
|
2568
|
+
return candidates.some((candidate) => regex.test(candidate));
|
|
2569
|
+
});
|
|
2570
|
+
if (!hit) return "files";
|
|
2571
|
+
}
|
|
2572
|
+
if (pretoolText(trigger) === void 0) return "payload";
|
|
2573
|
+
return "hit";
|
|
2574
|
+
}
|
|
2575
|
+
function pretoolText(trigger) {
|
|
2576
|
+
return trigger.enforce === "block" ? trigger.reason : trigger.text;
|
|
2577
|
+
}
|
|
2578
|
+
function pretoolMatch(trigger) {
|
|
2579
|
+
return {
|
|
2580
|
+
id: trigger.id,
|
|
2581
|
+
enforce: trigger.enforce,
|
|
2582
|
+
text: pretoolText(trigger),
|
|
2583
|
+
...trigger.when !== void 0 && { when: trigger.when },
|
|
2584
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2534
2587
|
function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
2535
2588
|
const matches = [];
|
|
2536
2589
|
for (const trigger of triggers) {
|
|
2537
2590
|
if (trigger.on !== "pretool") continue;
|
|
2538
|
-
if (trigger
|
|
2539
|
-
|
|
2540
|
-
const serialized = JSON.stringify(toolInput ?? {});
|
|
2541
|
-
if (!new RegExp(trigger.args_match).test(serialized)) continue;
|
|
2542
|
-
}
|
|
2543
|
-
if (trigger.files !== void 0 && trigger.files.length > 0) {
|
|
2544
|
-
const candidates = pathCandidates(toolInput, cwd);
|
|
2545
|
-
const hit = trigger.files.some((glob) => {
|
|
2546
|
-
const regex = globToRegExp(glob);
|
|
2547
|
-
return candidates.some((candidate) => regex.test(candidate));
|
|
2548
|
-
});
|
|
2549
|
-
if (!hit) continue;
|
|
2550
|
-
}
|
|
2551
|
-
const text = trigger.enforce === "block" ? trigger.reason : trigger.text;
|
|
2552
|
-
if (text === void 0) continue;
|
|
2553
|
-
matches.push({
|
|
2554
|
-
id: trigger.id,
|
|
2555
|
-
enforce: trigger.enforce,
|
|
2556
|
-
text,
|
|
2557
|
-
...trigger.when !== void 0 && { when: trigger.when },
|
|
2558
|
-
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2559
|
-
});
|
|
2591
|
+
if (pretoolStage(trigger, toolName, toolInput, cwd) !== "hit") continue;
|
|
2592
|
+
matches.push(pretoolMatch(trigger));
|
|
2560
2593
|
}
|
|
2561
2594
|
return matches;
|
|
2562
2595
|
}
|
|
@@ -2574,18 +2607,23 @@ function evaluateMatches(matches, vaultRoot2) {
|
|
|
2574
2607
|
}
|
|
2575
2608
|
return { fired, skipped };
|
|
2576
2609
|
}
|
|
2577
|
-
function
|
|
2578
|
-
if (typeof when === "string") return
|
|
2610
|
+
function evaluateWhenVerdict(when, vaultRoot2) {
|
|
2611
|
+
if (typeof when === "string") return "unknown";
|
|
2579
2612
|
try {
|
|
2580
2613
|
const than = newestUpdated(vaultRoot2, when.than);
|
|
2581
|
-
if (than === null) return
|
|
2614
|
+
if (than === null) return "vacuous";
|
|
2582
2615
|
const fresh = newestUpdated(vaultRoot2, when.fresh);
|
|
2583
|
-
if (fresh === null) return
|
|
2584
|
-
return fresh >= than;
|
|
2616
|
+
if (fresh === null) return "unmet";
|
|
2617
|
+
return fresh >= than ? "satisfied" : "unmet";
|
|
2585
2618
|
} catch {
|
|
2586
|
-
return
|
|
2619
|
+
return "unknown";
|
|
2587
2620
|
}
|
|
2588
2621
|
}
|
|
2622
|
+
function evaluateWhen(when, vaultRoot2) {
|
|
2623
|
+
const verdict = evaluateWhenVerdict(when, vaultRoot2);
|
|
2624
|
+
if (verdict === "unknown") return null;
|
|
2625
|
+
return verdict !== "unmet";
|
|
2626
|
+
}
|
|
2589
2627
|
function newestUpdated(vaultRoot2, globs) {
|
|
2590
2628
|
const regexes = globs.map(globToRegExp);
|
|
2591
2629
|
let newest = null;
|
|
@@ -2612,26 +2650,27 @@ function readUpdated(path) {
|
|
|
2612
2650
|
return null;
|
|
2613
2651
|
}
|
|
2614
2652
|
function renderPretool(matches, format) {
|
|
2615
|
-
const
|
|
2616
|
-
const
|
|
2617
|
-
const
|
|
2653
|
+
const byClass = (enforce) => uniqueTexts(matches.filter((match) => match.enforce === enforce).map((match) => match.text));
|
|
2654
|
+
const reasons = byClass("block");
|
|
2655
|
+
const context = byClass("inject");
|
|
2656
|
+
const warnings = byClass("warn");
|
|
2618
2657
|
if (format === "claude") {
|
|
2619
2658
|
const hookSpecificOutput = { hookEventName: "PreToolUse" };
|
|
2620
|
-
if (
|
|
2659
|
+
if (reasons.length > 0) {
|
|
2621
2660
|
hookSpecificOutput.permissionDecision = "deny";
|
|
2622
|
-
hookSpecificOutput.permissionDecisionReason =
|
|
2661
|
+
hookSpecificOutput.permissionDecisionReason = reasons.join("\n");
|
|
2623
2662
|
}
|
|
2624
2663
|
if (context.length > 0) {
|
|
2625
2664
|
hookSpecificOutput.additionalContext = context.join("\n");
|
|
2626
2665
|
}
|
|
2627
|
-
const decided =
|
|
2666
|
+
const decided = reasons.length > 0 || context.length > 0;
|
|
2628
2667
|
return { stdout: decided ? JSON.stringify({ hookSpecificOutput }) : null, stderr: warnings };
|
|
2629
2668
|
}
|
|
2630
2669
|
if (matches.length === 0) return { stdout: null, stderr: [] };
|
|
2631
2670
|
return {
|
|
2632
2671
|
stdout: JSON.stringify({
|
|
2633
|
-
decision:
|
|
2634
|
-
...
|
|
2672
|
+
decision: reasons.length > 0 ? "deny" : "none",
|
|
2673
|
+
...reasons.length > 0 && { reason: reasons.join("\n") },
|
|
2635
2674
|
context,
|
|
2636
2675
|
warnings
|
|
2637
2676
|
}),
|
|
@@ -2651,28 +2690,46 @@ function patchPaths(toolInput) {
|
|
|
2651
2690
|
}
|
|
2652
2691
|
return paths;
|
|
2653
2692
|
}
|
|
2693
|
+
function commandPaths(toolInput) {
|
|
2694
|
+
if (typeof toolInput !== "object" || toolInput === null) return [];
|
|
2695
|
+
const command2 = toolInput.command;
|
|
2696
|
+
if (typeof command2 !== "string") return [];
|
|
2697
|
+
const paths = [];
|
|
2698
|
+
for (const match of command2.matchAll(COMMAND_TOKEN_RE)) {
|
|
2699
|
+
const token = match[1] ?? match[2] ?? match[3];
|
|
2700
|
+
for (const piece of token.split(/[;|&<>()]+/)) {
|
|
2701
|
+
if (piece === "") continue;
|
|
2702
|
+
if (piece === "." || piece === ".." || PATHISH_RE.test(piece)) paths.push(piece);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return paths;
|
|
2706
|
+
}
|
|
2654
2707
|
function vaultPathTouched(toolInput, vaultRoot2, cwd) {
|
|
2655
2708
|
const root = resolve3(vaultRoot2);
|
|
2656
|
-
const candidates = [
|
|
2709
|
+
const candidates = [
|
|
2710
|
+
...pathCandidates(toolInput, cwd),
|
|
2711
|
+
...patchPaths(toolInput),
|
|
2712
|
+
...commandPaths(toolInput)
|
|
2713
|
+
];
|
|
2657
2714
|
return candidates.some((candidate) => {
|
|
2658
2715
|
const absolute = resolve3(cwd ?? ".", candidate);
|
|
2659
2716
|
return absolute === root || absolute.startsWith(`${root}/`);
|
|
2660
2717
|
});
|
|
2661
2718
|
}
|
|
2662
|
-
function renderPosttool(findings, synced, format) {
|
|
2719
|
+
function renderPosttool(findings, synced, format, messages = {}) {
|
|
2663
2720
|
if (findings.length === 0 && synced) return null;
|
|
2664
2721
|
const lines = findings.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
2665
2722
|
if (format === "claude") {
|
|
2666
2723
|
if (hasErrors(findings)) {
|
|
2667
2724
|
return JSON.stringify({
|
|
2668
2725
|
decision: "block",
|
|
2669
|
-
reason:
|
|
2726
|
+
reason: `${messages.reason ?? RESYNC_REASON}:
|
|
2670
2727
|
${lines.join("\n")}`
|
|
2671
2728
|
});
|
|
2672
2729
|
}
|
|
2673
2730
|
const hookSpecificOutput = { hookEventName: "PostToolUse" };
|
|
2674
2731
|
const notes = [...lines];
|
|
2675
|
-
if (!synced) notes.push(
|
|
2732
|
+
if (!synced) notes.push(messages.text ?? RESYNC_TEXT);
|
|
2676
2733
|
hookSpecificOutput.additionalContext = notes.join("\n");
|
|
2677
2734
|
return JSON.stringify({ hookSpecificOutput });
|
|
2678
2735
|
}
|
|
@@ -2689,60 +2746,151 @@ function parseStopEvent(raw) {
|
|
|
2689
2746
|
...typeof stop_hook_active === "boolean" && { stop_hook_active }
|
|
2690
2747
|
};
|
|
2691
2748
|
}
|
|
2692
|
-
function renderStop(findings) {
|
|
2749
|
+
function renderStop(findings, reason) {
|
|
2693
2750
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
2694
2751
|
if (errors.length === 0) return null;
|
|
2695
2752
|
const lines = errors.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
2696
2753
|
return JSON.stringify({
|
|
2697
2754
|
decision: "block",
|
|
2698
|
-
reason:
|
|
2755
|
+
reason: `${reason ?? HANDOFF_GATE_REASON}:
|
|
2699
2756
|
${lines.join("\n")}`
|
|
2700
2757
|
});
|
|
2701
2758
|
}
|
|
2702
|
-
function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
2759
|
+
function dedupePretoolMatches(stateDir, sessionId, matches, persist = true) {
|
|
2703
2760
|
const blocks = matches.filter((match) => match.enforce === "block");
|
|
2704
2761
|
const rest = matches.filter((match) => match.enforce !== "block");
|
|
2705
|
-
const fresh = dedupeMatches(stateDir, sessionId, rest);
|
|
2762
|
+
const fresh = dedupeMatches(stateDir, sessionId, rest, Date.now(), persist);
|
|
2706
2763
|
return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
|
|
2707
2764
|
}
|
|
2708
2765
|
function hookStateDir() {
|
|
2709
2766
|
return join10(kmdHome(), "state", "hook");
|
|
2710
2767
|
}
|
|
2711
|
-
function
|
|
2768
|
+
function explainPrompt(options) {
|
|
2769
|
+
const now = options.now ?? Date.now();
|
|
2770
|
+
const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
|
|
2771
|
+
const entries = [];
|
|
2772
|
+
const rendered = [];
|
|
2773
|
+
const state = { db: null };
|
|
2774
|
+
const getDb = () => state.db ??= openPromptIndex(options.prompt);
|
|
2775
|
+
try {
|
|
2776
|
+
for (const trigger of options.triggers) {
|
|
2777
|
+
if (!isInjectTrigger(trigger)) {
|
|
2778
|
+
entries.push({ id: trigger.id, considered: false });
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
const probe = promptProbe(trigger, options.prompt, getDb);
|
|
2782
|
+
const matched = promptHit(probe);
|
|
2783
|
+
const entry = {
|
|
2784
|
+
id: trigger.id,
|
|
2785
|
+
considered: true,
|
|
2786
|
+
keywords: probe.keywords,
|
|
2787
|
+
intent: probe.intent,
|
|
2788
|
+
matched
|
|
2789
|
+
};
|
|
2790
|
+
entries.push(entry);
|
|
2791
|
+
if (!matched) {
|
|
2792
|
+
entry.fired = false;
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
const key = dedupKey(trigger, now);
|
|
2796
|
+
entry.dedup = key === null ? "never" : fired.has(key) ? "suppressed" : "fresh";
|
|
2797
|
+
entry.fired = entry.dedup !== "suppressed";
|
|
2798
|
+
if (entry.fired) rendered.push({ id: trigger.id, text: trigger.text });
|
|
2799
|
+
}
|
|
2800
|
+
} finally {
|
|
2801
|
+
state.db?.close();
|
|
2802
|
+
}
|
|
2803
|
+
return { triggers: entries, output: renderPrompt(rendered) };
|
|
2804
|
+
}
|
|
2805
|
+
function explainPretool(options) {
|
|
2806
|
+
const now = options.now ?? Date.now();
|
|
2807
|
+
const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
|
|
2808
|
+
const entries = [];
|
|
2809
|
+
const rendered = [];
|
|
2810
|
+
for (const trigger of options.triggers) {
|
|
2811
|
+
if (trigger.on !== "pretool") {
|
|
2812
|
+
entries.push({ id: trigger.id, considered: false });
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
const stage = pretoolStage(trigger, options.toolName, options.toolInput, options.cwd);
|
|
2816
|
+
const entry = {
|
|
2817
|
+
id: trigger.id,
|
|
2818
|
+
enforce: trigger.enforce,
|
|
2819
|
+
considered: true,
|
|
2820
|
+
matcher: stage === "hit" ? "hit" : `${stage}-miss`
|
|
2821
|
+
};
|
|
2822
|
+
entries.push(entry);
|
|
2823
|
+
if (stage !== "hit") {
|
|
2824
|
+
entry.fired = false;
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
if (trigger.when !== void 0) {
|
|
2828
|
+
entry.when = evaluateWhenVerdict(trigger.when, options.vaultRoot);
|
|
2829
|
+
if (entry.when !== "unmet") {
|
|
2830
|
+
entry.fired = false;
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
const match = pretoolMatch(trigger);
|
|
2835
|
+
if (match.enforce === "block") {
|
|
2836
|
+
entry.dedup = "exempt";
|
|
2837
|
+
} else {
|
|
2838
|
+
const key = dedupKey(match, now);
|
|
2839
|
+
entry.dedup = key === null ? "never" : fired.has(key) ? "suppressed" : "fresh";
|
|
2840
|
+
}
|
|
2841
|
+
entry.fired = entry.dedup !== "suppressed";
|
|
2842
|
+
if (entry.fired) rendered.push(match);
|
|
2843
|
+
}
|
|
2844
|
+
return { triggers: entries, outcome: renderPretool(rendered, options.format ?? "neutral") };
|
|
2845
|
+
}
|
|
2846
|
+
function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist = true) {
|
|
2712
2847
|
if (matches.length === 0) return [];
|
|
2713
|
-
const
|
|
2714
|
-
const fired = readFired(
|
|
2848
|
+
const dir = join10(stateDir, safeName(sessionId));
|
|
2849
|
+
const fired = readFired(dir);
|
|
2715
2850
|
const fresh = [];
|
|
2716
2851
|
const record = [];
|
|
2717
2852
|
for (const match of matches) {
|
|
2718
|
-
|
|
2853
|
+
const key = dedupKey(match, now);
|
|
2854
|
+
if (key === null) {
|
|
2719
2855
|
fresh.push(match);
|
|
2720
2856
|
continue;
|
|
2721
2857
|
}
|
|
2722
|
-
const key = typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id;
|
|
2723
2858
|
if (fired.has(key)) continue;
|
|
2724
2859
|
fresh.push(match);
|
|
2725
2860
|
record.push(key);
|
|
2726
2861
|
}
|
|
2727
|
-
if (record.length > 0) {
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2862
|
+
if (persist && record.length > 0) {
|
|
2863
|
+
try {
|
|
2864
|
+
mkdirSync4(dir, { recursive: true });
|
|
2865
|
+
for (const key of record) {
|
|
2866
|
+
try {
|
|
2867
|
+
writeFileSync(join10(dir, key), "", { flag: "wx" });
|
|
2868
|
+
} catch (err) {
|
|
2869
|
+
if (err.code !== "EEXIST") throw err;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
pruneStale(stateDir, dir);
|
|
2873
|
+
} catch (err) {
|
|
2874
|
+
diag2(`dedup state not persisted: ${err instanceof Error ? err.message : String(err)}`);
|
|
2731
2875
|
}
|
|
2732
|
-
writeFileSync(file, JSON.stringify([...fired]));
|
|
2733
|
-
pruneStale(stateDir, file);
|
|
2734
2876
|
}
|
|
2735
2877
|
return fresh;
|
|
2736
2878
|
}
|
|
2737
|
-
function
|
|
2879
|
+
function safeName(part) {
|
|
2880
|
+
return part.replace(/[^A-Za-z0-9._@-]/g, "_");
|
|
2881
|
+
}
|
|
2882
|
+
function dedupKey(match, now) {
|
|
2883
|
+
if (match.dedup === "never") return null;
|
|
2884
|
+
return safeName(
|
|
2885
|
+
typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
function readFired(dir) {
|
|
2738
2889
|
try {
|
|
2739
|
-
|
|
2740
|
-
if (Array.isArray(data)) {
|
|
2741
|
-
return new Set(data.filter((entry) => typeof entry === "string"));
|
|
2742
|
-
}
|
|
2890
|
+
return new Set(readdirSync2(dir));
|
|
2743
2891
|
} catch {
|
|
2892
|
+
return /* @__PURE__ */ new Set();
|
|
2744
2893
|
}
|
|
2745
|
-
return /* @__PURE__ */ new Set();
|
|
2746
2894
|
}
|
|
2747
2895
|
function pruneStale(stateDir, keep) {
|
|
2748
2896
|
try {
|
|
@@ -2750,7 +2898,7 @@ function pruneStale(stateDir, keep) {
|
|
|
2750
2898
|
for (const entry of readdirSync2(stateDir)) {
|
|
2751
2899
|
const path = join10(stateDir, entry);
|
|
2752
2900
|
if (path !== keep && statSync(path).mtimeMs < cutoff) {
|
|
2753
|
-
rmSync2(path, { force: true });
|
|
2901
|
+
rmSync2(path, { recursive: true, force: true });
|
|
2754
2902
|
}
|
|
2755
2903
|
}
|
|
2756
2904
|
} catch {
|
|
@@ -2764,7 +2912,9 @@ function hookInvocation() {
|
|
|
2764
2912
|
options: {
|
|
2765
2913
|
scope: { type: "string" },
|
|
2766
2914
|
harness: { type: "string" },
|
|
2767
|
-
triggers: { type: "string" }
|
|
2915
|
+
triggers: { type: "string" },
|
|
2916
|
+
"dry-run": { type: "boolean" },
|
|
2917
|
+
explain: { type: "boolean" }
|
|
2768
2918
|
}
|
|
2769
2919
|
});
|
|
2770
2920
|
const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
|
|
@@ -2776,7 +2926,9 @@ function hookInvocation() {
|
|
|
2776
2926
|
vaultRoot: vaultRoot2,
|
|
2777
2927
|
scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
|
|
2778
2928
|
harness: values2.harness,
|
|
2779
|
-
triggersFile: values2.triggers
|
|
2929
|
+
triggersFile: values2.triggers,
|
|
2930
|
+
dryRun: values2["dry-run"] === true || values2.explain === true,
|
|
2931
|
+
explain: values2.explain === true
|
|
2780
2932
|
};
|
|
2781
2933
|
}
|
|
2782
2934
|
function resolveFileTriggers(invocation) {
|
|
@@ -2814,9 +2966,26 @@ async function runHookPrompt() {
|
|
|
2814
2966
|
for (const id of duplicates) {
|
|
2815
2967
|
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2816
2968
|
}
|
|
2969
|
+
if (invocation.explain) {
|
|
2970
|
+
const trace = explainPrompt({
|
|
2971
|
+
prompt: event.prompt,
|
|
2972
|
+
triggers,
|
|
2973
|
+
stateDir: hookStateDir(),
|
|
2974
|
+
sessionId: event.session_id
|
|
2975
|
+
});
|
|
2976
|
+
console.log(JSON.stringify({ event: "prompt", scope: scope ?? null, duplicates, ...trace }));
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2817
2979
|
const matches = matchPromptTriggers(event.prompt, triggers);
|
|
2818
|
-
|
|
2819
|
-
|
|
2980
|
+
const fresh = dedupeMatches(
|
|
2981
|
+
hookStateDir(),
|
|
2982
|
+
event.session_id,
|
|
2983
|
+
matches,
|
|
2984
|
+
Date.now(),
|
|
2985
|
+
!invocation.dryRun
|
|
2986
|
+
);
|
|
2987
|
+
for (const line of renderPrompt(fresh)) {
|
|
2988
|
+
console.log(line);
|
|
2820
2989
|
}
|
|
2821
2990
|
} catch (err) {
|
|
2822
2991
|
diag2(err instanceof Error ? err.message : String(err));
|
|
@@ -2848,13 +3017,27 @@ async function runHookPretool() {
|
|
|
2848
3017
|
for (const id of duplicates) {
|
|
2849
3018
|
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2850
3019
|
}
|
|
3020
|
+
if (invocation.explain) {
|
|
3021
|
+
const trace = explainPretool({
|
|
3022
|
+
toolName: event.tool_name,
|
|
3023
|
+
toolInput: event.tool_input,
|
|
3024
|
+
triggers,
|
|
3025
|
+
vaultRoot: vaultRoot2,
|
|
3026
|
+
stateDir: hookStateDir(),
|
|
3027
|
+
sessionId: event.session_id,
|
|
3028
|
+
...event.cwd !== void 0 && { cwd: event.cwd },
|
|
3029
|
+
format
|
|
3030
|
+
});
|
|
3031
|
+
console.log(JSON.stringify({ event: "pretool", scope: scope ?? null, duplicates, ...trace }));
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
2851
3034
|
const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
|
|
2852
3035
|
const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
|
|
2853
3036
|
for (const id of skipped) {
|
|
2854
3037
|
diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
|
|
2855
3038
|
}
|
|
2856
3039
|
const rendered = renderPretool(
|
|
2857
|
-
dedupePretoolMatches(hookStateDir(), event.session_id, fired),
|
|
3040
|
+
dedupePretoolMatches(hookStateDir(), event.session_id, fired, !invocation.dryRun),
|
|
2858
3041
|
format
|
|
2859
3042
|
);
|
|
2860
3043
|
for (const line of rendered.stderr) {
|
|
@@ -2871,6 +3054,10 @@ async function runHookPosttool() {
|
|
|
2871
3054
|
try {
|
|
2872
3055
|
const invocation = hookInvocation();
|
|
2873
3056
|
if (invocation === null) return;
|
|
3057
|
+
if (invocation.dryRun) {
|
|
3058
|
+
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
2874
3061
|
let format = "neutral";
|
|
2875
3062
|
if (invocation.harness === "claude") {
|
|
2876
3063
|
format = "claude";
|
|
@@ -2883,6 +3070,7 @@ async function runHookPosttool() {
|
|
|
2883
3070
|
return;
|
|
2884
3071
|
}
|
|
2885
3072
|
if (!vaultPathTouched(event.tool_input, invocation.vaultRoot, event.cwd)) return;
|
|
3073
|
+
const config = await loadVaultConfig(invocation.vaultRoot);
|
|
2886
3074
|
const findings = await validateVault(invocation.vaultRoot);
|
|
2887
3075
|
let synced = false;
|
|
2888
3076
|
if (!hasErrors(findings)) {
|
|
@@ -2893,7 +3081,7 @@ async function runHookPosttool() {
|
|
|
2893
3081
|
diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2894
3082
|
}
|
|
2895
3083
|
}
|
|
2896
|
-
const rendered = renderPosttool(findings, synced, format);
|
|
3084
|
+
const rendered = renderPosttool(findings, synced, format, config.builtin_hooks?.resync ?? {});
|
|
2897
3085
|
if (rendered !== null) {
|
|
2898
3086
|
console.log(rendered);
|
|
2899
3087
|
}
|
|
@@ -2905,6 +3093,10 @@ async function runHookStop() {
|
|
|
2905
3093
|
try {
|
|
2906
3094
|
const invocation = hookInvocation();
|
|
2907
3095
|
if (invocation === null) return;
|
|
3096
|
+
if (invocation.dryRun) {
|
|
3097
|
+
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
2908
3100
|
const event = parseStopEvent(await readStdin());
|
|
2909
3101
|
if (event === null) {
|
|
2910
3102
|
diag2("stdin is not a stop event ({session_id})");
|
|
@@ -2914,9 +3106,12 @@ async function runHookStop() {
|
|
|
2914
3106
|
const config = await loadVaultConfig(invocation.vaultRoot);
|
|
2915
3107
|
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2916
3108
|
if (scope === void 0) return;
|
|
2917
|
-
const rendered = renderStop(
|
|
3109
|
+
const rendered = renderStop(
|
|
3110
|
+
await validateVault(invocation.vaultRoot),
|
|
3111
|
+
config.builtin_hooks?.["handoff-gate"]?.reason
|
|
3112
|
+
);
|
|
2918
3113
|
if (rendered === null) return;
|
|
2919
|
-
const fired = dedupeMatches(hookStateDir(), event.session_id, [{ id: "
|
|
3114
|
+
const fired = dedupeMatches(hookStateDir(), event.session_id, [{ id: "handoff-gate" }]);
|
|
2920
3115
|
if (fired.length === 0) return;
|
|
2921
3116
|
console.log(rendered);
|
|
2922
3117
|
} catch (err) {
|
|
@@ -2934,7 +3129,7 @@ async function readStdin() {
|
|
|
2934
3129
|
function diag2(message) {
|
|
2935
3130
|
console.error(`kmd hook: ${message}`);
|
|
2936
3131
|
}
|
|
2937
|
-
var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE;
|
|
3132
|
+
var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE, COMMAND_TOKEN_RE, PATHISH_RE, RESYNC_REASON, RESYNC_TEXT, HANDOFF_GATE_REASON;
|
|
2938
3133
|
var init_hook = __esm({
|
|
2939
3134
|
"../cli/src/hook.ts"() {
|
|
2940
3135
|
"use strict";
|
|
@@ -2948,6 +3143,11 @@ var init_hook = __esm({
|
|
|
2948
3143
|
KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
|
|
2949
3144
|
ALL_SCOPES_KEY = "_all";
|
|
2950
3145
|
PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
3146
|
+
COMMAND_TOKEN_RE = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
3147
|
+
PATHISH_RE = /[/*$]|\.(?:md|base|canvas|ya?ml)$/i;
|
|
3148
|
+
RESYNC_REASON = "Edit landed; the index sync is held until these validate errors are fixed";
|
|
3149
|
+
RESYNC_TEXT = "kmd sync failed \u2014 index not updated; see hook stderr";
|
|
3150
|
+
HANDOFF_GATE_REASON = "Validate errors are outstanding and the index sync is held \u2014 fix them, let the resync run, then finish";
|
|
2951
3151
|
}
|
|
2952
3152
|
});
|
|
2953
3153
|
|