@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.
Files changed (44) hide show
  1. package/README.md +6 -0
  2. package/bin/cmk-guard-memory.mjs +57 -0
  3. package/package.json +3 -2
  4. package/src/agent-profile.mjs +115 -0
  5. package/src/agent-profiles.mjs +118 -0
  6. package/src/auto-persona.mjs +4 -1
  7. package/src/compress-session.mjs +13 -1
  8. package/src/config-core.mjs +7 -9
  9. package/src/decisions-journal.mjs +71 -3
  10. package/src/doctor.mjs +86 -4
  11. package/src/guard-memory.mjs +151 -0
  12. package/src/import-anthropic-memory.mjs +15 -1
  13. package/src/inject-context.mjs +34 -3
  14. package/src/install-agent.mjs +220 -0
  15. package/src/install-kiro.mjs +287 -0
  16. package/src/install.mjs +16 -3
  17. package/src/kiro-cli-agent.mjs +270 -0
  18. package/src/kiro-constants.mjs +19 -0
  19. package/src/kiro-hook-bin.mjs +105 -0
  20. package/src/kiro-hook-command.mjs +67 -0
  21. package/src/kiro-hook-dispatch.mjs +115 -0
  22. package/src/kiro-ide-hooks.mjs +219 -0
  23. package/src/kiro-permissions.mjs +175 -0
  24. package/src/kiro-skills.mjs +96 -0
  25. package/src/kiro-transcript.mjs +366 -0
  26. package/src/kiro-trusted-commands.mjs +130 -0
  27. package/src/managed-block.mjs +138 -0
  28. package/src/memory-write.mjs +23 -8
  29. package/src/mutate-agent-config.mjs +243 -0
  30. package/src/read-json.mjs +43 -0
  31. package/src/reindex.mjs +15 -2
  32. package/src/repair.mjs +39 -3
  33. package/src/result-shapes.mjs +8 -0
  34. package/src/review-queue.mjs +3 -0
  35. package/src/scratchpad.mjs +12 -2
  36. package/src/search.mjs +12 -5
  37. package/src/semantic-backend.mjs +7 -9
  38. package/src/settings-hooks.mjs +12 -2
  39. package/src/subcommands.mjs +360 -27
  40. package/src/tier-paths.mjs +48 -1
  41. package/src/weekly-curate.mjs +6 -2
  42. package/template/.claude/skills/memory-search/SKILL.md +14 -1
  43. package/template/.claude/skills/memory-write/SKILL.md +37 -1
  44. package/template/project/memory/INDEX.md.template +1 -1
