@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 CHANGED
@@ -809,15 +809,11 @@ async function syncVault(vaultRoot2) {
809
809
  }
810
810
  indexedPaths.push(path);
811
811
  }
812
- let pagesDeleted = 0;
813
- let linksDeleted = 0;
814
- if (indexedPaths.length > 0) {
815
- const placeholders = indexedPaths.map(() => "?").join(", ");
816
- const pageResult = db.prepare(`DELETE FROM pages WHERE path NOT IN (${placeholders})`).run(...indexedPaths);
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; skipping orphan deletion (safety)");
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
- let db = null;
2488
+ const state = { db: null };
2489
+ const getDb = () => state.db ??= openPromptIndex(prompt);
2474
2490
  try {
2475
2491
  for (const trigger of candidates) {
2476
- let hit = false;
2477
- if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
2478
- db ??= openPromptIndex(prompt);
2479
- const row = db.prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
2480
- hit = row.n > 0;
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.tool !== void 0 && trigger.tool !== toolName) continue;
2551
- if (trigger.args_match !== void 0) {
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 evaluateWhen(when, vaultRoot2) {
2590
- if (typeof when === "string") return null;
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 true;
2614
+ if (than === null) return "vacuous";
2594
2615
  const fresh = newestUpdated(vaultRoot2, when.fresh);
2595
- if (fresh === null) return false;
2596
- return fresh >= than;
2616
+ if (fresh === null) return "unmet";
2617
+ return fresh >= than ? "satisfied" : "unmet";
2597
2618
  } catch {
2598
- return null;
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 block = matches.find((match) => match.enforce === "block");
2628
- const context = matches.filter((match) => match.enforce === "inject").map((match) => match.text);
2629
- const warnings = matches.filter((match) => match.enforce === "warn").map((match) => match.text);
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 (block !== void 0) {
2659
+ if (reasons.length > 0) {
2633
2660
  hookSpecificOutput.permissionDecision = "deny";
2634
- hookSpecificOutput.permissionDecisionReason = block.text;
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 = block !== void 0 || context.length > 0;
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: block !== void 0 ? "deny" : "none",
2646
- ...block !== void 0 && { reason: block.text },
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 = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
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 dedupeMatches(stateDir, sessionId, matches, now = Date.now()) {
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 file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
2726
- const fired = readFired(file);
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
- if (match.dedup === "never") {
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
- mkdirSync4(stateDir, { recursive: true });
2741
- for (const key of record) {
2742
- fired.add(key);
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 readFired(file) {
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
- const data = JSON.parse(readFileSync(file, "utf8"));
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
- for (const match of dedupeMatches(hookStateDir(), event.session_id, matches)) {
2831
- console.log(match.text);
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";