@lh8ppl/claude-memory-kit 0.3.5 → 0.4.1

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.
Files changed (53) hide show
  1. package/README.md +137 -50
  2. package/bin/cmk-approve-permission.mjs +62 -0
  3. package/bin/cmk-daily-distill.mjs +14 -0
  4. package/bin/cmk-guard-memory.mjs +57 -0
  5. package/bin/cmk-inject-context.mjs +12 -0
  6. package/bin/cmk-weekly-curate.mjs +12 -0
  7. package/package.json +4 -2
  8. package/src/agent-profile.mjs +115 -0
  9. package/src/agent-profiles.mjs +118 -0
  10. package/src/approve-permission.mjs +92 -0
  11. package/src/auto-extract.mjs +17 -10
  12. package/src/auto-persona.mjs +11 -4
  13. package/src/compaction-state.mjs +204 -0
  14. package/src/compress-session.mjs +13 -1
  15. package/src/config-core.mjs +7 -9
  16. package/src/decisions-journal.mjs +71 -3
  17. package/src/doctor.mjs +128 -5
  18. package/src/guard-memory.mjs +151 -0
  19. package/src/import-anthropic-memory.mjs +15 -1
  20. package/src/inject-context.mjs +42 -18
  21. package/src/install-agent.mjs +220 -0
  22. package/src/install-kiro.mjs +287 -0
  23. package/src/install.mjs +53 -7
  24. package/src/kiro-cli-agent.mjs +270 -0
  25. package/src/kiro-constants.mjs +19 -0
  26. package/src/kiro-hook-bin.mjs +105 -0
  27. package/src/kiro-hook-command.mjs +67 -0
  28. package/src/kiro-hook-dispatch.mjs +115 -0
  29. package/src/kiro-ide-hooks.mjs +219 -0
  30. package/src/kiro-permissions.mjs +175 -0
  31. package/src/kiro-skills.mjs +96 -0
  32. package/src/kiro-transcript.mjs +366 -0
  33. package/src/kiro-trusted-commands.mjs +130 -0
  34. package/src/lazy-compress.mjs +43 -110
  35. package/src/managed-block.mjs +138 -0
  36. package/src/memory-write.mjs +23 -8
  37. package/src/mutate-agent-config.mjs +243 -0
  38. package/src/read-json.mjs +43 -0
  39. package/src/register-crons.mjs +31 -0
  40. package/src/reindex.mjs +15 -2
  41. package/src/repair.mjs +39 -3
  42. package/src/result-shapes.mjs +8 -0
  43. package/src/review-queue.mjs +3 -0
  44. package/src/scratchpad.mjs +12 -2
  45. package/src/search.mjs +12 -5
  46. package/src/semantic-backend.mjs +7 -9
  47. package/src/settings-hooks.mjs +70 -3
  48. package/src/subcommands.mjs +360 -27
  49. package/src/tier-paths.mjs +82 -1
  50. package/src/weekly-curate.mjs +6 -2
  51. package/template/.claude/skills/memory-search/SKILL.md +14 -1
  52. package/template/.claude/skills/memory-write/SKILL.md +37 -1
  53. package/template/project/memory/INDEX.md.template +1 -1
