@bartolli/kmd 0.9.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 +275 -94
- package/dist/kmd.mjs.map +3 -3
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -809,15 +809,11 @@ async function syncVault(vaultRoot2) {
|
|
|
809
809
|
}
|
|
810
810
|
indexedPaths.push(path);
|
|
811
811
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
pagesDeleted = Number(pageResult.changes);
|
|
818
|
-
const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
|
|
819
|
-
linksDeleted = Number(linkResult.changes);
|
|
820
|
-
}
|
|
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);
|
|
821
817
|
db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
|
|
822
818
|
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
|
|
823
819
|
setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -841,7 +837,7 @@ async function runSync() {
|
|
|
841
837
|
console.log(`sync: ${env.WIKI_VAULT} \u2192 ${resolveIndexPath(env.WIKI_VAULT)}`);
|
|
842
838
|
const stats = await syncVault(env.WIKI_VAULT);
|
|
843
839
|
if (stats.noPages) {
|
|
844
|
-
console.warn("no indexable pages found;
|
|
840
|
+
console.warn("no indexable pages found; index swept empty");
|
|
845
841
|
}
|
|
846
842
|
console.log(
|
|
847
843
|
`done: ${stats.changed} changed, ${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.pagesDeleted} pages deleted, ${stats.linksDeleted} link orphans cleared`
|
|
@@ -2361,6 +2357,8 @@ __export(hook_exports, {
|
|
|
2361
2357
|
dedupePretoolMatches: () => dedupePretoolMatches,
|
|
2362
2358
|
effectiveTriggers: () => effectiveTriggers,
|
|
2363
2359
|
evaluateMatches: () => evaluateMatches,
|
|
2360
|
+
explainPretool: () => explainPretool,
|
|
2361
|
+
explainPrompt: () => explainPrompt,
|
|
2364
2362
|
hookStateDir: () => hookStateDir,
|
|
2365
2363
|
kiroIdePromptEvent: () => kiroIdePromptEvent,
|
|
2366
2364
|
loadTriggerFile: () => loadTriggerFile,
|
|
@@ -2371,6 +2369,7 @@ __export(hook_exports, {
|
|
|
2371
2369
|
parseStopEvent: () => parseStopEvent,
|
|
2372
2370
|
renderPosttool: () => renderPosttool,
|
|
2373
2371
|
renderPretool: () => renderPretool,
|
|
2372
|
+
renderPrompt: () => renderPrompt,
|
|
2374
2373
|
renderStop: () => renderStop,
|
|
2375
2374
|
resolveScope: () => resolveScope,
|
|
2376
2375
|
runHookPosttool: () => runHookPosttool,
|
|
@@ -2464,37 +2463,50 @@ function openPromptIndex(prompt) {
|
|
|
2464
2463
|
db.prepare("INSERT INTO prompt_doc (text) VALUES (?)").run(prompt);
|
|
2465
2464
|
return db;
|
|
2466
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
|
+
}
|
|
2467
2484
|
function matchPromptTriggers(prompt, triggers) {
|
|
2468
|
-
const candidates = triggers.filter(
|
|
2469
|
-
(trigger) => trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0
|
|
2470
|
-
);
|
|
2485
|
+
const candidates = triggers.filter(isInjectTrigger);
|
|
2471
2486
|
if (candidates.length === 0) return [];
|
|
2472
2487
|
const matches = [];
|
|
2473
|
-
|
|
2488
|
+
const state = { db: null };
|
|
2489
|
+
const getDb = () => state.db ??= openPromptIndex(prompt);
|
|
2474
2490
|
try {
|
|
2475
2491
|
for (const trigger of candidates) {
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
}
|
|
2482
|
-
if (!hit && trigger.intent !== void 0) {
|
|
2483
|
-
hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
|
|
2484
|
-
}
|
|
2485
|
-
if (hit) {
|
|
2486
|
-
matches.push({
|
|
2487
|
-
id: trigger.id,
|
|
2488
|
-
text: trigger.text,
|
|
2489
|
-
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2490
|
-
});
|
|
2491
|
-
}
|
|
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
|
+
});
|
|
2492
2498
|
}
|
|
2493
2499
|
} finally {
|
|
2494
|
-
db?.close();
|
|
2500
|
+
state.db?.close();
|
|
2495
2501
|
}
|
|
2496
2502
|
return matches;
|
|
2497
2503
|
}
|
|
2504
|
+
function uniqueTexts(texts) {
|
|
2505
|
+
return [...new Set(texts)];
|
|
2506
|
+
}
|
|
2507
|
+
function renderPrompt(matches) {
|
|
2508
|
+
return uniqueTexts(matches.map((match) => match.text));
|
|
2509
|
+
}
|
|
2498
2510
|
function parsePretoolEvent(raw) {
|
|
2499
2511
|
const fields = eventFields(raw);
|
|
2500
2512
|
if (fields === null) return null;
|
|
@@ -2543,32 +2555,41 @@ function pathCandidates(toolInput, cwd) {
|
|
|
2543
2555
|
}
|
|
2544
2556
|
return candidates;
|
|
2545
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
|
+
}
|
|
2546
2587
|
function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
2547
2588
|
const matches = [];
|
|
2548
2589
|
for (const trigger of triggers) {
|
|
2549
2590
|
if (trigger.on !== "pretool") continue;
|
|
2550
|
-
if (trigger
|
|
2551
|
-
|
|
2552
|
-
const serialized = JSON.stringify(toolInput ?? {});
|
|
2553
|
-
if (!new RegExp(trigger.args_match).test(serialized)) continue;
|
|
2554
|
-
}
|
|
2555
|
-
if (trigger.files !== void 0 && trigger.files.length > 0) {
|
|
2556
|
-
const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
|
|
2557
|
-
const hit = trigger.files.some((glob) => {
|
|
2558
|
-
const regex = globToRegExp(glob);
|
|
2559
|
-
return candidates.some((candidate) => regex.test(candidate));
|
|
2560
|
-
});
|
|
2561
|
-
if (!hit) continue;
|
|
2562
|
-
}
|
|
2563
|
-
const text = trigger.enforce === "block" ? trigger.reason : trigger.text;
|
|
2564
|
-
if (text === void 0) continue;
|
|
2565
|
-
matches.push({
|
|
2566
|
-
id: trigger.id,
|
|
2567
|
-
enforce: trigger.enforce,
|
|
2568
|
-
text,
|
|
2569
|
-
...trigger.when !== void 0 && { when: trigger.when },
|
|
2570
|
-
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2571
|
-
});
|
|
2591
|
+
if (pretoolStage(trigger, toolName, toolInput, cwd) !== "hit") continue;
|
|
2592
|
+
matches.push(pretoolMatch(trigger));
|
|
2572
2593
|
}
|
|
2573
2594
|
return matches;
|
|
2574
2595
|
}
|
|
@@ -2586,18 +2607,23 @@ function evaluateMatches(matches, vaultRoot2) {
|
|
|
2586
2607
|
}
|
|
2587
2608
|
return { fired, skipped };
|
|
2588
2609
|
}
|
|
2589
|
-
function
|
|
2590
|
-
if (typeof when === "string") return
|
|
2610
|
+
function evaluateWhenVerdict(when, vaultRoot2) {
|
|
2611
|
+
if (typeof when === "string") return "unknown";
|
|
2591
2612
|
try {
|
|
2592
2613
|
const than = newestUpdated(vaultRoot2, when.than);
|
|
2593
|
-
if (than === null) return
|
|
2614
|
+
if (than === null) return "vacuous";
|
|
2594
2615
|
const fresh = newestUpdated(vaultRoot2, when.fresh);
|
|
2595
|
-
if (fresh === null) return
|
|
2596
|
-
return fresh >= than;
|
|
2616
|
+
if (fresh === null) return "unmet";
|
|
2617
|
+
return fresh >= than ? "satisfied" : "unmet";
|
|
2597
2618
|
} catch {
|
|
2598
|
-
return
|
|
2619
|
+
return "unknown";
|
|
2599
2620
|
}
|
|
2600
2621
|
}
|
|
2622
|
+
function evaluateWhen(when, vaultRoot2) {
|
|
2623
|
+
const verdict = evaluateWhenVerdict(when, vaultRoot2);
|
|
2624
|
+
if (verdict === "unknown") return null;
|
|
2625
|
+
return verdict !== "unmet";
|
|
2626
|
+
}
|
|
2601
2627
|
function newestUpdated(vaultRoot2, globs) {
|
|
2602
2628
|
const regexes = globs.map(globToRegExp);
|
|
2603
2629
|
let newest = null;
|
|
@@ -2624,26 +2650,27 @@ function readUpdated(path) {
|
|
|
2624
2650
|
return null;
|
|
2625
2651
|
}
|
|
2626
2652
|
function renderPretool(matches, format) {
|
|
2627
|
-
const
|
|
2628
|
-
const
|
|
2629
|
-
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");
|
|
2630
2657
|
if (format === "claude") {
|
|
2631
2658
|
const hookSpecificOutput = { hookEventName: "PreToolUse" };
|
|
2632
|
-
if (
|
|
2659
|
+
if (reasons.length > 0) {
|
|
2633
2660
|
hookSpecificOutput.permissionDecision = "deny";
|
|
2634
|
-
hookSpecificOutput.permissionDecisionReason =
|
|
2661
|
+
hookSpecificOutput.permissionDecisionReason = reasons.join("\n");
|
|
2635
2662
|
}
|
|
2636
2663
|
if (context.length > 0) {
|
|
2637
2664
|
hookSpecificOutput.additionalContext = context.join("\n");
|
|
2638
2665
|
}
|
|
2639
|
-
const decided =
|
|
2666
|
+
const decided = reasons.length > 0 || context.length > 0;
|
|
2640
2667
|
return { stdout: decided ? JSON.stringify({ hookSpecificOutput }) : null, stderr: warnings };
|
|
2641
2668
|
}
|
|
2642
2669
|
if (matches.length === 0) return { stdout: null, stderr: [] };
|
|
2643
2670
|
return {
|
|
2644
2671
|
stdout: JSON.stringify({
|
|
2645
|
-
decision:
|
|
2646
|
-
...
|
|
2672
|
+
decision: reasons.length > 0 ? "deny" : "none",
|
|
2673
|
+
...reasons.length > 0 && { reason: reasons.join("\n") },
|
|
2647
2674
|
context,
|
|
2648
2675
|
warnings
|
|
2649
2676
|
}),
|
|
@@ -2663,9 +2690,27 @@ function patchPaths(toolInput) {
|
|
|
2663
2690
|
}
|
|
2664
2691
|
return paths;
|
|
2665
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
|
+
}
|
|
2666
2707
|
function vaultPathTouched(toolInput, vaultRoot2, cwd) {
|
|
2667
2708
|
const root = resolve3(vaultRoot2);
|
|
2668
|
-
const candidates = [
|
|
2709
|
+
const candidates = [
|
|
2710
|
+
...pathCandidates(toolInput, cwd),
|
|
2711
|
+
...patchPaths(toolInput),
|
|
2712
|
+
...commandPaths(toolInput)
|
|
2713
|
+
];
|
|
2669
2714
|
return candidates.some((candidate) => {
|
|
2670
2715
|
const absolute = resolve3(cwd ?? ".", candidate);
|
|
2671
2716
|
return absolute === root || absolute.startsWith(`${root}/`);
|
|
@@ -2711,50 +2756,141 @@ function renderStop(findings, reason) {
|
|
|
2711
2756
|
${lines.join("\n")}`
|
|
2712
2757
|
});
|
|
2713
2758
|
}
|
|
2714
|
-
function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
2759
|
+
function dedupePretoolMatches(stateDir, sessionId, matches, persist = true) {
|
|
2715
2760
|
const blocks = matches.filter((match) => match.enforce === "block");
|
|
2716
2761
|
const rest = matches.filter((match) => match.enforce !== "block");
|
|
2717
|
-
const fresh = dedupeMatches(stateDir, sessionId, rest);
|
|
2762
|
+
const fresh = dedupeMatches(stateDir, sessionId, rest, Date.now(), persist);
|
|
2718
2763
|
return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
|
|
2719
2764
|
}
|
|
2720
2765
|
function hookStateDir() {
|
|
2721
2766
|
return join10(kmdHome(), "state", "hook");
|
|
2722
2767
|
}
|
|
2723
|
-
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) {
|
|
2724
2847
|
if (matches.length === 0) return [];
|
|
2725
|
-
const
|
|
2726
|
-
const fired = readFired(
|
|
2848
|
+
const dir = join10(stateDir, safeName(sessionId));
|
|
2849
|
+
const fired = readFired(dir);
|
|
2727
2850
|
const fresh = [];
|
|
2728
2851
|
const record = [];
|
|
2729
2852
|
for (const match of matches) {
|
|
2730
|
-
|
|
2853
|
+
const key = dedupKey(match, now);
|
|
2854
|
+
if (key === null) {
|
|
2731
2855
|
fresh.push(match);
|
|
2732
2856
|
continue;
|
|
2733
2857
|
}
|
|
2734
|
-
const key = typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id;
|
|
2735
2858
|
if (fired.has(key)) continue;
|
|
2736
2859
|
fresh.push(match);
|
|
2737
2860
|
record.push(key);
|
|
2738
2861
|
}
|
|
2739
|
-
if (record.length > 0) {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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)}`);
|
|
2743
2875
|
}
|
|
2744
|
-
writeFileSync(file, JSON.stringify([...fired]));
|
|
2745
|
-
pruneStale(stateDir, file);
|
|
2746
2876
|
}
|
|
2747
2877
|
return fresh;
|
|
2748
2878
|
}
|
|
2749
|
-
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) {
|
|
2750
2889
|
try {
|
|
2751
|
-
|
|
2752
|
-
if (Array.isArray(data)) {
|
|
2753
|
-
return new Set(data.filter((entry) => typeof entry === "string"));
|
|
2754
|
-
}
|
|
2890
|
+
return new Set(readdirSync2(dir));
|
|
2755
2891
|
} catch {
|
|
2892
|
+
return /* @__PURE__ */ new Set();
|
|
2756
2893
|
}
|
|
2757
|
-
return /* @__PURE__ */ new Set();
|
|
2758
2894
|
}
|
|
2759
2895
|
function pruneStale(stateDir, keep) {
|
|
2760
2896
|
try {
|
|
@@ -2762,7 +2898,7 @@ function pruneStale(stateDir, keep) {
|
|
|
2762
2898
|
for (const entry of readdirSync2(stateDir)) {
|
|
2763
2899
|
const path = join10(stateDir, entry);
|
|
2764
2900
|
if (path !== keep && statSync(path).mtimeMs < cutoff) {
|
|
2765
|
-
rmSync2(path, { force: true });
|
|
2901
|
+
rmSync2(path, { recursive: true, force: true });
|
|
2766
2902
|
}
|
|
2767
2903
|
}
|
|
2768
2904
|
} catch {
|
|
@@ -2776,7 +2912,9 @@ function hookInvocation() {
|
|
|
2776
2912
|
options: {
|
|
2777
2913
|
scope: { type: "string" },
|
|
2778
2914
|
harness: { type: "string" },
|
|
2779
|
-
triggers: { type: "string" }
|
|
2915
|
+
triggers: { type: "string" },
|
|
2916
|
+
"dry-run": { type: "boolean" },
|
|
2917
|
+
explain: { type: "boolean" }
|
|
2780
2918
|
}
|
|
2781
2919
|
});
|
|
2782
2920
|
const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
|
|
@@ -2788,7 +2926,9 @@ function hookInvocation() {
|
|
|
2788
2926
|
vaultRoot: vaultRoot2,
|
|
2789
2927
|
scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
|
|
2790
2928
|
harness: values2.harness,
|
|
2791
|
-
triggersFile: values2.triggers
|
|
2929
|
+
triggersFile: values2.triggers,
|
|
2930
|
+
dryRun: values2["dry-run"] === true || values2.explain === true,
|
|
2931
|
+
explain: values2.explain === true
|
|
2792
2932
|
};
|
|
2793
2933
|
}
|
|
2794
2934
|
function resolveFileTriggers(invocation) {
|
|
@@ -2826,9 +2966,26 @@ async function runHookPrompt() {
|
|
|
2826
2966
|
for (const id of duplicates) {
|
|
2827
2967
|
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2828
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
|
+
}
|
|
2829
2979
|
const matches = matchPromptTriggers(event.prompt, triggers);
|
|
2830
|
-
|
|
2831
|
-
|
|
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);
|
|
2832
2989
|
}
|
|
2833
2990
|
} catch (err) {
|
|
2834
2991
|
diag2(err instanceof Error ? err.message : String(err));
|
|
@@ -2860,13 +3017,27 @@ async function runHookPretool() {
|
|
|
2860
3017
|
for (const id of duplicates) {
|
|
2861
3018
|
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2862
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
|
+
}
|
|
2863
3034
|
const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
|
|
2864
3035
|
const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
|
|
2865
3036
|
for (const id of skipped) {
|
|
2866
3037
|
diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
|
|
2867
3038
|
}
|
|
2868
3039
|
const rendered = renderPretool(
|
|
2869
|
-
dedupePretoolMatches(hookStateDir(), event.session_id, fired),
|
|
3040
|
+
dedupePretoolMatches(hookStateDir(), event.session_id, fired, !invocation.dryRun),
|
|
2870
3041
|
format
|
|
2871
3042
|
);
|
|
2872
3043
|
for (const line of rendered.stderr) {
|
|
@@ -2883,6 +3054,10 @@ async function runHookPosttool() {
|
|
|
2883
3054
|
try {
|
|
2884
3055
|
const invocation = hookInvocation();
|
|
2885
3056
|
if (invocation === null) return;
|
|
3057
|
+
if (invocation.dryRun) {
|
|
3058
|
+
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
2886
3061
|
let format = "neutral";
|
|
2887
3062
|
if (invocation.harness === "claude") {
|
|
2888
3063
|
format = "claude";
|
|
@@ -2918,6 +3093,10 @@ async function runHookStop() {
|
|
|
2918
3093
|
try {
|
|
2919
3094
|
const invocation = hookInvocation();
|
|
2920
3095
|
if (invocation === null) return;
|
|
3096
|
+
if (invocation.dryRun) {
|
|
3097
|
+
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
2921
3100
|
const event = parseStopEvent(await readStdin());
|
|
2922
3101
|
if (event === null) {
|
|
2923
3102
|
diag2("stdin is not a stop event ({session_id})");
|
|
@@ -2950,7 +3129,7 @@ async function readStdin() {
|
|
|
2950
3129
|
function diag2(message) {
|
|
2951
3130
|
console.error(`kmd hook: ${message}`);
|
|
2952
3131
|
}
|
|
2953
|
-
var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE, RESYNC_REASON, RESYNC_TEXT, HANDOFF_GATE_REASON;
|
|
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;
|
|
2954
3133
|
var init_hook = __esm({
|
|
2955
3134
|
"../cli/src/hook.ts"() {
|
|
2956
3135
|
"use strict";
|
|
@@ -2964,6 +3143,8 @@ var init_hook = __esm({
|
|
|
2964
3143
|
KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
|
|
2965
3144
|
ALL_SCOPES_KEY = "_all";
|
|
2966
3145
|
PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
3146
|
+
COMMAND_TOKEN_RE = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
3147
|
+
PATHISH_RE = /[/*$]|\.(?:md|base|canvas|ya?ml)$/i;
|
|
2967
3148
|
RESYNC_REASON = "Edit landed; the index sync is held until these validate errors are fixed";
|
|
2968
3149
|
RESYNC_TEXT = "kmd sync failed \u2014 index not updated; see hook stderr";
|
|
2969
3150
|
HANDOFF_GATE_REASON = "Validate errors are outstanding and the index sync is held \u2014 fix them, let the resync run, then finish";
|