@bartolli/kmd 0.9.0 → 0.11.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 +328 -97
- package/dist/kmd.mjs.map +3 -3
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -248,6 +248,12 @@ var init_vault_config = __esm({
|
|
|
248
248
|
}).optional(),
|
|
249
249
|
"handoff-gate": z.strictObject({
|
|
250
250
|
reason: z.string().min(1).optional().describe("Stop-block preamble; the engine appends the error lines.")
|
|
251
|
+
}).optional(),
|
|
252
|
+
orient: z.strictObject({
|
|
253
|
+
text: z.string().min(1).optional().describe("Session-start prime instruction; the engine prepends the resolved scope.")
|
|
254
|
+
}).optional(),
|
|
255
|
+
reorient: z.strictObject({
|
|
256
|
+
text: z.string().min(1).optional().describe("Post-compaction re-orientation; the engine prepends the resolved scope.")
|
|
251
257
|
}).optional()
|
|
252
258
|
});
|
|
253
259
|
VaultConfigSchema = z.strictObject({
|
|
@@ -269,7 +275,7 @@ var init_vault_config = __esm({
|
|
|
269
275
|
'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
|
|
270
276
|
),
|
|
271
277
|
builtin_hooks: BuiltinHooksSchema.optional().describe(
|
|
272
|
-
"Message overrides for the fixed-function hooks (resync, handoff-gate) by public id."
|
|
278
|
+
"Message overrides for the fixed-function hooks (resync, handoff-gate, orient, reorient) by public id."
|
|
273
279
|
),
|
|
274
280
|
triggers_extra: TriggersSchema.optional().describe(
|
|
275
281
|
'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
|
|
@@ -809,15 +815,11 @@ async function syncVault(vaultRoot2) {
|
|
|
809
815
|
}
|
|
810
816
|
indexedPaths.push(path);
|
|
811
817
|
}
|
|
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
|
-
}
|
|
818
|
+
const placeholders = indexedPaths.map(() => "?").join(", ");
|
|
819
|
+
const pageResult = indexedPaths.length > 0 ? db.prepare(`DELETE FROM pages WHERE path NOT IN (${placeholders})`).run(...indexedPaths) : db.prepare("DELETE FROM pages").run();
|
|
820
|
+
const pagesDeleted = Number(pageResult.changes);
|
|
821
|
+
const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
|
|
822
|
+
const linksDeleted = Number(linkResult.changes);
|
|
821
823
|
db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
|
|
822
824
|
setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
|
|
823
825
|
setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -841,7 +843,7 @@ async function runSync() {
|
|
|
841
843
|
console.log(`sync: ${env.WIKI_VAULT} \u2192 ${resolveIndexPath(env.WIKI_VAULT)}`);
|
|
842
844
|
const stats = await syncVault(env.WIKI_VAULT);
|
|
843
845
|
if (stats.noPages) {
|
|
844
|
-
console.warn("no indexable pages found;
|
|
846
|
+
console.warn("no indexable pages found; index swept empty");
|
|
845
847
|
}
|
|
846
848
|
console.log(
|
|
847
849
|
`done: ${stats.changed} changed, ${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.pagesDeleted} pages deleted, ${stats.linksDeleted} link orphans cleared`
|
|
@@ -2361,6 +2363,8 @@ __export(hook_exports, {
|
|
|
2361
2363
|
dedupePretoolMatches: () => dedupePretoolMatches,
|
|
2362
2364
|
effectiveTriggers: () => effectiveTriggers,
|
|
2363
2365
|
evaluateMatches: () => evaluateMatches,
|
|
2366
|
+
explainPretool: () => explainPretool,
|
|
2367
|
+
explainPrompt: () => explainPrompt,
|
|
2364
2368
|
hookStateDir: () => hookStateDir,
|
|
2365
2369
|
kiroIdePromptEvent: () => kiroIdePromptEvent,
|
|
2366
2370
|
loadTriggerFile: () => loadTriggerFile,
|
|
@@ -2368,14 +2372,18 @@ __export(hook_exports, {
|
|
|
2368
2372
|
matchPromptTriggers: () => matchPromptTriggers,
|
|
2369
2373
|
parsePretoolEvent: () => parsePretoolEvent,
|
|
2370
2374
|
parsePromptEvent: () => parsePromptEvent,
|
|
2375
|
+
parseSessionStartEvent: () => parseSessionStartEvent,
|
|
2371
2376
|
parseStopEvent: () => parseStopEvent,
|
|
2372
2377
|
renderPosttool: () => renderPosttool,
|
|
2373
2378
|
renderPretool: () => renderPretool,
|
|
2379
|
+
renderPrompt: () => renderPrompt,
|
|
2380
|
+
renderSessionStart: () => renderSessionStart,
|
|
2374
2381
|
renderStop: () => renderStop,
|
|
2375
2382
|
resolveScope: () => resolveScope,
|
|
2376
2383
|
runHookPosttool: () => runHookPosttool,
|
|
2377
2384
|
runHookPretool: () => runHookPretool,
|
|
2378
2385
|
runHookPrompt: () => runHookPrompt,
|
|
2386
|
+
runHookSessionStart: () => runHookSessionStart,
|
|
2379
2387
|
runHookStop: () => runHookStop,
|
|
2380
2388
|
vaultPathTouched: () => vaultPathTouched
|
|
2381
2389
|
});
|
|
@@ -2464,37 +2472,50 @@ function openPromptIndex(prompt) {
|
|
|
2464
2472
|
db.prepare("INSERT INTO prompt_doc (text) VALUES (?)").run(prompt);
|
|
2465
2473
|
return db;
|
|
2466
2474
|
}
|
|
2475
|
+
function promptProbe(trigger, prompt, getDb) {
|
|
2476
|
+
let keywords = "unset";
|
|
2477
|
+
if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
|
|
2478
|
+
const row = getDb().prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
|
|
2479
|
+
keywords = row.n > 0 ? "hit" : "miss";
|
|
2480
|
+
}
|
|
2481
|
+
let intent = "unset";
|
|
2482
|
+
if (trigger.intent !== void 0) {
|
|
2483
|
+
intent = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt)) ? "hit" : "miss";
|
|
2484
|
+
}
|
|
2485
|
+
return { keywords, intent };
|
|
2486
|
+
}
|
|
2487
|
+
function promptHit(probe) {
|
|
2488
|
+
return probe.keywords === "hit" || probe.intent === "hit";
|
|
2489
|
+
}
|
|
2490
|
+
function isInjectTrigger(trigger) {
|
|
2491
|
+
return trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0;
|
|
2492
|
+
}
|
|
2467
2493
|
function matchPromptTriggers(prompt, triggers) {
|
|
2468
|
-
const candidates = triggers.filter(
|
|
2469
|
-
(trigger) => trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0
|
|
2470
|
-
);
|
|
2494
|
+
const candidates = triggers.filter(isInjectTrigger);
|
|
2471
2495
|
if (candidates.length === 0) return [];
|
|
2472
2496
|
const matches = [];
|
|
2473
|
-
|
|
2497
|
+
const state = { db: null };
|
|
2498
|
+
const getDb = () => state.db ??= openPromptIndex(prompt);
|
|
2474
2499
|
try {
|
|
2475
2500
|
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
|
-
}
|
|
2501
|
+
if (!promptHit(promptProbe(trigger, prompt, getDb))) continue;
|
|
2502
|
+
matches.push({
|
|
2503
|
+
id: trigger.id,
|
|
2504
|
+
text: trigger.text,
|
|
2505
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2506
|
+
});
|
|
2492
2507
|
}
|
|
2493
2508
|
} finally {
|
|
2494
|
-
db?.close();
|
|
2509
|
+
state.db?.close();
|
|
2495
2510
|
}
|
|
2496
2511
|
return matches;
|
|
2497
2512
|
}
|
|
2513
|
+
function uniqueTexts(texts) {
|
|
2514
|
+
return [...new Set(texts)];
|
|
2515
|
+
}
|
|
2516
|
+
function renderPrompt(matches) {
|
|
2517
|
+
return uniqueTexts(matches.map((match) => match.text));
|
|
2518
|
+
}
|
|
2498
2519
|
function parsePretoolEvent(raw) {
|
|
2499
2520
|
const fields = eventFields(raw);
|
|
2500
2521
|
if (fields === null) return null;
|
|
@@ -2543,32 +2564,41 @@ function pathCandidates(toolInput, cwd) {
|
|
|
2543
2564
|
}
|
|
2544
2565
|
return candidates;
|
|
2545
2566
|
}
|
|
2567
|
+
function pretoolStage(trigger, toolName, toolInput, cwd) {
|
|
2568
|
+
if (trigger.tool !== void 0 && trigger.tool !== toolName) return "tool";
|
|
2569
|
+
if (trigger.args_match !== void 0) {
|
|
2570
|
+
const serialized = JSON.stringify(toolInput ?? {});
|
|
2571
|
+
if (!new RegExp(trigger.args_match).test(serialized)) return "args";
|
|
2572
|
+
}
|
|
2573
|
+
if (trigger.files !== void 0 && trigger.files.length > 0) {
|
|
2574
|
+
const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
|
|
2575
|
+
const hit = trigger.files.some((glob) => {
|
|
2576
|
+
const regex = globToRegExp(glob);
|
|
2577
|
+
return candidates.some((candidate) => regex.test(candidate));
|
|
2578
|
+
});
|
|
2579
|
+
if (!hit) return "files";
|
|
2580
|
+
}
|
|
2581
|
+
if (pretoolText(trigger) === void 0) return "payload";
|
|
2582
|
+
return "hit";
|
|
2583
|
+
}
|
|
2584
|
+
function pretoolText(trigger) {
|
|
2585
|
+
return trigger.enforce === "block" ? trigger.reason : trigger.text;
|
|
2586
|
+
}
|
|
2587
|
+
function pretoolMatch(trigger) {
|
|
2588
|
+
return {
|
|
2589
|
+
id: trigger.id,
|
|
2590
|
+
enforce: trigger.enforce,
|
|
2591
|
+
text: pretoolText(trigger),
|
|
2592
|
+
...trigger.when !== void 0 && { when: trigger.when },
|
|
2593
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2546
2596
|
function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
2547
2597
|
const matches = [];
|
|
2548
2598
|
for (const trigger of triggers) {
|
|
2549
2599
|
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
|
-
});
|
|
2600
|
+
if (pretoolStage(trigger, toolName, toolInput, cwd) !== "hit") continue;
|
|
2601
|
+
matches.push(pretoolMatch(trigger));
|
|
2572
2602
|
}
|
|
2573
2603
|
return matches;
|
|
2574
2604
|
}
|
|
@@ -2586,18 +2616,23 @@ function evaluateMatches(matches, vaultRoot2) {
|
|
|
2586
2616
|
}
|
|
2587
2617
|
return { fired, skipped };
|
|
2588
2618
|
}
|
|
2589
|
-
function
|
|
2590
|
-
if (typeof when === "string") return
|
|
2619
|
+
function evaluateWhenVerdict(when, vaultRoot2) {
|
|
2620
|
+
if (typeof when === "string") return "unknown";
|
|
2591
2621
|
try {
|
|
2592
2622
|
const than = newestUpdated(vaultRoot2, when.than);
|
|
2593
|
-
if (than === null) return
|
|
2623
|
+
if (than === null) return "vacuous";
|
|
2594
2624
|
const fresh = newestUpdated(vaultRoot2, when.fresh);
|
|
2595
|
-
if (fresh === null) return
|
|
2596
|
-
return fresh >= than;
|
|
2625
|
+
if (fresh === null) return "unmet";
|
|
2626
|
+
return fresh >= than ? "satisfied" : "unmet";
|
|
2597
2627
|
} catch {
|
|
2598
|
-
return
|
|
2628
|
+
return "unknown";
|
|
2599
2629
|
}
|
|
2600
2630
|
}
|
|
2631
|
+
function evaluateWhen(when, vaultRoot2) {
|
|
2632
|
+
const verdict = evaluateWhenVerdict(when, vaultRoot2);
|
|
2633
|
+
if (verdict === "unknown") return null;
|
|
2634
|
+
return verdict !== "unmet";
|
|
2635
|
+
}
|
|
2601
2636
|
function newestUpdated(vaultRoot2, globs) {
|
|
2602
2637
|
const regexes = globs.map(globToRegExp);
|
|
2603
2638
|
let newest = null;
|
|
@@ -2624,26 +2659,27 @@ function readUpdated(path) {
|
|
|
2624
2659
|
return null;
|
|
2625
2660
|
}
|
|
2626
2661
|
function renderPretool(matches, format) {
|
|
2627
|
-
const
|
|
2628
|
-
const
|
|
2629
|
-
const
|
|
2662
|
+
const byClass = (enforce) => uniqueTexts(matches.filter((match) => match.enforce === enforce).map((match) => match.text));
|
|
2663
|
+
const reasons = byClass("block");
|
|
2664
|
+
const context = byClass("inject");
|
|
2665
|
+
const warnings = byClass("warn");
|
|
2630
2666
|
if (format === "claude") {
|
|
2631
2667
|
const hookSpecificOutput = { hookEventName: "PreToolUse" };
|
|
2632
|
-
if (
|
|
2668
|
+
if (reasons.length > 0) {
|
|
2633
2669
|
hookSpecificOutput.permissionDecision = "deny";
|
|
2634
|
-
hookSpecificOutput.permissionDecisionReason =
|
|
2670
|
+
hookSpecificOutput.permissionDecisionReason = reasons.join("\n");
|
|
2635
2671
|
}
|
|
2636
2672
|
if (context.length > 0) {
|
|
2637
2673
|
hookSpecificOutput.additionalContext = context.join("\n");
|
|
2638
2674
|
}
|
|
2639
|
-
const decided =
|
|
2675
|
+
const decided = reasons.length > 0 || context.length > 0;
|
|
2640
2676
|
return { stdout: decided ? JSON.stringify({ hookSpecificOutput }) : null, stderr: warnings };
|
|
2641
2677
|
}
|
|
2642
2678
|
if (matches.length === 0) return { stdout: null, stderr: [] };
|
|
2643
2679
|
return {
|
|
2644
2680
|
stdout: JSON.stringify({
|
|
2645
|
-
decision:
|
|
2646
|
-
...
|
|
2681
|
+
decision: reasons.length > 0 ? "deny" : "none",
|
|
2682
|
+
...reasons.length > 0 && { reason: reasons.join("\n") },
|
|
2647
2683
|
context,
|
|
2648
2684
|
warnings
|
|
2649
2685
|
}),
|
|
@@ -2663,9 +2699,27 @@ function patchPaths(toolInput) {
|
|
|
2663
2699
|
}
|
|
2664
2700
|
return paths;
|
|
2665
2701
|
}
|
|
2702
|
+
function commandPaths(toolInput) {
|
|
2703
|
+
if (typeof toolInput !== "object" || toolInput === null) return [];
|
|
2704
|
+
const command2 = toolInput.command;
|
|
2705
|
+
if (typeof command2 !== "string") return [];
|
|
2706
|
+
const paths = [];
|
|
2707
|
+
for (const match of command2.matchAll(COMMAND_TOKEN_RE)) {
|
|
2708
|
+
const token = match[1] ?? match[2] ?? match[3];
|
|
2709
|
+
for (const piece of token.split(/[;|&<>()]+/)) {
|
|
2710
|
+
if (piece === "") continue;
|
|
2711
|
+
if (piece === "." || piece === ".." || PATHISH_RE.test(piece)) paths.push(piece);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
return paths;
|
|
2715
|
+
}
|
|
2666
2716
|
function vaultPathTouched(toolInput, vaultRoot2, cwd) {
|
|
2667
2717
|
const root = resolve3(vaultRoot2);
|
|
2668
|
-
const candidates = [
|
|
2718
|
+
const candidates = [
|
|
2719
|
+
...pathCandidates(toolInput, cwd),
|
|
2720
|
+
...patchPaths(toolInput),
|
|
2721
|
+
...commandPaths(toolInput)
|
|
2722
|
+
];
|
|
2669
2723
|
return candidates.some((candidate) => {
|
|
2670
2724
|
const absolute = resolve3(cwd ?? ".", candidate);
|
|
2671
2725
|
return absolute === root || absolute.startsWith(`${root}/`);
|
|
@@ -2711,50 +2765,141 @@ function renderStop(findings, reason) {
|
|
|
2711
2765
|
${lines.join("\n")}`
|
|
2712
2766
|
});
|
|
2713
2767
|
}
|
|
2714
|
-
function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
2768
|
+
function dedupePretoolMatches(stateDir, sessionId, matches, persist = true) {
|
|
2715
2769
|
const blocks = matches.filter((match) => match.enforce === "block");
|
|
2716
2770
|
const rest = matches.filter((match) => match.enforce !== "block");
|
|
2717
|
-
const fresh = dedupeMatches(stateDir, sessionId, rest);
|
|
2771
|
+
const fresh = dedupeMatches(stateDir, sessionId, rest, Date.now(), persist);
|
|
2718
2772
|
return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
|
|
2719
2773
|
}
|
|
2720
2774
|
function hookStateDir() {
|
|
2721
2775
|
return join10(kmdHome(), "state", "hook");
|
|
2722
2776
|
}
|
|
2723
|
-
function
|
|
2777
|
+
function explainPrompt(options) {
|
|
2778
|
+
const now = options.now ?? Date.now();
|
|
2779
|
+
const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
|
|
2780
|
+
const entries = [];
|
|
2781
|
+
const rendered = [];
|
|
2782
|
+
const state = { db: null };
|
|
2783
|
+
const getDb = () => state.db ??= openPromptIndex(options.prompt);
|
|
2784
|
+
try {
|
|
2785
|
+
for (const trigger of options.triggers) {
|
|
2786
|
+
if (!isInjectTrigger(trigger)) {
|
|
2787
|
+
entries.push({ id: trigger.id, considered: false });
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
const probe = promptProbe(trigger, options.prompt, getDb);
|
|
2791
|
+
const matched = promptHit(probe);
|
|
2792
|
+
const entry = {
|
|
2793
|
+
id: trigger.id,
|
|
2794
|
+
considered: true,
|
|
2795
|
+
keywords: probe.keywords,
|
|
2796
|
+
intent: probe.intent,
|
|
2797
|
+
matched
|
|
2798
|
+
};
|
|
2799
|
+
entries.push(entry);
|
|
2800
|
+
if (!matched) {
|
|
2801
|
+
entry.fired = false;
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
const key = dedupKey(trigger, now);
|
|
2805
|
+
entry.dedup = key === null ? "never" : fired.has(key) ? "suppressed" : "fresh";
|
|
2806
|
+
entry.fired = entry.dedup !== "suppressed";
|
|
2807
|
+
if (entry.fired) rendered.push({ id: trigger.id, text: trigger.text });
|
|
2808
|
+
}
|
|
2809
|
+
} finally {
|
|
2810
|
+
state.db?.close();
|
|
2811
|
+
}
|
|
2812
|
+
return { triggers: entries, output: renderPrompt(rendered) };
|
|
2813
|
+
}
|
|
2814
|
+
function explainPretool(options) {
|
|
2815
|
+
const now = options.now ?? Date.now();
|
|
2816
|
+
const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
|
|
2817
|
+
const entries = [];
|
|
2818
|
+
const rendered = [];
|
|
2819
|
+
for (const trigger of options.triggers) {
|
|
2820
|
+
if (trigger.on !== "pretool") {
|
|
2821
|
+
entries.push({ id: trigger.id, considered: false });
|
|
2822
|
+
continue;
|
|
2823
|
+
}
|
|
2824
|
+
const stage = pretoolStage(trigger, options.toolName, options.toolInput, options.cwd);
|
|
2825
|
+
const entry = {
|
|
2826
|
+
id: trigger.id,
|
|
2827
|
+
enforce: trigger.enforce,
|
|
2828
|
+
considered: true,
|
|
2829
|
+
matcher: stage === "hit" ? "hit" : `${stage}-miss`
|
|
2830
|
+
};
|
|
2831
|
+
entries.push(entry);
|
|
2832
|
+
if (stage !== "hit") {
|
|
2833
|
+
entry.fired = false;
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
if (trigger.when !== void 0) {
|
|
2837
|
+
entry.when = evaluateWhenVerdict(trigger.when, options.vaultRoot);
|
|
2838
|
+
if (entry.when !== "unmet") {
|
|
2839
|
+
entry.fired = false;
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
const match = pretoolMatch(trigger);
|
|
2844
|
+
if (match.enforce === "block") {
|
|
2845
|
+
entry.dedup = "exempt";
|
|
2846
|
+
} else {
|
|
2847
|
+
const key = dedupKey(match, now);
|
|
2848
|
+
entry.dedup = key === null ? "never" : fired.has(key) ? "suppressed" : "fresh";
|
|
2849
|
+
}
|
|
2850
|
+
entry.fired = entry.dedup !== "suppressed";
|
|
2851
|
+
if (entry.fired) rendered.push(match);
|
|
2852
|
+
}
|
|
2853
|
+
return { triggers: entries, outcome: renderPretool(rendered, options.format ?? "neutral") };
|
|
2854
|
+
}
|
|
2855
|
+
function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist = true) {
|
|
2724
2856
|
if (matches.length === 0) return [];
|
|
2725
|
-
const
|
|
2726
|
-
const fired = readFired(
|
|
2857
|
+
const dir = join10(stateDir, safeName(sessionId));
|
|
2858
|
+
const fired = readFired(dir);
|
|
2727
2859
|
const fresh = [];
|
|
2728
2860
|
const record = [];
|
|
2729
2861
|
for (const match of matches) {
|
|
2730
|
-
|
|
2862
|
+
const key = dedupKey(match, now);
|
|
2863
|
+
if (key === null) {
|
|
2731
2864
|
fresh.push(match);
|
|
2732
2865
|
continue;
|
|
2733
2866
|
}
|
|
2734
|
-
const key = typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id;
|
|
2735
2867
|
if (fired.has(key)) continue;
|
|
2736
2868
|
fresh.push(match);
|
|
2737
2869
|
record.push(key);
|
|
2738
2870
|
}
|
|
2739
|
-
if (record.length > 0) {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2871
|
+
if (persist && record.length > 0) {
|
|
2872
|
+
try {
|
|
2873
|
+
mkdirSync4(dir, { recursive: true });
|
|
2874
|
+
for (const key of record) {
|
|
2875
|
+
try {
|
|
2876
|
+
writeFileSync(join10(dir, key), "", { flag: "wx" });
|
|
2877
|
+
} catch (err) {
|
|
2878
|
+
if (err.code !== "EEXIST") throw err;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
pruneStale(stateDir, dir);
|
|
2882
|
+
} catch (err) {
|
|
2883
|
+
diag2(`dedup state not persisted: ${err instanceof Error ? err.message : String(err)}`);
|
|
2743
2884
|
}
|
|
2744
|
-
writeFileSync(file, JSON.stringify([...fired]));
|
|
2745
|
-
pruneStale(stateDir, file);
|
|
2746
2885
|
}
|
|
2747
2886
|
return fresh;
|
|
2748
2887
|
}
|
|
2749
|
-
function
|
|
2888
|
+
function safeName(part) {
|
|
2889
|
+
return part.replace(/[^A-Za-z0-9._@-]/g, "_");
|
|
2890
|
+
}
|
|
2891
|
+
function dedupKey(match, now) {
|
|
2892
|
+
if (match.dedup === "never") return null;
|
|
2893
|
+
return safeName(
|
|
2894
|
+
typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
function readFired(dir) {
|
|
2750
2898
|
try {
|
|
2751
|
-
|
|
2752
|
-
if (Array.isArray(data)) {
|
|
2753
|
-
return new Set(data.filter((entry) => typeof entry === "string"));
|
|
2754
|
-
}
|
|
2899
|
+
return new Set(readdirSync2(dir));
|
|
2755
2900
|
} catch {
|
|
2901
|
+
return /* @__PURE__ */ new Set();
|
|
2756
2902
|
}
|
|
2757
|
-
return /* @__PURE__ */ new Set();
|
|
2758
2903
|
}
|
|
2759
2904
|
function pruneStale(stateDir, keep) {
|
|
2760
2905
|
try {
|
|
@@ -2762,7 +2907,7 @@ function pruneStale(stateDir, keep) {
|
|
|
2762
2907
|
for (const entry of readdirSync2(stateDir)) {
|
|
2763
2908
|
const path = join10(stateDir, entry);
|
|
2764
2909
|
if (path !== keep && statSync(path).mtimeMs < cutoff) {
|
|
2765
|
-
rmSync2(path, { force: true });
|
|
2910
|
+
rmSync2(path, { recursive: true, force: true });
|
|
2766
2911
|
}
|
|
2767
2912
|
}
|
|
2768
2913
|
} catch {
|
|
@@ -2776,7 +2921,9 @@ function hookInvocation() {
|
|
|
2776
2921
|
options: {
|
|
2777
2922
|
scope: { type: "string" },
|
|
2778
2923
|
harness: { type: "string" },
|
|
2779
|
-
triggers: { type: "string" }
|
|
2924
|
+
triggers: { type: "string" },
|
|
2925
|
+
"dry-run": { type: "boolean" },
|
|
2926
|
+
explain: { type: "boolean" }
|
|
2780
2927
|
}
|
|
2781
2928
|
});
|
|
2782
2929
|
const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
|
|
@@ -2788,7 +2935,9 @@ function hookInvocation() {
|
|
|
2788
2935
|
vaultRoot: vaultRoot2,
|
|
2789
2936
|
scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
|
|
2790
2937
|
harness: values2.harness,
|
|
2791
|
-
triggersFile: values2.triggers
|
|
2938
|
+
triggersFile: values2.triggers,
|
|
2939
|
+
dryRun: values2["dry-run"] === true || values2.explain === true,
|
|
2940
|
+
explain: values2.explain === true
|
|
2792
2941
|
};
|
|
2793
2942
|
}
|
|
2794
2943
|
function resolveFileTriggers(invocation) {
|
|
@@ -2826,9 +2975,26 @@ async function runHookPrompt() {
|
|
|
2826
2975
|
for (const id of duplicates) {
|
|
2827
2976
|
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2828
2977
|
}
|
|
2978
|
+
if (invocation.explain) {
|
|
2979
|
+
const trace = explainPrompt({
|
|
2980
|
+
prompt: event.prompt,
|
|
2981
|
+
triggers,
|
|
2982
|
+
stateDir: hookStateDir(),
|
|
2983
|
+
sessionId: event.session_id
|
|
2984
|
+
});
|
|
2985
|
+
console.log(JSON.stringify({ event: "prompt", scope: scope ?? null, duplicates, ...trace }));
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2829
2988
|
const matches = matchPromptTriggers(event.prompt, triggers);
|
|
2830
|
-
|
|
2831
|
-
|
|
2989
|
+
const fresh = dedupeMatches(
|
|
2990
|
+
hookStateDir(),
|
|
2991
|
+
event.session_id,
|
|
2992
|
+
matches,
|
|
2993
|
+
Date.now(),
|
|
2994
|
+
!invocation.dryRun
|
|
2995
|
+
);
|
|
2996
|
+
for (const line of renderPrompt(fresh)) {
|
|
2997
|
+
console.log(line);
|
|
2832
2998
|
}
|
|
2833
2999
|
} catch (err) {
|
|
2834
3000
|
diag2(err instanceof Error ? err.message : String(err));
|
|
@@ -2860,13 +3026,27 @@ async function runHookPretool() {
|
|
|
2860
3026
|
for (const id of duplicates) {
|
|
2861
3027
|
diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
|
|
2862
3028
|
}
|
|
3029
|
+
if (invocation.explain) {
|
|
3030
|
+
const trace = explainPretool({
|
|
3031
|
+
toolName: event.tool_name,
|
|
3032
|
+
toolInput: event.tool_input,
|
|
3033
|
+
triggers,
|
|
3034
|
+
vaultRoot: vaultRoot2,
|
|
3035
|
+
stateDir: hookStateDir(),
|
|
3036
|
+
sessionId: event.session_id,
|
|
3037
|
+
...event.cwd !== void 0 && { cwd: event.cwd },
|
|
3038
|
+
format
|
|
3039
|
+
});
|
|
3040
|
+
console.log(JSON.stringify({ event: "pretool", scope: scope ?? null, duplicates, ...trace }));
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
2863
3043
|
const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
|
|
2864
3044
|
const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
|
|
2865
3045
|
for (const id of skipped) {
|
|
2866
3046
|
diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
|
|
2867
3047
|
}
|
|
2868
3048
|
const rendered = renderPretool(
|
|
2869
|
-
dedupePretoolMatches(hookStateDir(), event.session_id, fired),
|
|
3049
|
+
dedupePretoolMatches(hookStateDir(), event.session_id, fired, !invocation.dryRun),
|
|
2870
3050
|
format
|
|
2871
3051
|
);
|
|
2872
3052
|
for (const line of rendered.stderr) {
|
|
@@ -2883,6 +3063,10 @@ async function runHookPosttool() {
|
|
|
2883
3063
|
try {
|
|
2884
3064
|
const invocation = hookInvocation();
|
|
2885
3065
|
if (invocation === null) return;
|
|
3066
|
+
if (invocation.dryRun) {
|
|
3067
|
+
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
2886
3070
|
let format = "neutral";
|
|
2887
3071
|
if (invocation.harness === "claude") {
|
|
2888
3072
|
format = "claude";
|
|
@@ -2918,6 +3102,10 @@ async function runHookStop() {
|
|
|
2918
3102
|
try {
|
|
2919
3103
|
const invocation = hookInvocation();
|
|
2920
3104
|
if (invocation === null) return;
|
|
3105
|
+
if (invocation.dryRun) {
|
|
3106
|
+
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
2921
3109
|
const event = parseStopEvent(await readStdin());
|
|
2922
3110
|
if (event === null) {
|
|
2923
3111
|
diag2("stdin is not a stop event ({session_id})");
|
|
@@ -2939,6 +3127,42 @@ async function runHookStop() {
|
|
|
2939
3127
|
diag2(err instanceof Error ? err.message : String(err));
|
|
2940
3128
|
}
|
|
2941
3129
|
}
|
|
3130
|
+
function parseSessionStartEvent(raw) {
|
|
3131
|
+
const fields = eventFields(raw);
|
|
3132
|
+
if (fields === null) return null;
|
|
3133
|
+
const { session_id, cwd, source } = fields;
|
|
3134
|
+
if (typeof session_id !== "string") return null;
|
|
3135
|
+
return {
|
|
3136
|
+
session_id,
|
|
3137
|
+
...typeof cwd === "string" && { cwd },
|
|
3138
|
+
...typeof source === "string" && { source }
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
function renderSessionStart(scope, source, messages = {}) {
|
|
3142
|
+
const text = source === "compact" ? messages.reorient?.text ?? REORIENT_TEXT : messages.orient?.text ?? ORIENT_TEXT;
|
|
3143
|
+
return `Wiki scope "${scope}": ${text}`;
|
|
3144
|
+
}
|
|
3145
|
+
async function runHookSessionStart() {
|
|
3146
|
+
try {
|
|
3147
|
+
const invocation = hookInvocation();
|
|
3148
|
+
if (invocation === null) return;
|
|
3149
|
+
if (invocation.dryRun) {
|
|
3150
|
+
diag2("--dry-run/--explain support prompt and pretool events only");
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
const event = parseSessionStartEvent(await readStdin());
|
|
3154
|
+
if (event === null) {
|
|
3155
|
+
diag2("stdin is not a session-start event ({session_id})");
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
const config = await loadVaultConfig(invocation.vaultRoot);
|
|
3159
|
+
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
3160
|
+
if (scope === void 0) return;
|
|
3161
|
+
console.log(renderSessionStart(scope, event.source, config.builtin_hooks ?? {}));
|
|
3162
|
+
} catch (err) {
|
|
3163
|
+
diag2(err instanceof Error ? err.message : String(err));
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
2942
3166
|
async function readStdin() {
|
|
2943
3167
|
process.stdin.setEncoding("utf8");
|
|
2944
3168
|
let input = "";
|
|
@@ -2950,7 +3174,7 @@ async function readStdin() {
|
|
|
2950
3174
|
function diag2(message) {
|
|
2951
3175
|
console.error(`kmd hook: ${message}`);
|
|
2952
3176
|
}
|
|
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;
|
|
3177
|
+
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, ORIENT_TEXT, REORIENT_TEXT;
|
|
2954
3178
|
var init_hook = __esm({
|
|
2955
3179
|
"../cli/src/hook.ts"() {
|
|
2956
3180
|
"use strict";
|
|
@@ -2964,9 +3188,13 @@ var init_hook = __esm({
|
|
|
2964
3188
|
KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
|
|
2965
3189
|
ALL_SCOPES_KEY = "_all";
|
|
2966
3190
|
PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
3191
|
+
COMMAND_TOKEN_RE = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
3192
|
+
PATHISH_RE = /[/*$]|\.(?:md|base|canvas|ya?ml)$/i;
|
|
2967
3193
|
RESYNC_REASON = "Edit landed; the index sync is held until these validate errors are fixed";
|
|
2968
3194
|
RESYNC_TEXT = "kmd sync failed \u2014 index not updated; see hook stderr";
|
|
2969
3195
|
HANDOFF_GATE_REASON = "Validate errors are outstanding and the index sync is held \u2014 fix them, let the resync run, then finish";
|
|
3196
|
+
ORIENT_TEXT = "prime via the wiki MCP prime tool before substantive work \u2014 the primer carries current focus, book of work, and invariants.";
|
|
3197
|
+
REORIENT_TEXT = "context was compacted and transcript detail is lost \u2014 re-read the primer via the wiki MCP prime tool and route uncaptured findings into the wiki before continuing.";
|
|
2970
3198
|
}
|
|
2971
3199
|
});
|
|
2972
3200
|
|
|
@@ -2987,7 +3215,7 @@ commands:
|
|
|
2987
3215
|
mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
|
|
2988
3216
|
config [<vault-root>] print vault + index resolution; with no vault, list known vaults
|
|
2989
3217
|
db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
|
|
2990
|
-
hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
3218
|
+
hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2991
3219
|
harness gate engine: JSON event on stdin, decision/context on stdout;
|
|
2992
3220
|
posttool auto-runs validate + sync after a vault write;
|
|
2993
3221
|
stop blocks the handoff once while validate errors hold the sync
|
|
@@ -3068,11 +3296,14 @@ async function run() {
|
|
|
3068
3296
|
} else if (sub === "stop") {
|
|
3069
3297
|
const { runHookStop: runHookStop2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
3070
3298
|
await runHookStop2();
|
|
3299
|
+
} else if (sub === "session-start") {
|
|
3300
|
+
const { runHookSessionStart: runHookSessionStart2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
3301
|
+
await runHookSessionStart2();
|
|
3071
3302
|
} else if (sub) {
|
|
3072
3303
|
console.error(`kmd hook: unknown event: ${sub}`);
|
|
3073
3304
|
} else {
|
|
3074
3305
|
console.error(
|
|
3075
|
-
"usage: kmd hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
3306
|
+
"usage: kmd hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
3076
3307
|
);
|
|
3077
3308
|
process.exit(2);
|
|
3078
3309
|
}
|