@@ -0,0 +1,270 @@
1
+ // kiro-cli-agent.mjs — the Kiro CLI agent-config + default-agent registration (Task 50.L / D-198).
2
+ //
3
+ // kiro-cli (= Amazon Q Developer CLI) hooks live in an agent-config JSON. Its
4
+ // hooks auto-fire ONLY for the resolved-ACTIVE agent, so for automatic memory
5
+ // with no `--agent` flag, cmk must be the DEFAULT agent. TWO files, verified
6
+ // against a real kiro-cli 2.8.1 (`kiro-cli agent list` + `agent validate`):
7
+ //
8
+ // 1. ~/.kiro/agents/cmk.json — the agent config (hooks/mcp/prompt/…)
9
+ // 2. ~/.kiro/settings/cli.json — {"chat.defaultAgent":"cmk"} (the
10
+ // LOAD-BEARING registration; without it
11
+ // the built-in `kiro_default` runs and
12
+ // NO kit hooks fire)
13
+ //
14
+ // ── D-198 (the cut-gate-kiro root cause) ──────────────────────────────────────
15
+ // The original impl wrote `~/.aws/amazonq/cli-agents/q_cli_default.json`. That is
16
+ // the WRONG location for kiro-cli 2.8.1: `kiro-cli agent list` resolves agents
17
+ // from `~/.kiro/agents/` (global) + `<project>/.kiro/agents/` (workspace), and
18
+ // the default is the built-in `kiro_default`. Our `~/.aws/...` file was NEVER
19
+ // loaded — so the instrumented cut-gate probe showed NEITHER agentSpawn NOR
20
+ // preToolUse firing, and KG-guard let a memory delete through every time. Our own
21
+ // research (2026-06-20-kiro-automatic-memory-deep-research §3, "the load-bearing
22
+ // step the kit is missing") had `~/.kiro/agents/cmk.json` + `~/.kiro/settings/
23
+ // cli.json` correct; the impl dropped it. This module follows the research +
24
+ // the live-verified contract. The matcher fix (D-197, `'*'`) was correct but
25
+ // secondary — a right fix to a file kiro-cli never read.
26
+ //
27
+ // Also: kiro-cli's `agent validate` is STRICT — it rejects unknown top-level
28
+ // fields. The old `managedBy` marker FAILS validation (`unknown field
29
+ // managedBy`). Ownership is now tracked structurally WITHOUT an invalid field:
30
+ // the agent file lives at our well-known path (`agents/cmk.json`) and the
31
+ // settings pointer names `cmk`; uninstall keys on that, plus a marker carried in
32
+ // a VALID field (the `description`, which validate accepts) as a belt.
33
+ //
34
+ // Public surface:
35
+ // installKiroCliAgent({ kiroDir? }) → { action, defaultAgent, changed, path }
36
+ // uninstallKiroCliAgent({ kiroDir? }) → { action, changed }
37
+ // hasOurCliAgent({ kiroDir? }) → boolean
38
+ // (kiroDir overrides the ~/.kiro base; defaults to $MEMORY_KIT_KIRO_DIR or ~/.kiro.
39
+ // $MEMORY_KIT_AWS_DIR is still honored as a back-compat alias for the base.)
40
+
41
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
42
+ import { homedir } from 'node:os';
43
+ import { join } from 'node:path';
44
+ import { kiroHookCommand, kiroGuardCommand, kiroCliAllowedCommands } from './kiro-hook-command.mjs';
45
+ import { parseJsonFile } from './read-json.mjs';
46
+
47
+ // The kit's agent name. NOT `q_cli_default` — that name doesn't exist in kiro-cli
48
+ // (the built-in default is `kiro_default`). We register `cmk` and point
49
+ // chat.defaultAgent at it.
50
+ const AGENT_NAME = 'cmk';
51
+
52
+ // A marker carried in the `description` (a VALID schema field — unlike the old
53
+ // top-level `managedBy`, which kiro-cli's strict `agent validate` rejects). Used
54
+ // as a belt on top of the well-known path for ownership detection on uninstall.
55
+ const MANAGED_MARKER = '[claude-memory-kit]';
56
+
57
+ // The kiro config root is `~/.kiro/`. $MEMORY_KIT_KIRO_DIR (or the back-compat
58
+ // $MEMORY_KIT_AWS_DIR) overrides the BASE — REQUIRED in tests so the user-tier
59
+ // write lands in a sandbox, never the real ~/.kiro. Production passes nothing.
60
+ function kiroRoot(kiroDir) {
61
+ const base = kiroDir || process.env.MEMORY_KIT_KIRO_DIR || process.env.MEMORY_KIT_AWS_DIR;
62
+ return base ? base : join(homedir(), '.kiro');
63
+ }
64
+
65
+ function agentsDirOf(kiroDir) {
66
+ return join(kiroRoot(kiroDir), 'agents');
67
+ }
68
+ function agentPathOf(kiroDir) {
69
+ return join(agentsDirOf(kiroDir), `${AGENT_NAME}.json`);
70
+ }
71
+ function cliSettingsPathOf(kiroDir) {
72
+ return join(kiroRoot(kiroDir), 'settings', 'cli.json');
73
+ }
74
+
75
+ // Build the agent-config in the live-verified kiro-cli schema. ONLY valid
76
+ // top-level fields (kiro-cli `agent validate` is strict): name, description,
77
+ // prompt, mcpServers, tools, allowedTools, resources, hooks, toolsSettings,
78
+ // includeMcpJson, useLegacyMcpJson, model. NO `managedBy` (rejected).
79
+ function buildAgentConfig() {
80
+ const cfg = {
81
+ name: AGENT_NAME,
82
+ description: `claude-memory-kit — automatic per-session memory (inject + capture). ${MANAGED_MARKER}`,
83
+ // ★ `tools` is the agent's CAPABILITY SET — what it CAN use. WITHOUT it, a
84
+ // custom agent has NO tools: it cannot run shell commands at all, so the model
85
+ // "calls" cmk remember but nothing executes (the cut-gate-kiro-cli silent
86
+ // failure — kiro.dev/docs/cli/custom-agents/configuration-reference: "the
87
+ // tools field lists all tools the agent can potentially use"). `'*'` = all
88
+ // built-in tools (incl. shell, so `cmk remember`/`cmk search` actually run) +
89
+ // any MCP tools. This was THE missing piece behind the explicit-memory failure.
90
+ //
91
+ // ★ Do NOT "tighten" this to a scoped list. `tools` is the CAPABILITY set, NOT
92
+ // the security boundary — auto-execution is gated separately by `allowedTools`
93
+ // (absent) + `toolsSettings.shell.allowedCommands` (scoped to `^cmk …`) + the
94
+ // `preToolUse` delete-guardrail. With `'*'` but no `allowedTools`, every tool
95
+ // call EXCEPT the pre-trusted `^cmk` shell commands still hits kiro-cli's native
96
+ // Run/Reject/Trust prompt — so `'*'` widens capability, not silent execution.
97
+ // A hardcoded scoped list (e.g. `execute_bash`) would also silently drop the
98
+ // shell tool across the V2→V3 rename (`execute_bash`→`execute_command`),
99
+ // re-breaking this version-dependently. The approval gate is the boundary.
100
+ tools: ['*'],
101
+ // INLINE prompt — NOT `file://AGENTS.md`. kiro-cli resolves a config's
102
+ // `prompt`/`resources` file:// paths RELATIVE TO THE AGENT FILE'S DIR
103
+ // (~/.kiro/agents/), per kiro.dev/docs/cli/custom-agents/configuration-
104
+ // reference — so `file://AGENTS.md` would point at ~/.kiro/agents/AGENTS.md,
105
+ // NOT the project root (D-198). AGENTS.md is AUTO-INCLUDED by kiro-cli from
106
+ // the workspace root with no ref needed (it's "always included"), so the
107
+ // project instruction loads regardless; this inline prompt is the
108
+ // belt-and-suspenders recall/persist directive that travels WITH the agent.
109
+ // ★ kiro-cli memory = HOOKS (automatic) + CLI commands (explicit), NOT MCP.
110
+ // WHY no MCP here (the cut-gate-kiro-cli findings + Kiro issues #5873/#5662):
111
+ // kiro-cli does NOT wire MCP tools to a CUSTOM agent's LLM (only the built-in
112
+ // kiro_default gets them), so the MCP memory tools silently no-op. Loading the
113
+ // MCP server in the kiro-cli agent gains NOTHING (dead tools) and COSTS a
114
+ // visible `cmd.exe /C cmk mcp serve` console window every session on Windows
115
+ // (kiro's MCP launcher — not suppressible from the kit). So `includeMcpJson:
116
+ // false` keeps the kiro-cli agent OFF MCP entirely → no dead tools, no popup.
117
+ // (The Kiro IDE keeps its OWN MCP via `.kiro/settings/mcp.json`, where MCP
118
+ // WORKS — the IDE does not read this agent config, so it's unaffected.)
119
+ //
120
+ // The working memory paths in kiro-cli:
121
+ // • AUTOMATIC — the agentSpawn (inject) + stop (capture) hooks below run
122
+ // `cmk hook`, which kiro feeds the project cwd via the hook stdin payload,
123
+ // so they capture/recall correctly with no model action.
124
+ // • EXPLICIT recall — `cmk search` (a pre-trusted shell command).
125
+ prompt:
126
+ 'You have claude-memory-kit memory for this project, captured automatically ' +
127
+ 'each turn by the kit\'s hooks. Use the kit\'s SHELL COMMANDS for explicit ' +
128
+ 'recall/save. ### CRITICAL command form — do NOT prefix a kit command with ' +
129
+ '`cd`. A `cd … && cmk …` prefix breaks kiro-cli\'s command allowlist (the ' +
130
+ 'command then needs manual approval and may be skipped). Instead pass the ' +
131
+ 'project root with `--project` and run the bare command. To RECALL: `cmk ' +
132
+ 'search "<topic>" --project "<absolute project path>"` before answering (do ' +
133
+ 'not re-derive what memory records). To SAVE: `cmk remember "<the fact>" ' +
134
+ '--project "<absolute project path>"` (add `--why "..." --how "..." --type ' +
135
+ 'user|feedback|project` for a rich preference). The absolute project path is ' +
136
+ 'this workspace\'s root. Treat AGENTS.md as authoritative project instruction.',
137
+ // includeMcpJson:false — the kiro-cli agent does NOT load any MCP server (see
138
+ // the rationale above: dead tools + a popup, no benefit). The IDE's MCP is
139
+ // wired separately in `.kiro/settings/mcp.json` and is untouched by this.
140
+ includeMcpJson: false,
141
+ // NO `allowedTools` — there are no MCP tools to pre-approve (includeMcpJson is
142
+ // false). The kit's shell commands are trusted via toolsSettings below.
143
+ // NO `resources` file:// refs: a project-relative path can't resolve from the
144
+ // GLOBAL agent dir (~/.kiro/agents/) — D-198. AGENTS.md auto-loads anyway.
145
+ // Pre-trust the kit's OWN hook + CLI commands (no per-command approval) — D-194.
146
+ toolsSettings: { shell: { allowedCommands: kiroCliAllowedCommands() } },
147
+ };
148
+ // hooks: object keyed by trigger → array of {command, timeout_ms}. agentSpawn
149
+ // (inject) + userPromptSubmit (inject + prompt-capture) + stop (capture) +
150
+ // preToolUse (the delete-guardrail). timeout_ms (NOT `timeout`). preToolUse
151
+ // `matcher: '*'` (all tools) — kiro-cli matchers are literal strings, not regex
152
+ // (D-197); the bin exits 2 to BLOCK. userPromptSubmit (50.N.1) routes to BOTH
153
+ // inject AND capturePrompt (the <private>-strip + transcript-append half of
154
+ // Claude Code's UserPromptSubmit); kiro-cli's userPromptSubmit stdin carries
155
+ // `prompt`, which capturePrompt reads.
156
+ // postToolUse (50.N.2) → observe-edit, scoped to the file-write tool with
157
+ // `matcher: 'fs_write'` (kiro-cli matchers are literal tool names — the runHook
158
+ // adapter maps fs_write → Write for the shared observeEdit core). Only fires on
159
+ // a file write, so it's cheap.
160
+ cfg.hooks = {
161
+ agentSpawn: [{ command: kiroHookCommand('agentSpawn'), timeout_ms: 10000 }],
162
+ userPromptSubmit: [{ command: kiroHookCommand('userPromptSubmit'), timeout_ms: 10000 }],
163
+ postToolUse: [{ command: kiroHookCommand('postToolUse'), timeout_ms: 10000, matcher: 'fs_write' }],
164
+ stop: [{ command: kiroHookCommand('stop'), timeout_ms: 30000 }],
165
+ preToolUse: [{ command: kiroGuardCommand(), timeout_ms: 5000, matcher: '*' }],
166
+ };
167
+ return cfg;
168
+ }
169
+
170
+ export function installKiroCliAgent({ kiroDir, awsDir } = {}) {
171
+ kiroDir = kiroDir ?? awsDir; // `awsDir` accepted as a back-compat alias for the sandbox base
172
+ const agentsDir = agentsDirOf(kiroDir);
173
+ const agentPath = agentPathOf(kiroDir);
174
+ const settingsPath = cliSettingsPathOf(kiroDir);
175
+
176
+ mkdirSync(agentsDir, { recursive: true });
177
+
178
+ // 1. write the agent config
179
+ const changedAgent = writeIfChanged(agentPath, `${JSON.stringify(buildAgentConfig(), null, 2)}\n`);
180
+
181
+ // 2. register chat.defaultAgent — the LOAD-BEARING step. GUARDED: never clobber
182
+ // a user's existing default that ISN'T ours. If they already point at a
183
+ // different agent, we install `cmk` but leave their default alone (they run
184
+ // `kiro-cli --agent cmk` or set it themselves) and report skipped-existing.
185
+ const existingDefault = readDefaultAgentSetting(settingsPath);
186
+ const userHasForeignDefault = existingDefault != null && existingDefault !== AGENT_NAME;
187
+
188
+ let defaultAgent;
189
+ let changedSettings = false;
190
+ if (userHasForeignDefault) {
191
+ defaultAgent = 'skipped-existing';
192
+ } else {
193
+ changedSettings = mergeDefaultAgentSetting(settingsPath, AGENT_NAME);
194
+ defaultAgent = 'set';
195
+ }
196
+
197
+ return {
198
+ action: 'installed',
199
+ defaultAgent,
200
+ changed: changedAgent || changedSettings,
201
+ path: agentPath,
202
+ };
203
+ }
204
+
205
+ // Write only if the serialized content differs (idempotent re-install).
206
+ function writeIfChanged(path, serialized) {
207
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
208
+ if (existing === serialized) return false;
209
+ writeFileSync(path, serialized, 'utf8');
210
+ return true;
211
+ }
212
+
213
+ // Merge {"chat.defaultAgent": name} into ~/.kiro/settings/cli.json, BYTE-
214
+ // PRESERVING every other key (managed-merge discipline — same as the Claude
215
+ // settings path). Returns true if the file changed.
216
+ function mergeDefaultAgentSetting(settingsPath, name) {
217
+ mkdirSync(join(settingsPath, '..'), { recursive: true });
218
+ const current = parseJsonFile(settingsPath, { fallback: null }) ?? {};
219
+ if (current['chat.defaultAgent'] === name) return false;
220
+ current['chat.defaultAgent'] = name;
221
+ writeFileSync(settingsPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8');
222
+ return true;
223
+ }
224
+
225
+ export function uninstallKiroCliAgent({ kiroDir, awsDir } = {}) {
226
+ kiroDir = kiroDir ?? awsDir;
227
+ const agentPath = agentPathOf(kiroDir);
228
+ const settingsPath = cliSettingsPathOf(kiroDir);
229
+ let changed = false;
230
+
231
+ // remove OUR agent file only (well-known path + our marker)
232
+ if (existsSync(agentPath) && isOurAgent(agentPath)) {
233
+ rmSync(agentPath, { force: true });
234
+ changed = true;
235
+ }
236
+
237
+ // un-register the default ONLY if it points at us (byte-preserve other keys)
238
+ const current = parseJsonFile(settingsPath, { fallback: null });
239
+ if (current != null && current['chat.defaultAgent'] === AGENT_NAME) {
240
+ delete current['chat.defaultAgent'];
241
+ writeFileSync(settingsPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8');
242
+ changed = true;
243
+ }
244
+
245
+ return { action: 'uninstalled', changed };
246
+ }
247
+
248
+ // Does a cmk-owned kiro-cli agent exist? (the well-known ~/.kiro/agents/cmk.json
249
+ // with our marker). Used by `cmk doctor` HC-1 — a kiro-cli user's capture/inject
250
+ // fires via THIS surface (D-186).
251
+ export function hasOurCliAgent({ kiroDir, awsDir } = {}) {
252
+ kiroDir = kiroDir ?? awsDir;
253
+ const agentPath = agentPathOf(kiroDir);
254
+ return existsSync(agentPath) && isOurAgent(agentPath);
255
+ }
256
+
257
+ // ── internal ─────────────────────────────────────────────────────────────────
258
+
259
+ // Is the agent at `path` ours? Keyed on our marker in the (valid) `description`
260
+ // field — NOT a rejected top-level field. BOM-tolerant.
261
+ function isOurAgent(path) {
262
+ const j = parseJsonFile(path, { fallback: null });
263
+ return j != null && typeof j.description === 'string' && j.description.includes(MANAGED_MARKER);
264
+ }
265
+
266
+ // Read the user's chat.defaultAgent from ~/.kiro/settings/cli.json. BOM-tolerant.
267
+ function readDefaultAgentSetting(settingsPath) {
268
+ const j = parseJsonFile(settingsPath, { fallback: null });
269
+ return j != null ? (j['chat.defaultAgent'] ?? null) : null;
270
+ }
@@ -0,0 +1,19 @@
1
+ // kiro-constants.mjs — leaf module of shared Kiro constants, so multiple Kiro
2
+ // modules can import them without an import cycle (install-kiro ↔ kiro-permissions).
3
+
4
+ // The kit's 11 MCP tool names (bare). Used for BOTH the IDE mcp.json `autoApprove`
5
+ // (install-kiro) AND the IDE-1.0 permissions.yaml `capability: mcp` match list
6
+ // (kiro-permissions) — one source so the two never drift.
7
+ export const MCP_AUTO_APPROVE = Object.freeze([
8
+ 'mk_remember',
9
+ 'mk_search',
10
+ 'mk_get',
11
+ 'mk_timeline',
12
+ 'mk_cite',
13
+ 'mk_recent_activity',
14
+ 'mk_trust',
15
+ 'mk_lessons_promote',
16
+ 'mk_forget',
17
+ 'mk_queue_list',
18
+ 'mk_queue_resolve',
19
+ ]);
@@ -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
+ }