@bartolli/kmd 0.7.0 → 0.9.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 +117 -19
- 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, 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";
|
|
@@ -177,6 +177,10 @@ var init_vault_config = __esm({
|
|
|
177
177
|
than: z.array(z.string().min(1)).min(1)
|
|
178
178
|
})
|
|
179
179
|
]);
|
|
180
|
+
DedupSchema = z.union([
|
|
181
|
+
z.enum(["session", "never"]),
|
|
182
|
+
z.strictObject({ minutes: z.number().int().positive() })
|
|
183
|
+
]);
|
|
180
184
|
TriggerSchema = z.strictObject({
|
|
181
185
|
id: z.string().min(1).describe("Unique per scope list; duplicates keep the first occurrence."),
|
|
182
186
|
on: z.enum(["prompt", "pretool"]),
|
|
@@ -190,7 +194,10 @@ var init_vault_config = __esm({
|
|
|
190
194
|
"Precondition \u2014 the gate fires only when it is UNMET. newer-than: the newest page matching fresh must carry frontmatter updated at or after the newest matching than."
|
|
191
195
|
),
|
|
192
196
|
text: z.string().optional().describe("Required for inject and warn \u2014 the line emitted."),
|
|
193
|
-
reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads.")
|
|
197
|
+
reason: z.string().optional().describe("Required for block \u2014 the denial the agent reads."),
|
|
198
|
+
dedup: DedupSchema.optional().describe(
|
|
199
|
+
"Re-fire policy: session (default, once per session), never, or {minutes: N} for at most once per bucket. Rejected on block triggers \u2014 blocks are dedup-exempt."
|
|
200
|
+
)
|
|
194
201
|
}).superRefine((trigger, ctx) => {
|
|
195
202
|
if (trigger.on === "prompt" && !trigger.keywords?.length && !trigger.intent?.length) {
|
|
196
203
|
ctx.addIssue({
|
|
@@ -210,6 +217,12 @@ var init_vault_config = __esm({
|
|
|
210
217
|
message: `trigger "${trigger.id}": files applies to pretool triggers only`
|
|
211
218
|
});
|
|
212
219
|
}
|
|
220
|
+
if (trigger.enforce === "block" && trigger.dedup !== void 0) {
|
|
221
|
+
ctx.addIssue({
|
|
222
|
+
code: "custom",
|
|
223
|
+
message: `block trigger "${trigger.id}" may not set dedup \u2014 blocks fire on every matching event`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
213
226
|
if (trigger.enforce === "block" ? trigger.reason === void 0 : trigger.text === void 0) {
|
|
214
227
|
ctx.addIssue({
|
|
215
228
|
code: "custom",
|
|
@@ -228,6 +241,15 @@ var init_vault_config = __esm({
|
|
|
228
241
|
}
|
|
229
242
|
});
|
|
230
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
|
+
});
|
|
231
253
|
VaultConfigSchema = z.strictObject({
|
|
232
254
|
scopes: z.record(z.string(), ScopeSchema).describe("Scope name \u2192 entry; key = directory name under projects/."),
|
|
233
255
|
kinds: z.array(KindEntrySchema).describe(
|
|
@@ -246,6 +268,9 @@ var init_vault_config = __esm({
|
|
|
246
268
|
triggers: TriggersSchema.optional().describe(
|
|
247
269
|
'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
|
|
248
270
|
),
|
|
271
|
+
builtin_hooks: BuiltinHooksSchema.optional().describe(
|
|
272
|
+
"Message overrides for the fixed-function hooks (resync, handoff-gate) by public id."
|
|
273
|
+
),
|
|
249
274
|
triggers_extra: TriggersSchema.optional().describe(
|
|
250
275
|
'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
|
|
251
276
|
)
|
|
@@ -1737,7 +1762,7 @@ var init_authoring = __esm({
|
|
|
1737
1762
|
].join("\n");
|
|
1738
1763
|
DEFAULT_SYNC_PROTOCOL = [
|
|
1739
1764
|
"Edit the smallest set of files that reflects the change. A milestone tick is plan-only; don't cascade to index.md unless phase or status changed. Controlled-vocabulary edits (`vault.yaml`) need explicit user approval.",
|
|
1740
|
-
"
|
|
1765
|
+
"Harnesses with the posttool hook validate and sync automatically on every vault write. Check `kmd config`: if the `synced` line did not advance past your edits, the hook is not wired \u2014 run `kmd validate`, fix findings, then `kmd sync`."
|
|
1741
1766
|
].join("\n");
|
|
1742
1767
|
CANONICAL_STATUS_FLOW = ["draft", "active", "superseded", "archived"];
|
|
1743
1768
|
}
|
|
@@ -2343,12 +2368,15 @@ __export(hook_exports, {
|
|
|
2343
2368
|
matchPromptTriggers: () => matchPromptTriggers,
|
|
2344
2369
|
parsePretoolEvent: () => parsePretoolEvent,
|
|
2345
2370
|
parsePromptEvent: () => parsePromptEvent,
|
|
2371
|
+
parseStopEvent: () => parseStopEvent,
|
|
2346
2372
|
renderPosttool: () => renderPosttool,
|
|
2347
2373
|
renderPretool: () => renderPretool,
|
|
2374
|
+
renderStop: () => renderStop,
|
|
2348
2375
|
resolveScope: () => resolveScope,
|
|
2349
2376
|
runHookPosttool: () => runHookPosttool,
|
|
2350
2377
|
runHookPretool: () => runHookPretool,
|
|
2351
2378
|
runHookPrompt: () => runHookPrompt,
|
|
2379
|
+
runHookStop: () => runHookStop,
|
|
2352
2380
|
vaultPathTouched: () => vaultPathTouched
|
|
2353
2381
|
});
|
|
2354
2382
|
import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
|
|
@@ -2455,7 +2483,11 @@ function matchPromptTriggers(prompt, triggers) {
|
|
|
2455
2483
|
hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
|
|
2456
2484
|
}
|
|
2457
2485
|
if (hit) {
|
|
2458
|
-
matches.push({
|
|
2486
|
+
matches.push({
|
|
2487
|
+
id: trigger.id,
|
|
2488
|
+
text: trigger.text,
|
|
2489
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2490
|
+
});
|
|
2459
2491
|
}
|
|
2460
2492
|
}
|
|
2461
2493
|
} finally {
|
|
@@ -2521,7 +2553,7 @@ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
|
2521
2553
|
if (!new RegExp(trigger.args_match).test(serialized)) continue;
|
|
2522
2554
|
}
|
|
2523
2555
|
if (trigger.files !== void 0 && trigger.files.length > 0) {
|
|
2524
|
-
const candidates = pathCandidates(toolInput, cwd);
|
|
2556
|
+
const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
|
|
2525
2557
|
const hit = trigger.files.some((glob) => {
|
|
2526
2558
|
const regex = globToRegExp(glob);
|
|
2527
2559
|
return candidates.some((candidate) => regex.test(candidate));
|
|
@@ -2534,7 +2566,8 @@ function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
|
|
|
2534
2566
|
id: trigger.id,
|
|
2535
2567
|
enforce: trigger.enforce,
|
|
2536
2568
|
text,
|
|
2537
|
-
...trigger.when !== void 0 && { when: trigger.when }
|
|
2569
|
+
...trigger.when !== void 0 && { when: trigger.when },
|
|
2570
|
+
...trigger.dedup !== void 0 && { dedup: trigger.dedup }
|
|
2538
2571
|
});
|
|
2539
2572
|
}
|
|
2540
2573
|
return matches;
|
|
@@ -2638,25 +2671,46 @@ function vaultPathTouched(toolInput, vaultRoot2, cwd) {
|
|
|
2638
2671
|
return absolute === root || absolute.startsWith(`${root}/`);
|
|
2639
2672
|
});
|
|
2640
2673
|
}
|
|
2641
|
-
function renderPosttool(findings, synced, format) {
|
|
2674
|
+
function renderPosttool(findings, synced, format, messages = {}) {
|
|
2642
2675
|
if (findings.length === 0 && synced) return null;
|
|
2643
2676
|
const lines = findings.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
2644
2677
|
if (format === "claude") {
|
|
2645
2678
|
if (hasErrors(findings)) {
|
|
2646
2679
|
return JSON.stringify({
|
|
2647
2680
|
decision: "block",
|
|
2648
|
-
reason:
|
|
2681
|
+
reason: `${messages.reason ?? RESYNC_REASON}:
|
|
2649
2682
|
${lines.join("\n")}`
|
|
2650
2683
|
});
|
|
2651
2684
|
}
|
|
2652
2685
|
const hookSpecificOutput = { hookEventName: "PostToolUse" };
|
|
2653
2686
|
const notes = [...lines];
|
|
2654
|
-
if (!synced) notes.push(
|
|
2687
|
+
if (!synced) notes.push(messages.text ?? RESYNC_TEXT);
|
|
2655
2688
|
hookSpecificOutput.additionalContext = notes.join("\n");
|
|
2656
2689
|
return JSON.stringify({ hookSpecificOutput });
|
|
2657
2690
|
}
|
|
2658
2691
|
return JSON.stringify({ findings, synced });
|
|
2659
2692
|
}
|
|
2693
|
+
function parseStopEvent(raw) {
|
|
2694
|
+
const fields = eventFields(raw);
|
|
2695
|
+
if (fields === null) return null;
|
|
2696
|
+
const { session_id, cwd, stop_hook_active } = fields;
|
|
2697
|
+
if (typeof session_id !== "string") return null;
|
|
2698
|
+
return {
|
|
2699
|
+
session_id,
|
|
2700
|
+
...typeof cwd === "string" && { cwd },
|
|
2701
|
+
...typeof stop_hook_active === "boolean" && { stop_hook_active }
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
function renderStop(findings, reason) {
|
|
2705
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
2706
|
+
if (errors.length === 0) return null;
|
|
2707
|
+
const lines = errors.map((f) => `${f.severity}: ${f.path} [${f.rule}] ${f.message}`);
|
|
2708
|
+
return JSON.stringify({
|
|
2709
|
+
decision: "block",
|
|
2710
|
+
reason: `${reason ?? HANDOFF_GATE_REASON}:
|
|
2711
|
+
${lines.join("\n")}`
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2660
2714
|
function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
2661
2715
|
const blocks = matches.filter((match) => match.enforce === "block");
|
|
2662
2716
|
const rest = matches.filter((match) => match.enforce !== "block");
|
|
@@ -2666,15 +2720,26 @@ function dedupePretoolMatches(stateDir, sessionId, matches) {
|
|
|
2666
2720
|
function hookStateDir() {
|
|
2667
2721
|
return join10(kmdHome(), "state", "hook");
|
|
2668
2722
|
}
|
|
2669
|
-
function dedupeMatches(stateDir, sessionId, matches) {
|
|
2723
|
+
function dedupeMatches(stateDir, sessionId, matches, now = Date.now()) {
|
|
2670
2724
|
if (matches.length === 0) return [];
|
|
2671
2725
|
const file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
2672
2726
|
const fired = readFired(file);
|
|
2673
|
-
const fresh =
|
|
2674
|
-
|
|
2727
|
+
const fresh = [];
|
|
2728
|
+
const record = [];
|
|
2729
|
+
for (const match of matches) {
|
|
2730
|
+
if (match.dedup === "never") {
|
|
2731
|
+
fresh.push(match);
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
const key = typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id;
|
|
2735
|
+
if (fired.has(key)) continue;
|
|
2736
|
+
fresh.push(match);
|
|
2737
|
+
record.push(key);
|
|
2738
|
+
}
|
|
2739
|
+
if (record.length > 0) {
|
|
2675
2740
|
mkdirSync4(stateDir, { recursive: true });
|
|
2676
|
-
for (const
|
|
2677
|
-
fired.add(
|
|
2741
|
+
for (const key of record) {
|
|
2742
|
+
fired.add(key);
|
|
2678
2743
|
}
|
|
2679
2744
|
writeFileSync(file, JSON.stringify([...fired]));
|
|
2680
2745
|
pruneStale(stateDir, file);
|
|
@@ -2830,6 +2895,7 @@ async function runHookPosttool() {
|
|
|
2830
2895
|
return;
|
|
2831
2896
|
}
|
|
2832
2897
|
if (!vaultPathTouched(event.tool_input, invocation.vaultRoot, event.cwd)) return;
|
|
2898
|
+
const config = await loadVaultConfig(invocation.vaultRoot);
|
|
2833
2899
|
const findings = await validateVault(invocation.vaultRoot);
|
|
2834
2900
|
let synced = false;
|
|
2835
2901
|
if (!hasErrors(findings)) {
|
|
@@ -2840,7 +2906,7 @@ async function runHookPosttool() {
|
|
|
2840
2906
|
diag2(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2841
2907
|
}
|
|
2842
2908
|
}
|
|
2843
|
-
const rendered = renderPosttool(findings, synced, format);
|
|
2909
|
+
const rendered = renderPosttool(findings, synced, format, config.builtin_hooks?.resync ?? {});
|
|
2844
2910
|
if (rendered !== null) {
|
|
2845
2911
|
console.log(rendered);
|
|
2846
2912
|
}
|
|
@@ -2848,6 +2914,31 @@ async function runHookPosttool() {
|
|
|
2848
2914
|
diag2(err instanceof Error ? err.message : String(err));
|
|
2849
2915
|
}
|
|
2850
2916
|
}
|
|
2917
|
+
async function runHookStop() {
|
|
2918
|
+
try {
|
|
2919
|
+
const invocation = hookInvocation();
|
|
2920
|
+
if (invocation === null) return;
|
|
2921
|
+
const event = parseStopEvent(await readStdin());
|
|
2922
|
+
if (event === null) {
|
|
2923
|
+
diag2("stdin is not a stop event ({session_id})");
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
if (event.stop_hook_active === true) return;
|
|
2927
|
+
const config = await loadVaultConfig(invocation.vaultRoot);
|
|
2928
|
+
const scope = invocation.scope ?? resolveScope(config, event.cwd);
|
|
2929
|
+
if (scope === void 0) return;
|
|
2930
|
+
const rendered = renderStop(
|
|
2931
|
+
await validateVault(invocation.vaultRoot),
|
|
2932
|
+
config.builtin_hooks?.["handoff-gate"]?.reason
|
|
2933
|
+
);
|
|
2934
|
+
if (rendered === null) return;
|
|
2935
|
+
const fired = dedupeMatches(hookStateDir(), event.session_id, [{ id: "handoff-gate" }]);
|
|
2936
|
+
if (fired.length === 0) return;
|
|
2937
|
+
console.log(rendered);
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
diag2(err instanceof Error ? err.message : String(err));
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2851
2942
|
async function readStdin() {
|
|
2852
2943
|
process.stdin.setEncoding("utf8");
|
|
2853
2944
|
let input = "";
|
|
@@ -2859,7 +2950,7 @@ async function readStdin() {
|
|
|
2859
2950
|
function diag2(message) {
|
|
2860
2951
|
console.error(`kmd hook: ${message}`);
|
|
2861
2952
|
}
|
|
2862
|
-
var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE;
|
|
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;
|
|
2863
2954
|
var init_hook = __esm({
|
|
2864
2955
|
"../cli/src/hook.ts"() {
|
|
2865
2956
|
"use strict";
|
|
@@ -2873,6 +2964,9 @@ var init_hook = __esm({
|
|
|
2873
2964
|
KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
|
|
2874
2965
|
ALL_SCOPES_KEY = "_all";
|
|
2875
2966
|
PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
2967
|
+
RESYNC_REASON = "Edit landed; the index sync is held until these validate errors are fixed";
|
|
2968
|
+
RESYNC_TEXT = "kmd sync failed \u2014 index not updated; see hook stderr";
|
|
2969
|
+
HANDOFF_GATE_REASON = "Validate errors are outstanding and the index sync is held \u2014 fix them, let the resync run, then finish";
|
|
2876
2970
|
}
|
|
2877
2971
|
});
|
|
2878
2972
|
|
|
@@ -2893,9 +2987,10 @@ commands:
|
|
|
2893
2987
|
mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
|
|
2894
2988
|
config [<vault-root>] print vault + index resolution; with no vault, list known vaults
|
|
2895
2989
|
db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
|
|
2896
|
-
hook <prompt|pretool|posttool> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2990
|
+
hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
|
|
2897
2991
|
harness gate engine: JSON event on stdin, decision/context on stdout;
|
|
2898
|
-
posttool auto-runs validate + sync after a vault write
|
|
2992
|
+
posttool auto-runs validate + sync after a vault write;
|
|
2993
|
+
stop blocks the handoff once while validate errors hold the sync
|
|
2899
2994
|
|
|
2900
2995
|
options:
|
|
2901
2996
|
--version print version
|
|
@@ -2970,11 +3065,14 @@ async function run() {
|
|
|
2970
3065
|
} else if (sub === "posttool") {
|
|
2971
3066
|
const { runHookPosttool: runHookPosttool2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
2972
3067
|
await runHookPosttool2();
|
|
3068
|
+
} else if (sub === "stop") {
|
|
3069
|
+
const { runHookStop: runHookStop2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
|
|
3070
|
+
await runHookStop2();
|
|
2973
3071
|
} else if (sub) {
|
|
2974
3072
|
console.error(`kmd hook: unknown event: ${sub}`);
|
|
2975
3073
|
} else {
|
|
2976
3074
|
console.error(
|
|
2977
|
-
"usage: kmd hook <prompt|pretool|posttool> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
3075
|
+
"usage: kmd hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
|
|
2978
3076
|
);
|
|
2979
3077
|
process.exit(2);
|
|
2980
3078
|
}
|