@@ -0,0 +1,287 @@
1
+ // install-kiro.mjs — the Kiro orchestrator, all surfaces (Task 50, D-182).
2
+ //
3
+ // Kiro needs its OWN installer branch (D-182): the generic installAgent assumed
4
+ // a Claude-Code-shaped model (one settings file + one instruction file) that
5
+ // doesn't fit Kiro's distinct surfaces. This composes the verified surface
6
+ // modules:
7
+ // MCP → .kiro/settings/mcp.json (mcpServers.cmk) via mutateAgentConfig
8
+ // steering → .kiro/steering/cmk.md (inclusion: always), managed marker block
9
+ // skills → .kiro/skills/{memory-search,memory-write}/ via installKiroSkills
10
+ // IDE hooks → .kiro/hooks/cmk-{capture,inject}.kiro.hook via installKiroIdeHooks
11
+ // CLI agent → ~/.kiro/agents/cmk.json + ~/.kiro/settings/cli.json (agentSpawn/stop hooks + default-agent
12
+ // default-agent) via installKiroCliAgent — for kiro-cli users
13
+ //
14
+ // IDE hooks auto-fire with no agent selection; the CLI agent needs cmk to be the
15
+ // default agent (guarded — never clobbers a user's existing default). Both hook
16
+ // surfaces reuse the SAME `cmk hook <event>` dispatcher (the shared core).
17
+ //
18
+ // Each leg reuses the kit's safe primitives: mutateAgentConfig (touch-only-our-
19
+ // keys, refuse-to-clobber-on-parse-error), managed marker blocks (byte-preserve),
20
+ // and the skills/hooks copiers (idempotent, remove-only-ours on uninstall).
21
+ //
22
+ // Public surface:
23
+ // installKiro({ projectRoot, awsDir? }) → { action, changed, surfaces, cliDefaultAgent, errors? }
24
+ // uninstallKiro({ projectRoot, awsDir? }) → { action, changed }
25
+ // (awsDir is the kept-for-back-compat alias for the CLI-agent sandbox base —
26
+ // it sandboxes the CLI-agent leg in tests; production writes the real ~/.kiro
27
+ // [agents/cmk.json + settings/cli.json], NOT ~/.aws. The name predates the
28
+ // D-198 ~/.aws→~/.kiro relocation; MEMORY_KIT_KIRO_DIR is the current env var.)
29
+
30
+ import { join } from 'node:path';
31
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
32
+ import { mutateAgentConfig } from './mutate-agent-config.mjs';
33
+ import { installKiroSkills, uninstallKiroSkills } from './kiro-skills.mjs';
34
+ import { installKiroIdeHooks, uninstallKiroIdeHooks } from './kiro-ide-hooks.mjs';
35
+ import { installKiroCliAgent, uninstallKiroCliAgent } from './kiro-cli-agent.mjs';
36
+ import { installKiroTrustedCommands, uninstallKiroTrustedCommands } from './kiro-trusted-commands.mjs';
37
+ import { installKiroPermissions, uninstallKiroPermissions } from './kiro-permissions.mjs';
38
+ import {
39
+ writeManagedBlock,
40
+ removeManagedBlock,
41
+ removeJsonKey,
42
+ pruneEmptyParent,
43
+ } from './managed-block.mjs';
44
+
45
+ const MCP_PATH = ['settings', 'mcp.json'];
46
+ const MCP_SERVERS_KEY = 'mcpServers';
47
+ const MCP_SERVER_NAME = 'claude-memory-kit';
48
+ // autoApprove pre-approves the kit's MCP tools so Kiro runs them WITHOUT a
49
+ // per-call "Reject / Trust / Run" prompt (found live in cut-gate-kiro Session 1:
50
+ // Kiro gates MCP TOOL calls separately from the shell-command hooks D-194 wired,
51
+ // so mk_remember etc. prompted every time). Verified shape from kiro.dev/docs/mcp:
52
+ // an `autoApprove` array of bare tool names INSIDE the server entry. Explicit
53
+ // list of the 11 kit tools — scoped to OUR tools, never a `"*"` wildcard (which
54
+ // would auto-approve any tool the server ever adds). mk_forget is safe to
55
+ // auto-approve the CALL: it has its own two-step confirm-token before deleting.
56
+ // MCP_AUTO_APPROVE now lives in the leaf kiro-constants.mjs (shared with
57
+ // kiro-permissions, no import cycle); imported for local use + re-exported for
58
+ // back-compat (existing importers of install-kiro.MCP_AUTO_APPROVE keep working).
59
+ import { MCP_AUTO_APPROVE } from './kiro-constants.mjs';
60
+ export { MCP_AUTO_APPROVE };
61
+ // The MCP entry for `.kiro/settings/mcp.json` — the KIRO IDE's MCP surface (the
62
+ // IDE wires MCP tools to the chat; the kiro-cli agent sets includeMcpJson:false
63
+ // and does NOT use MCP — it uses the `cmk remember`/`cmk search` shell commands).
64
+ // `cmk mcp serve` resolves its project from CLAUDE_PROJECT_DIR (Claude Code) or
65
+ // the launch cwd; the IDE launches from the workspace, so no per-project arg is
66
+ // needed here (the IDE worked without one).
67
+ const MCP_ENTRY = Object.freeze({
68
+ type: 'stdio',
69
+ command: 'cmk',
70
+ args: ['mcp', 'serve'],
71
+ autoApprove: MCP_AUTO_APPROVE,
72
+ });
73
+
74
+ const STEERING_PATH = ['steering', 'cmk.md'];
75
+ const STEERING_FRONTMATTER = '---\ninclusion: always\n---\n\n';
76
+ const MEMORY_BODY = [
77
+ '# claude-memory-kit',
78
+ '',
79
+ 'This project uses claude-memory-kit for durable, in-repo memory across sessions.',
80
+ 'Recall before re-deriving: run `cmk search "<topic>"` for prior decisions,',
81
+ 'preferences, and project facts; the curated tiers live under `context/`.',
82
+ 'Capture durable facts with `cmk remember` — never hand-edit the memory files.',
83
+ ].join('\n');
84
+ const STEERING_BODY = MEMORY_BODY;
85
+
86
+ // AGENTS.md — Kiro's real, always-loaded instruction file (kiro.dev/docs/steering:
87
+ // auto-included from the project root, no inclusion modes). The CLI agent-config's
88
+ // `prompt: file://AGENTS.md` resolves to THIS (D-188). A managed block so it
89
+ // coexists with any user AGENTS.md content; no frontmatter (AGENTS.md is the
90
+ // cross-tool standard — inclusion modes are a Kiro-steering-only feature).
91
+ const AGENTS_MD_PATH = 'AGENTS.md';
92
+ const AGENTS_MD_BODY = MEMORY_BODY;
93
+
94
+ export function installKiro({ projectRoot, awsDir } = {}) {
95
+ if (!projectRoot) throw new Error('installKiro: projectRoot is required');
96
+ const kiro = (parts) => join(projectRoot, '.kiro', ...parts);
97
+
98
+ const surfaces = [];
99
+ const errors = [];
100
+ const warnings = []; // non-fatal surface failures (e.g. a corrupt user .vscode/settings.json)
101
+ let changed = false;
102
+ let cliDefaultAgent = null;
103
+
104
+ // ── MCP ──────────────────────────────────────────────────────────────────
105
+ const mcp = mutateAgentConfig({
106
+ path: kiro(MCP_PATH),
107
+ format: 'json',
108
+ keyPath: [MCP_SERVERS_KEY, MCP_SERVER_NAME],
109
+ entry: MCP_ENTRY,
110
+ });
111
+ if (mcp.action === 'error') {
112
+ errors.push({ surface: 'mcp', ...mcp });
113
+ } else {
114
+ surfaces.push('mcp');
115
+ if (mcp.changed) changed = true;
116
+ }
117
+
118
+ // If MCP refused (corrupt config), halt — report, don't push past it.
119
+ if (errors.length > 0) {
120
+ return { action: 'error', changed, surfaces, errors };
121
+ }
122
+
123
+ // ── steering ───────────────────────────────────────────────────────────────
124
+ if (writeManagedBlock(kiro(STEERING_PATH), { body: STEERING_BODY, frontmatter: STEERING_FRONTMATTER })) changed = true;
125
+ surfaces.push('steering');
126
+
127
+ // ── AGENTS.md (project root) — Kiro's always-loaded instruction file; the CLI
128
+ // agent-config's prompt:file://AGENTS.md points here (D-188). Managed block,
129
+ // no frontmatter. Coexists with a Claude-Code CLAUDE.md (each agent reads
130
+ // its own) and with any user-authored AGENTS.md content.
131
+ if (writeManagedBlock(join(projectRoot, AGENTS_MD_PATH), { body: AGENTS_MD_BODY })) changed = true;
132
+ surfaces.push('agents-md');
133
+
134
+ // ── skills ───────────────────────────────────────────────────────────────
135
+ const skills = installKiroSkills({ projectRoot });
136
+ if (skills.changed) changed = true;
137
+ surfaces.push('skills');
138
+
139
+ // ── IDE hooks ──────────────────────────────────────────────────────────────
140
+ const hooks = installKiroIdeHooks({ projectRoot });
141
+ if (hooks.changed) changed = true;
142
+ surfaces.push('ide-hooks');
143
+
144
+ // ── trusted commands (D-194) — pre-trust the kit's hook commands in the
145
+ // workspace .vscode/settings.json so Kiro auto-runs them instead of
146
+ // prompting "Run / Reject" on every hook fire. Without this the IDE hooks
147
+ // above are wired but not silent. Array-union (preserves a user's existing
148
+ // trustedCommands); refuse-to-clobber a corrupt settings.json.
149
+ //
150
+ // NON-FATAL on error, UNLIKE the MCP leg above (skill-review I1): MCP is the
151
+ // FIRST surface (a corrupt MCP config → clean abort, nothing written yet).
152
+ // Trust runs LAST-but-one, after MCP/steering/AGENTS.md/skills/ide-hooks all
153
+ // succeeded — and it's the LEAST critical surface (if it can't write, the
154
+ // hooks still fire, they just prompt). So a user's pre-existing malformed
155
+ // .vscode/settings.json must NOT abort an otherwise-good install: record the
156
+ // warning, skip the surface, and continue to the CLI agent.
157
+ const trust = installKiroTrustedCommands({ projectRoot });
158
+ if (trust.action === 'error') {
159
+ warnings.push({ surface: 'trusted-commands', ...trust });
160
+ } else {
161
+ if (trust.changed) changed = true;
162
+ surfaces.push('trusted-commands');
163
+ }
164
+
165
+ // ── IDE-1.0 permissions (50.N.5 / D-203h/i) — Kiro IDE 1.0's AUTHORITATIVE trust
166
+ // is ~/.kiro/workspace-roots/<hash>/permissions.yaml (NOT .vscode; Kiro
167
+ // migrates .vscode into it). The `.vscode` trustedCommands above keep 0.x +
168
+ // pre-migration working; this pre-trusts the kit's shell + mcp + SKILL on 1.0
169
+ // so even the first memory-write skill-load runs prompt-free (the one thing
170
+ // `.vscode` has NO setting for). Best-effort like trusted-commands — a write
171
+ // failure never aborts the install.
172
+ let permissions;
173
+ try {
174
+ permissions = installKiroPermissions({ projectRoot });
175
+ if (permissions.changed) changed = true;
176
+ surfaces.push('permissions');
177
+ } catch (err) {
178
+ warnings.push({ surface: 'permissions', action: 'error', error: err?.message ?? String(err) });
179
+ }
180
+
181
+ // ── CLI agent-config (kiro-cli, user-tier) — agentSpawn/stop hooks + the
182
+ // guarded default-agent. Covers terminal `kiro-cli` users; the IDE hooks
183
+ // above cover the GUI. Both reuse the same `cmk hook` dispatcher.
184
+ const cli = installKiroCliAgent({ awsDir });
185
+ cliDefaultAgent = cli.defaultAgent; // 'set' | 'skipped-existing'
186
+ if (cli.changed) changed = true;
187
+ surfaces.push('cli-agent');
188
+
189
+ const result = { action: 'installed', changed, surfaces, cliDefaultAgent };
190
+ if (warnings.length > 0) result.warnings = warnings;
191
+ return result;
192
+ }
193
+
194
+ export function uninstallKiro({ projectRoot, awsDir } = {}) {
195
+ if (!projectRoot) throw new Error('uninstallKiro: projectRoot is required');
196
+ const kiro = (parts) => join(projectRoot, '.kiro', ...parts);
197
+ let changed = false;
198
+
199
+ // MCP: remove only our server key, prune an emptied servers object.
200
+ const mcpPath = kiro(MCP_PATH);
201
+ const mcpTouched = removeJsonKey(mcpPath, [MCP_SERVERS_KEY, MCP_SERVER_NAME]);
202
+ if (mcpTouched) changed = true;
203
+ pruneEmptyParent(mcpPath, [MCP_SERVERS_KEY]);
204
+
205
+ // steering: strip our marker block (byte-preserve the rest).
206
+ const steeringPath = kiro(STEERING_PATH);
207
+ const steeringTouched = removeManagedBlock(steeringPath);
208
+ if (steeringTouched) changed = true;
209
+
210
+ // AGENTS.md: strip our marker block only (a user's own AGENTS.md content,
211
+ // outside our markers, is byte-preserved).
212
+ const agentsMdPath = join(projectRoot, AGENTS_MD_PATH);
213
+ const agentsMdTouched = removeManagedBlock(agentsMdPath);
214
+ if (agentsMdTouched) changed = true;
215
+
216
+ // No husks (D-191): a file the kit created that uninstall just emptied —
217
+ // an empty AGENTS.md, a `{}` mcp.json, a frontmatter-only steering file —
218
+ // should be removed, not left as a confusing shell. removeIfHusk deletes
219
+ // ONLY when no user content remains. GATED on "the kit's removal step actually
220
+ // changed THIS file" (B2 defense-in-depth) — so a pristine, never-kit-managed
221
+ // file is never even a deletion candidate, narrowing the blast radius.
222
+ if (agentsMdTouched && removeIfHusk(agentsMdPath)) changed = true;
223
+ if (steeringTouched && removeIfHusk(steeringPath)) changed = true;
224
+ if (mcpTouched && removeIfHusk(mcpPath)) changed = true;
225
+
226
+ // skills + IDE hooks + CLI agent-config: remove our files only.
227
+ if (uninstallKiroSkills({ projectRoot }).changed) changed = true;
228
+ if (uninstallKiroIdeHooks({ projectRoot }).changed) changed = true;
229
+ if (uninstallKiroCliAgent({ awsDir }).changed) changed = true;
230
+
231
+ // trusted commands (D-194): remove ONLY our patterns from
232
+ // .vscode/settings.json, preserving the user's; prune an emptied key.
233
+ if (uninstallKiroTrustedCommands({ projectRoot }).changed) changed = true;
234
+
235
+ // IDE-1.0 permissions (50.N.5): remove ONLY our rules from
236
+ // ~/.kiro/workspace-roots/<hash>/permissions.yaml, preserving the user's.
237
+ // Best-effort (the file is per-user, may be absent); never abort uninstall.
238
+ try {
239
+ if (uninstallKiroPermissions({ projectRoot }).changed) changed = true;
240
+ } catch {
241
+ /* best-effort — a missing/locked permissions.yaml never blocks uninstall */
242
+ }
243
+
244
+ return { action: 'uninstalled', changed };
245
+ }
246
+
247
+ // Is `trimmed` ONLY a YAML frontmatter block (opener `---`, a closing `---`,
248
+ // and nothing but blank lines after the FIRST closing fence)? Line-scan, not a
249
+ // backtracking regex (the managed-block.mjs ReDoS-safe-string-utils discipline)
250
+ // — AND, critically, the closing fence must be the FIRST `---` after the opener
251
+ // with nothing meaningful after it. A naive `---[\s\S]*?---$` regex backtracks
252
+ // to a LATER `---` (a user's horizontal rule) and would match — then DELETE — a
253
+ // file full of user prose bordered by `---` (the skill-review B1 data-loss bug).
254
+ function isOnlyFrontmatter(trimmed) {
255
+ const lines = trimmed.split(/\r?\n/);
256
+ if (lines[0] !== '---') return false;
257
+ const close = lines.indexOf('---', 1);
258
+ if (close === -1) return false;
259
+ // nothing but blank lines may follow the first closing fence
260
+ return lines.slice(close + 1).every((l) => l.trim() === '');
261
+ }
262
+
263
+ // Delete a file the kit created IFF uninstall left it with no real content —
264
+ // i.e. it's now empty, an empty JSON object (`{}`), or only YAML frontmatter.
265
+ // Anything else (user prose, a sibling MCP server, user frontmatter+body) means
266
+ // real content remains → keep the file. Conservative by construction: a
267
+ // non-husk is never touched. Returns true if a file was removed.
268
+ // (D-191 — no empty husks after a Kiro uninstall; B1-fixed predicate.)
269
+ function removeIfHusk(path) {
270
+ if (!existsSync(path)) return false;
271
+ let raw;
272
+ try {
273
+ raw = readFileSync(path, 'utf8');
274
+ } catch {
275
+ return false;
276
+ }
277
+ const trimmed = raw.trim();
278
+ if (trimmed === '' || trimmed === '{}' || isOnlyFrontmatter(trimmed)) {
279
+ try {
280
+ rmSync(path, { force: true });
281
+ return true;
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+ return false;
287
+ }
package/src/install.mjs CHANGED
@@ -371,8 +371,13 @@ export async function install(options = {}) {
371
371
  // design §1.3. Same boundary as the tiers: idempotent skip-existing +
372
372
  // over-mutation-safe (a hand-edited skill survives a re-install). The skill
373
373
  // files carry no {{placeholders}}, so renderTemplate is a byte-passthrough.
374
+ //
375
+ // `.claude/skills/` is CLAUDE-CODE-SPECIFIC. A `--ide kiro` install gets its
376
+ // skills at `.kiro/skills/` (written by the Kiro orchestrator), so it passes
377
+ // skipClaudeFiles to avoid leaving a dead Claude skills dir on a Kiro project
378
+ // (the cut-gate find: a Kiro user shouldn't carry Claude Code's skill files).
374
379
  const skillsSrc = join(templateDir, '.claude', 'skills');
375
- if (existsSync(skillsSrc)) {
380
+ if (!options.skipClaudeFiles && existsSync(skillsSrc)) {
376
381
  installTier(skillsSrc, join(projectRoot, '.claude', 'skills'), {
377
382
  created,
378
383
  skipped,
@@ -389,10 +394,18 @@ export async function install(options = {}) {
389
394
 
390
395
  // CLAUDE.md loader block — Task 4. Read the block content from the kit's
391
396
  // template/ and inject (or refresh) it inside marker delimiters. Never
392
- // touches content outside the markers.
397
+ // touches content outside the markers. CLAUDE.md is CLAUDE-CODE-SPECIFIC
398
+ // (Kiro reads AGENTS.md + steering, not CLAUDE.md) — a --ide kiro install
399
+ // passes skipClaudeFiles so it doesn't drop a CLAUDE.md the Kiro user can't
400
+ // use (D-188). An EXISTING CLAUDE.md from a prior Claude-Code install is left
401
+ // untouched regardless (we simply don't write a fresh one).
393
402
  const claudeMdTemplatePath = join(templateDir, 'CLAUDE.md.template');
394
403
  let claudeMd = { action: 'skipped', path: join(projectRoot, 'CLAUDE.md') };
395
- if (existsSync(claudeMdTemplatePath)) {
404
+ if (options.skipClaudeFiles) {
405
+ // a non-Claude-Code (--ide kiro) install: CLAUDE.md is intentionally not
406
+ // written; an existing one is left untouched. NOT an error.
407
+ claudeMd = { action: 'skipped', reason: 'non-claude-code-agent', path: join(projectRoot, 'CLAUDE.md') };
408
+ } else if (existsSync(claudeMdTemplatePath)) {
396
409
  const content = readFileSync(claudeMdTemplatePath, 'utf8');
397
410
  try {
398
411
  claudeMd = injectClaudeMdBlock({ projectRoot, content, version, force });
@@ -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
+ ]);