@lh8ppl/claude-memory-kit 0.3.5 → 0.4.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/README.md +6 -0
- package/bin/cmk-guard-memory.mjs +57 -0
- package/package.json +3 -2
- package/src/agent-profile.mjs +115 -0
- package/src/agent-profiles.mjs +118 -0
- package/src/auto-persona.mjs +4 -1
- package/src/compress-session.mjs +13 -1
- package/src/config-core.mjs +7 -9
- package/src/decisions-journal.mjs +71 -3
- package/src/doctor.mjs +86 -4
- package/src/guard-memory.mjs +151 -0
- package/src/import-anthropic-memory.mjs +15 -1
- package/src/inject-context.mjs +34 -3
- package/src/install-agent.mjs +220 -0
- package/src/install-kiro.mjs +287 -0
- package/src/install.mjs +16 -3
- package/src/kiro-cli-agent.mjs +270 -0
- package/src/kiro-constants.mjs +19 -0
- package/src/kiro-hook-bin.mjs +105 -0
- package/src/kiro-hook-command.mjs +67 -0
- package/src/kiro-hook-dispatch.mjs +115 -0
- package/src/kiro-ide-hooks.mjs +219 -0
- package/src/kiro-permissions.mjs +175 -0
- package/src/kiro-skills.mjs +96 -0
- package/src/kiro-transcript.mjs +366 -0
- package/src/kiro-trusted-commands.mjs +130 -0
- package/src/managed-block.mjs +138 -0
- package/src/memory-write.mjs +23 -8
- package/src/mutate-agent-config.mjs +243 -0
- package/src/read-json.mjs +43 -0
- package/src/reindex.mjs +15 -2
- package/src/repair.mjs +39 -3
- package/src/result-shapes.mjs +8 -0
- package/src/review-queue.mjs +3 -0
- package/src/scratchpad.mjs +12 -2
- package/src/search.mjs +12 -5
- package/src/semantic-backend.mjs +7 -9
- package/src/settings-hooks.mjs +12 -2
- package/src/subcommands.mjs +360 -27
- package/src/tier-paths.mjs +48 -1
- package/src/weekly-curate.mjs +6 -2
- package/template/.claude/skills/memory-search/SKILL.md +14 -1
- package/template/.claude/skills/memory-write/SKILL.md +37 -1
- package/template/project/memory/INDEX.md.template +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// kiro-hook-bin.mjs — the Kiro hook ADAPTER (Task 50.J/50.L).
|
|
2
|
+
//
|
|
3
|
+
// Bridges Kiro's runCommand hook input model to the kit's inject/capture cores.
|
|
4
|
+
// Kiro's model (LIVE-VERIFIED via probe, P-CJYGTQYR — and there is NO published
|
|
5
|
+
// runCommand prior art; every real Kiro hook is askAgent, so the probe is ground
|
|
6
|
+
// truth):
|
|
7
|
+
// - EVENT → argv (`cmk hook stop` → argv[0] = 'stop')
|
|
8
|
+
// - PROJECT→ process.cwd() (Kiro runs the hook in the project root)
|
|
9
|
+
// - PROMPT → process.env.USER_PROMPT (set on promptSubmit; empty on stop)
|
|
10
|
+
// - TURN → Kiro's transcript file (.history), NOT a stdin payload
|
|
11
|
+
// - TOOL → (preToolUse) the about-to-run tool command; passed via env/argv
|
|
12
|
+
// (exact field flagged for the cut-gate-kiro live test, D-192)
|
|
13
|
+
//
|
|
14
|
+
// This is the per-agent adapter the cross-agent seam needs: Claude Code reads a
|
|
15
|
+
// stdin JSON payload; Kiro reads argv+env+cwd+transcript. The dispatcher (50.J)
|
|
16
|
+
// owns the routing + the always-exit-0 invariant; this adapter owns the
|
|
17
|
+
// input translation. `deps` is the injection seam (tests pass fakes; the bin
|
|
18
|
+
// wires the real readKiroTurn / injectContext / captureTurn).
|
|
19
|
+
//
|
|
20
|
+
// Public surface:
|
|
21
|
+
// runKiroHook({ argv, cwd, env, deps }) → { action, exitCode: 0, stdout?, stderr? }
|
|
22
|
+
|
|
23
|
+
import { dispatchKiroHook } from './kiro-hook-dispatch.mjs';
|
|
24
|
+
|
|
25
|
+
// 50.N.2 — Kiro's file-write tool names → Claude's PostToolUse-eligible names, so
|
|
26
|
+
// the shared observeEdit core (keyed on Write/Edit/MultiEdit) recognizes a Kiro
|
|
27
|
+
// file edit. `fs_write` is Kiro's create-or-edit-a-file tool (kiro.dev tool list);
|
|
28
|
+
// it covers both the create + edit cases, mapping to Claude's Write/Edit class
|
|
29
|
+
// (observeEdit treats Write/Edit/MultiEdit identically — the target name only
|
|
30
|
+
// appears in the summary line, so the map is behavior-neutral).
|
|
31
|
+
// NOTE: if a real kiro-cli turns out to have a SEPARATE append/patch tool (e.g.
|
|
32
|
+
// `fs_append`), add it here AND to the agent-config `matcher` (today scoped to the
|
|
33
|
+
// literal `fs_write`). Flagged for the cut-gate to confirm `fs_write` is the only
|
|
34
|
+
// file-mutation tool — no Kiro tool enumeration is captured in the research yet.
|
|
35
|
+
const KIRO_EDIT_TOOL_MAP = Object.freeze({
|
|
36
|
+
fs_write: 'Write',
|
|
37
|
+
fsWrite: 'Write', // camelCase spelling tolerance
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export function runKiroHook({ argv = [], cwd = process.cwd(), env = process.env, payload = {}, deps = {} } = {}) {
|
|
41
|
+
const event = argv[0];
|
|
42
|
+
const { readKiroTurn, inject, capture, capturePrompt, observe, guard } = deps;
|
|
43
|
+
|
|
44
|
+
// Wrap the kit cores so the dispatcher's generic inject/capture contract is fed
|
|
45
|
+
// Kiro's actual inputs.
|
|
46
|
+
const wrappedInject = (args) => inject({ ...args, userPrompt: env.USER_PROMPT || '' });
|
|
47
|
+
|
|
48
|
+
const wrappedCapture = (args) => {
|
|
49
|
+
// Kiro has no stdin payload — read the turn from Kiro's transcript instead,
|
|
50
|
+
// then build the {assistant_message} payload captureTurn's extractTurnText
|
|
51
|
+
// understands. A failed read must NOT crash (dispatcher catches + exits 0).
|
|
52
|
+
const turn = readKiroTurn({ projectRoot: args.projectRoot, env }) || {};
|
|
53
|
+
const payload = {
|
|
54
|
+
assistant_message: turn.assistantText || '',
|
|
55
|
+
// carry the user prompt too (capture-prompt pairing analog)
|
|
56
|
+
...(turn.userText ? { user_message: turn.userText } : {}),
|
|
57
|
+
};
|
|
58
|
+
return capture({ ...args, payload });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 50.N.1 — prompt-capture on the prompt-submit events. The prompt text comes
|
|
62
|
+
// from the stdin payload (kiro-cli `userPromptSubmit` carries `prompt`) OR env
|
|
63
|
+
// USER_PROMPT (the IDE legacy surface). capturePrompt reads `payload.prompt`,
|
|
64
|
+
// so build a payload that carries whichever is present.
|
|
65
|
+
const wrappedCapturePrompt = capturePrompt
|
|
66
|
+
? (args) => {
|
|
67
|
+
const prompt =
|
|
68
|
+
(payload && typeof payload.prompt === 'string' && payload.prompt) ||
|
|
69
|
+
env.USER_PROMPT ||
|
|
70
|
+
'';
|
|
71
|
+
return capturePrompt({ ...args, payload: { ...payload, prompt } });
|
|
72
|
+
}
|
|
73
|
+
: undefined;
|
|
74
|
+
|
|
75
|
+
// 50.N.2 — observe-edit on postToolUse. Kiro's file-write tool is `fs_write`
|
|
76
|
+
// (not Claude's Write/Edit/MultiEdit), so map the Kiro tool name to an eligible
|
|
77
|
+
// one before observeEdit's eligibility check. observeEdit's path-extractor
|
|
78
|
+
// already probes `path` (which Kiro's fs_write tool_input uses) alongside
|
|
79
|
+
// file_path/filePath. The stdin payload (postToolUse → {tool_name, tool_input,
|
|
80
|
+
// tool_response}) carries everything observeEdit needs.
|
|
81
|
+
const wrappedObserve = observe
|
|
82
|
+
? (args) => {
|
|
83
|
+
const mapped = KIRO_EDIT_TOOL_MAP[payload?.tool_name] ?? payload?.tool_name;
|
|
84
|
+
return observe({ ...args, payload: { ...payload, tool_name: mapped } });
|
|
85
|
+
}
|
|
86
|
+
: undefined;
|
|
87
|
+
|
|
88
|
+
// preToolUse guard (forward-compat path — the production Kiro install calls the
|
|
89
|
+
// cmk-guard-memory bin directly, which reads the stdin payload). If a guard dep
|
|
90
|
+
// is wired, pass the stdin payload through so it can read tool_input.command.
|
|
91
|
+
const wrappedGuard = guard ? (args) => guard({ ...args, payload }) : undefined;
|
|
92
|
+
|
|
93
|
+
return dispatchKiroHook({
|
|
94
|
+
event,
|
|
95
|
+
payload,
|
|
96
|
+
cwd,
|
|
97
|
+
deps: {
|
|
98
|
+
inject: wrappedInject,
|
|
99
|
+
capture: wrappedCapture,
|
|
100
|
+
...(wrappedCapturePrompt ? { capturePrompt: wrappedCapturePrompt } : {}),
|
|
101
|
+
...(wrappedObserve ? { observe: wrappedObserve } : {}),
|
|
102
|
+
...(wrappedGuard ? { guard: wrappedGuard } : {}),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// kiro-hook-command.mjs — the platform-correct `cmk hook <event>` command string.
|
|
2
|
+
//
|
|
3
|
+
// Shared by BOTH Kiro hook surfaces (the IDE .kiro.hook writer + the CLI
|
|
4
|
+
// agent-config writer) so the platform logic lives in ONE place (the
|
|
5
|
+
// shared-module discipline).
|
|
6
|
+
//
|
|
7
|
+
// LIVE-VERIFIED 2026-06-21 (P-PM2CD6CB): Kiro runs a hook command through WSL on
|
|
8
|
+
// Windows, and WSL has no node, so a bare `cmk hook stop` fails ("node: not
|
|
9
|
+
// found"). Forcing the Windows-native shell with `cmd.exe /c` reaches the real
|
|
10
|
+
// node+cmk (proven: `cmd.exe /c cmk --version` → 0.3.5 in the Kiro chat). On
|
|
11
|
+
// macOS/Linux there's no WSL hop, so the native `cmk` runs directly.
|
|
12
|
+
// Windows → `cmd.exe /c cmk hook <event>`
|
|
13
|
+
// macOS/Linux → `cmk hook <event>`
|
|
14
|
+
//
|
|
15
|
+
// platform-commands: ignore (the Kiro-hook command runs in KIRO's shell, not the
|
|
16
|
+
// kit's — this is the deliberate cmd.exe form; it keys on the INSTALL host's
|
|
17
|
+
// process.platform, the right signal for "which OS will run these hooks").
|
|
18
|
+
|
|
19
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
20
|
+
|
|
21
|
+
/** Build the `cmk hook <event>` command, platform-wrapped. */
|
|
22
|
+
export function kiroHookCommand(event, cmkCmd = 'cmk') {
|
|
23
|
+
const inner = `${cmkCmd} hook ${event}`;
|
|
24
|
+
return IS_WINDOWS ? `cmd.exe /c ${inner}` : inner;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the memory delete-guardrail command (D-192), platform-wrapped. Kiro's
|
|
29
|
+
* `preToolUse` delivers `{ tool_name, tool_input: { command } }` on STDIN — the
|
|
30
|
+
* SAME shape as Claude Code (verified from the real oh-my-kiro + vibekit
|
|
31
|
+
* preToolUse hooks). So the `cmk-guard-memory` bin (which reads that stdin and
|
|
32
|
+
* exits 2 to BLOCK) guards BOTH agents — no Kiro-specific adapter needed.
|
|
33
|
+
*/
|
|
34
|
+
export function kiroGuardCommand(binCmd = 'cmk-guard-memory') {
|
|
35
|
+
return IS_WINDOWS ? `cmd.exe /c ${binCmd}` : binCmd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The kit's hook commands as CLI agent-config `toolsSettings.shell.allowedCommands`
|
|
40
|
+
* REGEX patterns (D-194). Distinct from the IDE `kiroAgent.trustedCommands` shape:
|
|
41
|
+
* the CLI agent-config uses REGEX (per the Kiro agent config reference —
|
|
42
|
+
* `deniedCommands: ["git commit .*"]`), the IDE uses wildcard PREFIX. Without these,
|
|
43
|
+
* the kiro-cli default agent prompts to approve its OWN inject/capture/guard hooks.
|
|
44
|
+
* We escape the `cmd.exe` dot and anchor each to the kit's own commands only —
|
|
45
|
+
* never a blanket `.*`.
|
|
46
|
+
*/
|
|
47
|
+
export function kiroCliAllowedCommands() {
|
|
48
|
+
// Regex, START-ANCHORED (`^`) to mirror the IDE side's prefix-from-start
|
|
49
|
+
// semantics (skill-review M2 — an unanchored `cmk hook .*` could match a
|
|
50
|
+
// command that merely CONTAINS the phrase mid-string). `.` in cmd.exe is
|
|
51
|
+
// escaped; `.*` matches the hook event / guard tail / command args.
|
|
52
|
+
//
|
|
53
|
+
// `cmk remember` + `cmk search` are pre-trusted because kiro-cli uses the CLI
|
|
54
|
+
// commands (not the MCP tools) for explicit capture/recall — kiro-cli does not
|
|
55
|
+
// wire MCP tools to a custom agent's LLM (Kiro #5873), so the shell commands are
|
|
56
|
+
// the working path. Pre-trusting them means they run WITHOUT a Run/Reject prompt
|
|
57
|
+
// (the same posture as the hook commands). Scoped to the kit's own verbs only.
|
|
58
|
+
const base = IS_WINDOWS
|
|
59
|
+
? [
|
|
60
|
+
'^cmd\\.exe /c cmk hook .*',
|
|
61
|
+
'^cmd\\.exe /c cmk-guard-memory',
|
|
62
|
+
'^cmk remember .*',
|
|
63
|
+
'^cmk search .*',
|
|
64
|
+
]
|
|
65
|
+
: ['^cmk hook .*', '^cmk-guard-memory', '^cmk remember .*', '^cmk search .*'];
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// kiro-hook-dispatch.mjs — the `cmk hook <event>` Kiro dispatcher (Task 50.J).
|
|
2
|
+
//
|
|
3
|
+
// ONE entrypoint that both Kiro hook surfaces (IDE .kiro.hook + CLI agent-config)
|
|
4
|
+
// call: `cmk hook <event>` with the Kiro hook payload on stdin. It fans out by
|
|
5
|
+
// event to the kit's EXISTING inject/capture logic (injectContext / captureTurn —
|
|
6
|
+
// the same cores the Claude-Code cmk-inject-context / cmk-capture-turn bins use),
|
|
7
|
+
// so memory logic is shared cross-agent and only the per-agent payload adapter
|
|
8
|
+
// differs.
|
|
9
|
+
//
|
|
10
|
+
// Kiro lifecycle events → kit operation:
|
|
11
|
+
// agentSpawn → inject (runs once, cached whole-conversation = SessionStart)
|
|
12
|
+
// promptSubmit / → inject (per-prompt recall). The IDE .kiro.hook surface
|
|
13
|
+
// userPromptSubmit emits `promptSubmit`; the Amazon-Q/CLI Rust contract
|
|
14
|
+
// names the same trigger `userPromptSubmit`. BOTH are
|
|
15
|
+
// recognized so the dispatcher is vocabulary-agnostic
|
|
16
|
+
// across the two hook surfaces (the I-1 composition fix:
|
|
17
|
+
// the CLI agent-config currently wires only
|
|
18
|
+
// agentSpawn+stop by design — inject-once is sufficient
|
|
19
|
+
// since agentSpawn caches the whole-conversation inject —
|
|
20
|
+
// but if a future CLI agent wires the contract's
|
|
21
|
+
// userPromptSubmit trigger, it routes to inject, not no-op).
|
|
22
|
+
// stop → capture (turn-end, the deterministic capture spine)
|
|
23
|
+
// <anything else> → no-op (forward-compatible: a new Kiro event never crashes)
|
|
24
|
+
//
|
|
25
|
+
// CRITICAL INVARIANT: always exit 0. A non-zero exit from a Kiro hook can break
|
|
26
|
+
// the session (the PILOT caveat — aws-bash-hooks §6). Every error is caught,
|
|
27
|
+
// reported on stderr, and the exit code stays 0. Memory capture is best-effort;
|
|
28
|
+
// a failed capture must never take the user's Kiro session down with it.
|
|
29
|
+
//
|
|
30
|
+
// Public surface:
|
|
31
|
+
// dispatchKiroHook({ event, payload, cwd, userDir?, deps }) →
|
|
32
|
+
// { action: 'inject'|'capture'|'noop'|'error', exitCode: 0, stdout?, stderr? }
|
|
33
|
+
// `deps.inject` / `deps.capture` are REQUIRED — this is a pure router. The
|
|
34
|
+
// cmk-hook bin wires the real injectContext / captureTurn (top-level await
|
|
35
|
+
// import); tests pass fakes. Keeping the router dep-free makes it trivially
|
|
36
|
+
// testable and keeps the inject/capture cores out of the no-op event path.
|
|
37
|
+
|
|
38
|
+
// `promptSubmit` (IDE .kiro.hook) and `userPromptSubmit` (Amazon-Q Rust contract)
|
|
39
|
+
// are the SAME trigger under two surface vocabularies — both map to inject.
|
|
40
|
+
const INJECT_EVENTS = new Set(['agentSpawn', 'promptSubmit', 'userPromptSubmit']);
|
|
41
|
+
const CAPTURE_EVENTS = new Set(['stop']);
|
|
42
|
+
// 50.N.1 — the prompt-submit events that ALSO capture the prompt (the <private>
|
|
43
|
+
// -strip + transcript-append half of Claude Code's UserPromptSubmit). An explicit
|
|
44
|
+
// ALLOW-set (not "INJECT minus agentSpawn") so a future inject-only event added to
|
|
45
|
+
// INJECT_EVENTS never silently starts calling capturePrompt with no prompt.
|
|
46
|
+
const PROMPT_CAPTURE_EVENTS = new Set(['promptSubmit', 'userPromptSubmit']);
|
|
47
|
+
// preToolUse → the memory delete-guardrail (D-192). The ONE event that may exit
|
|
48
|
+
// NON-zero: a non-zero preToolUse exit BLOCKS the Kiro tool (verified — the same
|
|
49
|
+
// mechanism the always-exit-0 invariant exists for). The guard exits 2 ONLY on a
|
|
50
|
+
// deliberate block; everything else (incl. a crashed guard via the catch) exits 0.
|
|
51
|
+
const GUARD_EVENTS = new Set(['preToolUse']);
|
|
52
|
+
// 50.N.2 — postToolUse → observe (the file-edit observation leg, matching Claude
|
|
53
|
+
// Code's PostToolUse → cmk-observe-edit). Kiro's file-write tool is `fs_write`
|
|
54
|
+
// (not Write/Edit); the runHook adapter maps the Kiro tool name + reads the path.
|
|
55
|
+
const OBSERVE_EVENTS = new Set(['postToolUse']);
|
|
56
|
+
|
|
57
|
+
export function dispatchKiroHook({ event, payload = {}, cwd, userDir, deps = {} } = {}) {
|
|
58
|
+
const { inject, capture, capturePrompt, observe, guard } = deps;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (INJECT_EVENTS.has(event)) {
|
|
62
|
+
// The prompt-submit events (promptSubmit / userPromptSubmit) do BOTH inject
|
|
63
|
+
// (recall) AND capturePrompt — the <private>-strip + transcript-append half,
|
|
64
|
+
// matching Claude Code's UserPromptSubmit (cmk-capture-prompt). agentSpawn is
|
|
65
|
+
// inject-only (no prompt to capture). capturePrompt is BEST-EFFORT: a throw
|
|
66
|
+
// must never break inject or the session (50.N.1). Older installs without
|
|
67
|
+
// the dep skip it cleanly.
|
|
68
|
+
if (PROMPT_CAPTURE_EVENTS.has(event) && typeof capturePrompt === 'function') {
|
|
69
|
+
try {
|
|
70
|
+
capturePrompt({ payload, projectRoot: cwd, ...(userDir ? { userDir } : {}) });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// swallow — capture is best-effort; inject below still runs.
|
|
73
|
+
process.stderr.write(`cmk hook ${event}: capturePrompt failed: ${err?.message ?? err}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const r = inject({ cwd, ...(userDir ? { userDir } : {}) });
|
|
77
|
+
// injectContext returns the text to surface as context; print on stdout so
|
|
78
|
+
// Kiro adds it to the agent's context (the runCommand→stdout→context path).
|
|
79
|
+
const stdout = r && typeof r.text === 'string' ? r.text : '';
|
|
80
|
+
return { action: 'inject', exitCode: 0, stdout };
|
|
81
|
+
}
|
|
82
|
+
if (OBSERVE_EVENTS.has(event)) {
|
|
83
|
+
// postToolUse → observe (the file-edit observation leg). Best-effort like
|
|
84
|
+
// capture; a throw must never wedge the tool/session. Older installs (no
|
|
85
|
+
// observe dep) skip cleanly → noop. The runHook adapter maps Kiro's tool
|
|
86
|
+
// name (fs_write → Write) before observeEdit's eligibility check.
|
|
87
|
+
if (typeof observe === 'function') {
|
|
88
|
+
observe({ payload, projectRoot: cwd, ...(userDir ? { userDir } : {}) });
|
|
89
|
+
return { action: 'observe', exitCode: 0 };
|
|
90
|
+
}
|
|
91
|
+
return { action: 'noop', exitCode: 0 };
|
|
92
|
+
}
|
|
93
|
+
if (CAPTURE_EVENTS.has(event)) {
|
|
94
|
+
capture({ payload, projectRoot: cwd, ...(userDir ? { userDir } : {}) });
|
|
95
|
+
return { action: 'capture', exitCode: 0 };
|
|
96
|
+
}
|
|
97
|
+
if (GUARD_EVENTS.has(event)) {
|
|
98
|
+
// guard() inspects the about-to-run tool command (from the Kiro payload)
|
|
99
|
+
// and returns { block, reason? }. A block → exit 2 (BLOCK the tool) with
|
|
100
|
+
// the reason on stderr; otherwise exit 0 (allow). If guard is not wired
|
|
101
|
+
// (older install), default to allow — fail-open, never block by accident.
|
|
102
|
+
const v = guard ? guard({ payload, cwd }) : { block: false };
|
|
103
|
+
if (v && v.block) {
|
|
104
|
+
return { action: 'blocked', exitCode: 2, stderr: v.reason ?? 'blocked by the memory delete-guardrail' };
|
|
105
|
+
}
|
|
106
|
+
return { action: 'allow', exitCode: 0 };
|
|
107
|
+
}
|
|
108
|
+
// unknown / not-yet-handled event — no-op, forward-compatible.
|
|
109
|
+
return { action: 'noop', exitCode: 0 };
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// NEVER propagate — exit 0 with the error on stderr so the Kiro session lives.
|
|
112
|
+
// (A CRASHED guard fails OPEN here: a broken guardrail must not wedge the tool.)
|
|
113
|
+
return { action: 'error', exitCode: 0, stderr: `cmk hook ${event}: ${err?.message ?? err}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// kiro-ide-hooks.mjs — write Kiro IDE hook files (Task 50.K + 50.N.3 v1 migration).
|
|
2
|
+
//
|
|
3
|
+
// TWO formats, DUAL-EMITTED for back-compat (D-203):
|
|
4
|
+
//
|
|
5
|
+
// LEGACY (Kiro 0.x) — individual `.kiro/hooks/<name>.kiro.hook` files:
|
|
6
|
+
// { "version": "1.0.0", "enabled", "name", "description",
|
|
7
|
+
// "when": { "type": "agentStop" },
|
|
8
|
+
// "then": { "type": "runCommand", "command", "timeout" } }
|
|
9
|
+
// Verified from a real GUI-created 0.x hook (P-WJRUQVSW).
|
|
10
|
+
//
|
|
11
|
+
// v1 (Kiro IDE 1.0+) — clean per-hook `.kiro/hooks/<name>.json` files that
|
|
12
|
+
// REPLACE the .kiro.hook format (which 1.0 no longer loads — D-203). Schema
|
|
13
|
+
// GROUND-TRUTH-VERIFIED against Kiro IDE 1.0's OWN migration output (D-203d —
|
|
14
|
+
// it migrated our `cmk-capture.kiro.hook` → `cmk-capture.json`):
|
|
15
|
+
// { "version": "v1", "hooks": [ { "name", "description", "trigger",
|
|
16
|
+
// "matcher"?, "action": { "type": "command", "command" }, "timeout",
|
|
17
|
+
// "enabled" } ] } — PascalCase triggers; `action.type:'command'` is the
|
|
18
|
+
// deterministic-shell action (no LLM). We write `cmk-capture.json`/
|
|
19
|
+
// `cmk-inject.json`/`cmk-guard.json`/`cmk-observe.json` — Kiro's exact filename
|
|
20
|
+
// convention, one hook per file (full isolation).
|
|
21
|
+
//
|
|
22
|
+
// v1 lets the IDE do the FULL Claude-Code hook set (one clean .json file PER
|
|
23
|
+
// hook): inject (UserPromptSubmit) + capture (Stop) + delete-guard (PreToolUse —
|
|
24
|
+
// CAN BLOCK on non-zero exit) + observe-edit (PostToolUse).
|
|
25
|
+
//
|
|
26
|
+
// We emit BOTH so a 0.x user keeps the legacy hooks and a 1.0 user gets v1 (a
|
|
27
|
+
// 1.0 IDE runs the .json + shows the .kiro.hook as inert "legacy"; a 0.x IDE
|
|
28
|
+
// runs the .kiro.hook + ignores the .json — no double-fire, verified D-203d).
|
|
29
|
+
//
|
|
30
|
+
// ⚠️ v1 behaviors LIVE-PROBED at the cut-gate (D-203 — `Stop` + the schema are
|
|
31
|
+
// CONFIRMED by Kiro 1.0's own migration D-203d; the rest flagged, NOT asserted):
|
|
32
|
+
// (1) auto-load of an installer-written json; (2) what a PreToolUse command
|
|
33
|
+
// receives (the path to inspect); (3) exit-code 1-vs-2 to
|
|
34
|
+
// block; (4) the matcher tool-name tokens; (5) the real session-end trigger
|
|
35
|
+
// name (v1's type list has SessionStart but no obvious Stop — we use `Stop`,
|
|
36
|
+
// the dispatcher's capture key, pending the probe).
|
|
37
|
+
//
|
|
38
|
+
// Public surface:
|
|
39
|
+
// installKiroIdeHooks({ projectRoot, command? }) → { action, changed, hooks }
|
|
40
|
+
// uninstallKiroIdeHooks({ projectRoot }) → { action, changed, hooks }
|
|
41
|
+
|
|
42
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
import { kiroHookCommand, kiroGuardCommand } from './kiro-hook-command.mjs';
|
|
45
|
+
|
|
46
|
+
const CMK = 'cmk';
|
|
47
|
+
const MANAGED = 'Managed by `cmk install` — do not hand-edit.';
|
|
48
|
+
|
|
49
|
+
// The platform-correct `cmk hook <event>` command (cmd.exe /c on Windows where
|
|
50
|
+
// Kiro routes hooks through WSL) is shared from kiro-hook-command.mjs.
|
|
51
|
+
const hookCommand = kiroHookCommand;
|
|
52
|
+
|
|
53
|
+
// One .kiro.hook spec. We intentionally do NOT put a `then` key on any JS object
|
|
54
|
+
// here — Kiro's schema needs a top-level `then` (its "action" leg), but a JS
|
|
55
|
+
// object with a `then` property is a "thenable" footgun (a static analyzer flags
|
|
56
|
+
// it, and an accidental thenable can hijack a Promise chain). So we model the
|
|
57
|
+
// action under a neutral `action` key and rename it to `then` ONLY at
|
|
58
|
+
// serialization (serializeHook), where it's pure JSON data, never a live object.
|
|
59
|
+
function hookSpecs(cmd) {
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
file: 'cmk-capture.kiro.hook',
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
enabled: true,
|
|
65
|
+
name: 'claude-memory-kit: capture',
|
|
66
|
+
description: 'Capture durable memory at the end of each turn (claude-memory-kit). Managed by `cmk install` — do not hand-edit.',
|
|
67
|
+
when: { type: 'agentStop' },
|
|
68
|
+
action: { type: 'runCommand', command: hookCommand('stop', cmd), timeout: 60 },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
file: 'cmk-inject.kiro.hook',
|
|
72
|
+
version: '1.0.0',
|
|
73
|
+
enabled: true,
|
|
74
|
+
name: 'claude-memory-kit: recall',
|
|
75
|
+
description: 'Inject recalled memory on each prompt (claude-memory-kit). Managed by `cmk install` — do not hand-edit.',
|
|
76
|
+
when: { type: 'promptSubmit' },
|
|
77
|
+
action: { type: 'runCommand', command: hookCommand('promptSubmit', cmd), timeout: 30 },
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Serialize a spec to the Kiro .kiro.hook JSON, mapping our internal `action`
|
|
83
|
+
// key to Kiro's required `then` field. We build the object with a placeholder
|
|
84
|
+
// key, then rename it in the JSON STRING — so no JS object literal ever carries
|
|
85
|
+
// a `then` property (the thenable footgun the static analyzer guards against).
|
|
86
|
+
const THEN_PLACEHOLDER = '__kiro_then__';
|
|
87
|
+
function serializeHook(spec) {
|
|
88
|
+
const { file, action, ...rest } = spec;
|
|
89
|
+
const obj = { ...rest, [THEN_PLACEHOLDER]: action };
|
|
90
|
+
return `${JSON.stringify(obj, null, 2).replace(`"${THEN_PLACEHOLDER}"`, '"then"')}\n`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The v1 hooks — ONE clean `<name>.json` file PER hook (Kiro IDE 1.0's own
|
|
94
|
+
// convention — D-203d). One hook per file → every hook independently isolated (a
|
|
95
|
+
// bad trigger can't dark the others). The full Claude-Code parity set:
|
|
96
|
+
// cmk-capture.json → Stop → capture (turn-end)
|
|
97
|
+
// cmk-inject.json → UserPromptSubmit → inject (recall)
|
|
98
|
+
// cmk-guard.json → PreToolUse → delete-guard (CAN BLOCK on non-zero exit)
|
|
99
|
+
// cmk-observe.json → PostToolUse → observe-edit (record large edits)
|
|
100
|
+
// Schema GROUND-TRUTH-verified against Kiro IDE 1.0's own migration output (D-203d):
|
|
101
|
+
// {version:'v1', hooks:[{name, description, trigger, matcher?, action:{type:
|
|
102
|
+
// 'command', command}, timeout, enabled}]}.
|
|
103
|
+
//
|
|
104
|
+
// SINGLE SOURCE: filename → hook-spec builder. V1_FILES is DERIVED from these keys
|
|
105
|
+
// (no array/switch to keep in sync). `Stop` (capture) is CONFIRMED by Kiro's
|
|
106
|
+
// migration; the other three triggers + the `fs_write` matcher are live-probed
|
|
107
|
+
// (cut-gate KHv1-*).
|
|
108
|
+
function v1HookFile(hook) {
|
|
109
|
+
return { version: 'v1', hooks: [hook] };
|
|
110
|
+
}
|
|
111
|
+
const V1_HOOK_BUILDERS = Object.freeze({
|
|
112
|
+
'cmk-capture.json': (cmd) =>
|
|
113
|
+
v1HookFile({
|
|
114
|
+
name: 'claude-memory-kit: capture',
|
|
115
|
+
description: `Capture durable memory at the end of each turn (claude-memory-kit). ${MANAGED}`,
|
|
116
|
+
trigger: 'Stop', // CONFIRMED by Kiro IDE 1.0's own migration (D-203d)
|
|
117
|
+
action: { type: 'command', command: hookCommand('stop', cmd) },
|
|
118
|
+
timeout: 60,
|
|
119
|
+
enabled: true,
|
|
120
|
+
}),
|
|
121
|
+
'cmk-inject.json': (cmd) =>
|
|
122
|
+
v1HookFile({
|
|
123
|
+
name: 'claude-memory-kit: recall',
|
|
124
|
+
description: `Inject recalled memory on each prompt (claude-memory-kit). ${MANAGED}`,
|
|
125
|
+
trigger: 'UserPromptSubmit',
|
|
126
|
+
action: { type: 'command', command: hookCommand('userPromptSubmit', cmd) },
|
|
127
|
+
timeout: 30,
|
|
128
|
+
enabled: true,
|
|
129
|
+
}),
|
|
130
|
+
'cmk-guard.json': () =>
|
|
131
|
+
v1HookFile({
|
|
132
|
+
name: 'claude-memory-kit: delete-guard',
|
|
133
|
+
description: `Block a destructive command aimed at a memory path (claude-memory-kit). ${MANAGED}`,
|
|
134
|
+
trigger: 'PreToolUse', // v1 PreToolUse can BLOCK (non-zero exit) — supersedes Task 165(b)
|
|
135
|
+
// `matcher` for PreToolUse is a TOOL-NAME glob (D-203 item 4 — exact tokens
|
|
136
|
+
// live-unverified). `'*'` is conservative; the guard itself filters to memory
|
|
137
|
+
// deletes, so an over-broad matcher costs nothing (allows all non-deletes).
|
|
138
|
+
matcher: '*',
|
|
139
|
+
action: { type: 'command', command: kiroGuardCommand() },
|
|
140
|
+
timeout: 5,
|
|
141
|
+
enabled: true,
|
|
142
|
+
}),
|
|
143
|
+
'cmk-observe.json': (cmd) =>
|
|
144
|
+
v1HookFile({
|
|
145
|
+
name: 'claude-memory-kit: observe-edit',
|
|
146
|
+
description: `Record large file edits (claude-memory-kit). ${MANAGED}`,
|
|
147
|
+
// PostToolUse (NOT PostFileSave) — observe-edit needs a TOOL-USE payload
|
|
148
|
+
// ({tool_name:'fs_write', …}) that observeEdit reads; a file-SAVE event
|
|
149
|
+
// carries no tool_name → silent noop (skill-review I1). Sibling of the
|
|
150
|
+
// kiro-cli postToolUse leg (50.N.2), same payload shape.
|
|
151
|
+
trigger: 'PostToolUse',
|
|
152
|
+
matcher: 'fs_write', // tool-name glob (like PreToolUse), scoped to file-writes
|
|
153
|
+
action: { type: 'command', command: hookCommand('postToolUse', cmd) },
|
|
154
|
+
timeout: 30,
|
|
155
|
+
enabled: true,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// V1_FILES is DERIVED from the builder keys — single source of truth, no array
|
|
160
|
+
// to keep in sync (an unmapped filename is structurally impossible).
|
|
161
|
+
const V1_FILES = Object.freeze(Object.keys(V1_HOOK_BUILDERS));
|
|
162
|
+
|
|
163
|
+
function serializeV1(file, cmd) {
|
|
164
|
+
return `${JSON.stringify(V1_HOOK_BUILDERS[file](cmd), null, 2)}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function installKiroIdeHooks({ projectRoot, command = CMK } = {}) {
|
|
168
|
+
if (!projectRoot) throw new Error('installKiroIdeHooks: projectRoot is required');
|
|
169
|
+
const hooksDir = join(projectRoot, '.kiro', 'hooks');
|
|
170
|
+
|
|
171
|
+
let changed = false;
|
|
172
|
+
const written = [];
|
|
173
|
+
// 1. Legacy .kiro.hook files (Kiro 0.x back-compat).
|
|
174
|
+
for (const spec of hookSpecs(command)) {
|
|
175
|
+
const path = join(hooksDir, spec.file);
|
|
176
|
+
const serialized = serializeHook(spec);
|
|
177
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
178
|
+
if (existing !== serialized) {
|
|
179
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
180
|
+
writeFileSync(path, serialized, 'utf8');
|
|
181
|
+
changed = true;
|
|
182
|
+
}
|
|
183
|
+
written.push(spec.file);
|
|
184
|
+
}
|
|
185
|
+
// 2. The v1 files (Kiro IDE 1.0+ — D-203/D-203d). A 0.x IDE ignores them; a 1.0
|
|
186
|
+
// IDE ignores the stale .kiro.hook files above (shows them "legacy", inert —
|
|
187
|
+
// no double-fire, verified D-203d). ONE clean `<name>.json` PER hook (Kiro's
|
|
188
|
+
// own migration convention), so every hook is isolated.
|
|
189
|
+
for (const file of V1_FILES) {
|
|
190
|
+
const path = join(hooksDir, file);
|
|
191
|
+
const serialized = serializeV1(file, command);
|
|
192
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
193
|
+
if (existing !== serialized) {
|
|
194
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
195
|
+
writeFileSync(path, serialized, 'utf8');
|
|
196
|
+
changed = true;
|
|
197
|
+
}
|
|
198
|
+
written.push(file);
|
|
199
|
+
}
|
|
200
|
+
return { action: 'installed', changed, hooks: written };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function uninstallKiroIdeHooks({ projectRoot, command = CMK } = {}) {
|
|
204
|
+
if (!projectRoot) throw new Error('uninstallKiroIdeHooks: projectRoot is required');
|
|
205
|
+
const hooksDir = join(projectRoot, '.kiro', 'hooks');
|
|
206
|
+
let changed = false;
|
|
207
|
+
const removed = [];
|
|
208
|
+
// legacy .kiro.hook files + all the v1 files — remove all.
|
|
209
|
+
const ourFiles = [...hookSpecs(command).map((s) => s.file), ...V1_FILES];
|
|
210
|
+
for (const file of ourFiles) {
|
|
211
|
+
const path = join(hooksDir, file);
|
|
212
|
+
if (existsSync(path)) {
|
|
213
|
+
rmSync(path, { force: true });
|
|
214
|
+
changed = true;
|
|
215
|
+
removed.push(file);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { action: 'uninstalled', changed, hooks: removed };
|
|
219
|
+
}
|