@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.
- package/README.md +137 -50
- package/bin/cmk-approve-permission.mjs +62 -0
- package/bin/cmk-daily-distill.mjs +14 -0
- package/bin/cmk-guard-memory.mjs +57 -0
- package/bin/cmk-inject-context.mjs +12 -0
- package/bin/cmk-weekly-curate.mjs +12 -0
- package/package.json +4 -2
- package/src/agent-profile.mjs +115 -0
- package/src/agent-profiles.mjs +118 -0
- package/src/approve-permission.mjs +92 -0
- package/src/auto-extract.mjs +17 -10
- package/src/auto-persona.mjs +11 -4
- package/src/compaction-state.mjs +204 -0
- 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 +128 -5
- package/src/guard-memory.mjs +151 -0
- package/src/import-anthropic-memory.mjs +15 -1
- package/src/inject-context.mjs +42 -18
- package/src/install-agent.mjs +220 -0
- package/src/install-kiro.mjs +287 -0
- package/src/install.mjs +53 -7
- 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/lazy-compress.mjs +43 -110
- 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/register-crons.mjs +31 -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 +70 -3
- package/src/subcommands.mjs +360 -27
- package/src/tier-paths.mjs +82 -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,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
|
+
}
|