@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,219 @@
1
+ // kiro-ide-hooks.mjs — write Kiro IDE hook files (Task 50.K + 50.N.3 v1 migration).
2
+ //
3
+ // TWO formats, DUAL-EMITTED for back-compat (D-203):
4
+ //
5
+ // LEGACY (Kiro 0.x) — individual `.kiro/hooks/<name>.kiro.hook` files:
6
+ // { "version": "1.0.0", "enabled", "name", "description",
7
+ // "when": { "type": "agentStop" },
8
+ // "then": { "type": "runCommand", "command", "timeout" } }
9
+ // Verified from a real GUI-created 0.x hook (P-WJRUQVSW).
10
+ //
11
+ // v1 (Kiro IDE 1.0+) — clean per-hook `.kiro/hooks/<name>.json` files that
12
+ // REPLACE the .kiro.hook format (which 1.0 no longer loads — D-203). Schema
13
+ // GROUND-TRUTH-VERIFIED against Kiro IDE 1.0's OWN migration output (D-203d —
14
+ // it migrated our `cmk-capture.kiro.hook` → `cmk-capture.json`):
15
+ // { "version": "v1", "hooks": [ { "name", "description", "trigger",
16
+ // "matcher"?, "action": { "type": "command", "command" }, "timeout",
17
+ // "enabled" } ] } — PascalCase triggers; `action.type:'command'` is the
18
+ // deterministic-shell action (no LLM). We write `cmk-capture.json`/
19
+ // `cmk-inject.json`/`cmk-guard.json`/`cmk-observe.json` — Kiro's exact filename
20
+ // convention, one hook per file (full isolation).
21
+ //
22
+ // v1 lets the IDE do the FULL Claude-Code hook set (one clean .json file PER
23
+ // hook): inject (UserPromptSubmit) + capture (Stop) + delete-guard (PreToolUse —
24
+ // CAN BLOCK on non-zero exit) + observe-edit (PostToolUse).
25
+ //
26
+ // We emit BOTH so a 0.x user keeps the legacy hooks and a 1.0 user gets v1 (a
27
+ // 1.0 IDE runs the .json + shows the .kiro.hook as inert "legacy"; a 0.x IDE
28
+ // runs the .kiro.hook + ignores the .json — no double-fire, verified D-203d).
29
+ //
30
+ // ⚠️ v1 behaviors LIVE-PROBED at the cut-gate (D-203 — `Stop` + the schema are
31
+ // CONFIRMED by Kiro 1.0's own migration D-203d; the rest flagged, NOT asserted):
32
+ // (1) auto-load of an installer-written json; (2) what a PreToolUse command
33
+ // receives (the path to inspect); (3) exit-code 1-vs-2 to
34
+ // block; (4) the matcher tool-name tokens; (5) the real session-end trigger
35
+ // name (v1's type list has SessionStart but no obvious Stop — we use `Stop`,
36
+ // the dispatcher's capture key, pending the probe).
37
+ //
38
+ // Public surface:
39
+ // installKiroIdeHooks({ projectRoot, command? }) → { action, changed, hooks }
40
+ // uninstallKiroIdeHooks({ projectRoot }) → { action, changed, hooks }
41
+
42
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
43
+ import { join } from 'node:path';
44
+ import { kiroHookCommand, kiroGuardCommand } from './kiro-hook-command.mjs';
45
+
46
+ const CMK = 'cmk';
47
+ const MANAGED = 'Managed by `cmk install` — do not hand-edit.';
48
+
49
+ // The platform-correct `cmk hook <event>` command (cmd.exe /c on Windows where
50
+ // Kiro routes hooks through WSL) is shared from kiro-hook-command.mjs.
51
+ const hookCommand = kiroHookCommand;
52
+
53
+ // One .kiro.hook spec. We intentionally do NOT put a `then` key on any JS object
54
+ // here — Kiro's schema needs a top-level `then` (its "action" leg), but a JS
55
+ // object with a `then` property is a "thenable" footgun (a static analyzer flags
56
+ // it, and an accidental thenable can hijack a Promise chain). So we model the
57
+ // action under a neutral `action` key and rename it to `then` ONLY at
58
+ // serialization (serializeHook), where it's pure JSON data, never a live object.
59
+ function hookSpecs(cmd) {
60
+ return [
61
+ {
62
+ file: 'cmk-capture.kiro.hook',
63
+ version: '1.0.0',
64
+ enabled: true,
65
+ name: 'claude-memory-kit: capture',
66
+ description: 'Capture durable memory at the end of each turn (claude-memory-kit). Managed by `cmk install` — do not hand-edit.',
67
+ when: { type: 'agentStop' },
68
+ action: { type: 'runCommand', command: hookCommand('stop', cmd), timeout: 60 },
69
+ },
70
+ {
71
+ file: 'cmk-inject.kiro.hook',
72
+ version: '1.0.0',
73
+ enabled: true,
74
+ name: 'claude-memory-kit: recall',
75
+ description: 'Inject recalled memory on each prompt (claude-memory-kit). Managed by `cmk install` — do not hand-edit.',
76
+ when: { type: 'promptSubmit' },
77
+ action: { type: 'runCommand', command: hookCommand('promptSubmit', cmd), timeout: 30 },
78
+ },
79
+ ];
80
+ }
81
+
82
+ // Serialize a spec to the Kiro .kiro.hook JSON, mapping our internal `action`
83
+ // key to Kiro's required `then` field. We build the object with a placeholder
84
+ // key, then rename it in the JSON STRING — so no JS object literal ever carries
85
+ // a `then` property (the thenable footgun the static analyzer guards against).
86
+ const THEN_PLACEHOLDER = '__kiro_then__';
87
+ function serializeHook(spec) {
88
+ const { file, action, ...rest } = spec;
89
+ const obj = { ...rest, [THEN_PLACEHOLDER]: action };
90
+ return `${JSON.stringify(obj, null, 2).replace(`"${THEN_PLACEHOLDER}"`, '"then"')}\n`;
91
+ }
92
+
93
+ // The v1 hooks — ONE clean `<name>.json` file PER hook (Kiro IDE 1.0's own
94
+ // convention — D-203d). One hook per file → every hook independently isolated (a
95
+ // bad trigger can't dark the others). The full Claude-Code parity set:
96
+ // cmk-capture.json → Stop → capture (turn-end)
97
+ // cmk-inject.json → UserPromptSubmit → inject (recall)
98
+ // cmk-guard.json → PreToolUse → delete-guard (CAN BLOCK on non-zero exit)
99
+ // cmk-observe.json → PostToolUse → observe-edit (record large edits)
100
+ // Schema GROUND-TRUTH-verified against Kiro IDE 1.0's own migration output (D-203d):
101
+ // {version:'v1', hooks:[{name, description, trigger, matcher?, action:{type:
102
+ // 'command', command}, timeout, enabled}]}.
103
+ //
104
+ // SINGLE SOURCE: filename → hook-spec builder. V1_FILES is DERIVED from these keys
105
+ // (no array/switch to keep in sync). `Stop` (capture) is CONFIRMED by Kiro's
106
+ // migration; the other three triggers + the `fs_write` matcher are live-probed
107
+ // (cut-gate KHv1-*).
108
+ function v1HookFile(hook) {
109
+ return { version: 'v1', hooks: [hook] };
110
+ }
111
+ const V1_HOOK_BUILDERS = Object.freeze({
112
+ 'cmk-capture.json': (cmd) =>
113
+ v1HookFile({
114
+ name: 'claude-memory-kit: capture',
115
+ description: `Capture durable memory at the end of each turn (claude-memory-kit). ${MANAGED}`,
116
+ trigger: 'Stop', // CONFIRMED by Kiro IDE 1.0's own migration (D-203d)
117
+ action: { type: 'command', command: hookCommand('stop', cmd) },
118
+ timeout: 60,
119
+ enabled: true,
120
+ }),
121
+ 'cmk-inject.json': (cmd) =>
122
+ v1HookFile({
123
+ name: 'claude-memory-kit: recall',
124
+ description: `Inject recalled memory on each prompt (claude-memory-kit). ${MANAGED}`,
125
+ trigger: 'UserPromptSubmit',
126
+ action: { type: 'command', command: hookCommand('userPromptSubmit', cmd) },
127
+ timeout: 30,
128
+ enabled: true,
129
+ }),
130
+ 'cmk-guard.json': () =>
131
+ v1HookFile({
132
+ name: 'claude-memory-kit: delete-guard',
133
+ description: `Block a destructive command aimed at a memory path (claude-memory-kit). ${MANAGED}`,
134
+ trigger: 'PreToolUse', // v1 PreToolUse can BLOCK (non-zero exit) — supersedes Task 165(b)
135
+ // `matcher` for PreToolUse is a TOOL-NAME glob (D-203 item 4 — exact tokens
136
+ // live-unverified). `'*'` is conservative; the guard itself filters to memory
137
+ // deletes, so an over-broad matcher costs nothing (allows all non-deletes).
138
+ matcher: '*',
139
+ action: { type: 'command', command: kiroGuardCommand() },
140
+ timeout: 5,
141
+ enabled: true,
142
+ }),
143
+ 'cmk-observe.json': (cmd) =>
144
+ v1HookFile({
145
+ name: 'claude-memory-kit: observe-edit',
146
+ description: `Record large file edits (claude-memory-kit). ${MANAGED}`,
147
+ // PostToolUse (NOT PostFileSave) — observe-edit needs a TOOL-USE payload
148
+ // ({tool_name:'fs_write', …}) that observeEdit reads; a file-SAVE event
149
+ // carries no tool_name → silent noop (skill-review I1). Sibling of the
150
+ // kiro-cli postToolUse leg (50.N.2), same payload shape.
151
+ trigger: 'PostToolUse',
152
+ matcher: 'fs_write', // tool-name glob (like PreToolUse), scoped to file-writes
153
+ action: { type: 'command', command: hookCommand('postToolUse', cmd) },
154
+ timeout: 30,
155
+ enabled: true,
156
+ }),
157
+ });
158
+
159
+ // V1_FILES is DERIVED from the builder keys — single source of truth, no array
160
+ // to keep in sync (an unmapped filename is structurally impossible).
161
+ const V1_FILES = Object.freeze(Object.keys(V1_HOOK_BUILDERS));
162
+
163
+ function serializeV1(file, cmd) {
164
+ return `${JSON.stringify(V1_HOOK_BUILDERS[file](cmd), null, 2)}\n`;
165
+ }
166
+
167
+ export function installKiroIdeHooks({ projectRoot, command = CMK } = {}) {
168
+ if (!projectRoot) throw new Error('installKiroIdeHooks: projectRoot is required');
169
+ const hooksDir = join(projectRoot, '.kiro', 'hooks');
170
+
171
+ let changed = false;
172
+ const written = [];
173
+ // 1. Legacy .kiro.hook files (Kiro 0.x back-compat).
174
+ for (const spec of hookSpecs(command)) {
175
+ const path = join(hooksDir, spec.file);
176
+ const serialized = serializeHook(spec);
177
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
178
+ if (existing !== serialized) {
179
+ mkdirSync(hooksDir, { recursive: true });
180
+ writeFileSync(path, serialized, 'utf8');
181
+ changed = true;
182
+ }
183
+ written.push(spec.file);
184
+ }
185
+ // 2. The v1 files (Kiro IDE 1.0+ — D-203/D-203d). A 0.x IDE ignores them; a 1.0
186
+ // IDE ignores the stale .kiro.hook files above (shows them "legacy", inert —
187
+ // no double-fire, verified D-203d). ONE clean `<name>.json` PER hook (Kiro's
188
+ // own migration convention), so every hook is isolated.
189
+ for (const file of V1_FILES) {
190
+ const path = join(hooksDir, file);
191
+ const serialized = serializeV1(file, command);
192
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
193
+ if (existing !== serialized) {
194
+ mkdirSync(hooksDir, { recursive: true });
195
+ writeFileSync(path, serialized, 'utf8');
196
+ changed = true;
197
+ }
198
+ written.push(file);
199
+ }
200
+ return { action: 'installed', changed, hooks: written };
201
+ }
202
+
203
+ export function uninstallKiroIdeHooks({ projectRoot, command = CMK } = {}) {
204
+ if (!projectRoot) throw new Error('uninstallKiroIdeHooks: projectRoot is required');
205
+ const hooksDir = join(projectRoot, '.kiro', 'hooks');
206
+ let changed = false;
207
+ const removed = [];
208
+ // legacy .kiro.hook files + all the v1 files — remove all.
209
+ const ourFiles = [...hookSpecs(command).map((s) => s.file), ...V1_FILES];
210
+ for (const file of ourFiles) {
211
+ const path = join(hooksDir, file);
212
+ if (existsSync(path)) {
213
+ rmSync(path, { force: true });
214
+ changed = true;
215
+ removed.push(file);
216
+ }
217
+ }
218
+ return { action: 'uninstalled', changed, hooks: removed };
219
+ }
@@ -0,0 +1,175 @@
1
+ // kiro-permissions.mjs — write Kiro IDE 1.0's authoritative trust store so the
2
+ // kit's surfaces run prompt-free (Task 50.N.5 / D-203h/D-203i).
3
+ //
4
+ // On Kiro IDE 1.0, the LIVE trust is `~/.kiro/workspace-roots/<hash>/
5
+ // permissions.yaml` (capability/match/effect), NOT `.vscode/settings.json` — Kiro
6
+ // auto-MIGRATES the latter into the former at first open (D-203i: proven by the
7
+ // `.trust-migration.json` sibling + MCP running prompt-free with no `.vscode`
8
+ // trustedMcpTools). There is NO `.vscode` setting for SKILL trust, so the only
9
+ // reliable pre-trust for the memory-write skill-load prompt is to write
10
+ // permissions.yaml directly.
11
+ //
12
+ // <hash> = sha256( projectRoot, normalized: forward-slash + no-trailing-slash +
13
+ // lowercase ).hexdigest().slice(0,16) — VERIFIED on a real install (D-203h:
14
+ // c:/temp/kiro-ide-gate → a7ffdb64ec4c31c8).
15
+ //
16
+ // Format (ground-truth, read from a real grant — D-203h):
17
+ // rules:
18
+ // - { capability: shell, match: [cmd.exe /c cmk hook *, ...], effect: allow }
19
+ // - { capability: mcp, match: [claude-memory-kit/mk_remember, ...], effect: allow }
20
+ // - { capability: skill, match: [memory-write, memory-search], effect: allow }
21
+ //
22
+ // Public surface:
23
+ // kiroWorkspaceHash(projectRoot) → string (the 16-hex workspace key)
24
+ // installKiroPermissions({ projectRoot, env? }) → { action, changed, path }
25
+ // uninstallKiroPermissions({ projectRoot, env? }) → { action, changed }
26
+ //
27
+ // Managed-merge: we own ONLY the rules whose capability+match are ours (matched
28
+ // by the kit's known tokens); a user's own rules are byte-preserved on install +
29
+ // uninstall (the over-mutation discipline).
30
+
31
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
32
+ import { homedir } from 'node:os';
33
+ import { join, dirname, resolve } from 'node:path';
34
+ import { createHash } from 'node:crypto';
35
+ import yaml from 'js-yaml';
36
+
37
+ import { MCP_AUTO_APPROVE } from './kiro-constants.mjs';
38
+
39
+ // The kit's MCP tools, namespaced as Kiro's permissions.yaml lists them
40
+ // (server/tool). MCP_AUTO_APPROVE is the shared source of the 11 tool names
41
+ // (also used for the IDE's mcp.json autoApprove), so the two never drift.
42
+ const MCP_MATCH = MCP_AUTO_APPROVE.map((t) => `claude-memory-kit/${t}`);
43
+ const SHELL_MATCH = Object.freeze(['cmd.exe /c cmk hook *', 'cmd.exe /c cmk-guard-memory*']);
44
+ const SKILL_MATCH = Object.freeze(['memory-write', 'memory-search']);
45
+
46
+ // The kit's owned MATCH ENTRIES, per capability+effect. Ownership is PER-ENTRY
47
+ // (not per-rule): Kiro stores ONE rule per (capability, effect) with a combined
48
+ // `match` array, so a user can co-locate their own match in the SAME rule as ours.
49
+ // We add/remove only OUR entries and never drop a co-located user entry (the
50
+ // over-mutation discipline — mirrors kiro-trusted-commands.mjs's per-entry filter,
51
+ // skill-review B1).
52
+ const OUR_MATCHES = Object.freeze({
53
+ shell: SHELL_MATCH,
54
+ mcp: MCP_MATCH,
55
+ skill: SKILL_MATCH,
56
+ });
57
+ function isOurMatch(capability, entry) {
58
+ const owned = OUR_MATCHES[capability];
59
+ return Array.isArray(owned) && owned.includes(entry);
60
+ }
61
+
62
+ // Merge our match entries into an existing rules array, PER-ENTRY + PER-(capability,
63
+ // effect:allow) rule. Preserves the user's rules + their co-located match entries;
64
+ // adds only the missing OUR entries; keeps rule order stable (in-place, no float).
65
+ function withOurRules(existing) {
66
+ // clone so we never mutate the parsed input
67
+ const rules = existing.map((r) => ({ ...r, match: Array.isArray(r.match) ? [...r.match] : r.match }));
68
+ for (const [capability, owned] of Object.entries(OUR_MATCHES)) {
69
+ // find the existing allow-rule for this capability (Kiro's convention: one per cap)
70
+ let rule = rules.find((r) => r && r.capability === capability && r.effect === 'allow' && Array.isArray(r.match));
71
+ if (!rule) {
72
+ rule = { capability, match: [], effect: 'allow' };
73
+ rules.push(rule);
74
+ }
75
+ for (const m of owned) if (!rule.match.includes(m)) rule.match.push(m);
76
+ }
77
+ return rules;
78
+ }
79
+
80
+ // Remove our match entries PER-ENTRY; drop a rule only if its match becomes empty
81
+ // AND it was an allow-rule for a capability we own (never delete a user's rule).
82
+ function withoutOurRules(existing) {
83
+ const out = [];
84
+ let changed = false;
85
+ for (const r of existing) {
86
+ if (!r || r.capability == null || r.effect !== 'allow' || !Array.isArray(r.match) || !OUR_MATCHES[r.capability]) {
87
+ out.push(r); // not a rule we touch
88
+ continue;
89
+ }
90
+ const kept = r.match.filter((m) => !isOurMatch(r.capability, m));
91
+ if (kept.length !== r.match.length) changed = true;
92
+ if (kept.length > 0) out.push({ ...r, match: kept }); // user entries survive
93
+ // else: the rule was ours-only → drop it
94
+ }
95
+ return { rules: out, changed };
96
+ }
97
+
98
+ /**
99
+ * Kiro IDE 1.0's workspace-roots hash for a project path.
100
+ * sha256(forward-slash + no-trailing-slash + lowercase).slice(0,16). (D-203h)
101
+ */
102
+ export function kiroWorkspaceHash(projectRoot) {
103
+ // resolve() a RELATIVE input to absolute (defense — Kiro keys on the absolute
104
+ // workspace root; a relative path would hash to a dir Kiro never reads, review
105
+ // M1). An ALREADY-absolute path (drive-letter `C:\…` or POSIX `/…`) is passed
106
+ // through verbatim — so a Windows path stays correct even when this runs on a
107
+ // POSIX CI (resolve() there would wrongly prepend cwd to `c:/…`). NOTE: only
108
+ // ordinary drive-letter paths are ground-truth-verified (D-203h); UNC/extended-
109
+ // length are untested.
110
+ const p = String(projectRoot);
111
+ const isAbsolute = /^[a-zA-Z]:[\\/]/.test(p) || /^[\\/]/.test(p);
112
+ const abs = isAbsolute ? p : resolve(p);
113
+ const norm = abs.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
114
+ return createHash('sha256').update(norm).digest('hex').slice(0, 16);
115
+ }
116
+
117
+ function permissionsPath(projectRoot, env) {
118
+ const home = env.USERPROFILE || env.HOME || homedir();
119
+ return join(home, '.kiro', 'workspace-roots', kiroWorkspaceHash(projectRoot), 'permissions.yaml');
120
+ }
121
+
122
+ // Parse an existing permissions.yaml → its rules array. A missing file → []. A
123
+ // MALFORMED file → { malformed: true } so the caller REFUSES to overwrite it
124
+ // (mirrors kiro-trusted-commands' don't-clobber-a-corrupt-file posture, review
125
+ // M3 — this writes to the user's HOME; never destroy content we couldn't parse).
126
+ function readRules(path) {
127
+ if (!existsSync(path)) return { rules: [], existed: false };
128
+ let parsed;
129
+ try {
130
+ parsed = yaml.load(readFileSync(path, 'utf8'));
131
+ } catch {
132
+ return { rules: [], existed: true, malformed: true };
133
+ }
134
+ // a parse that yields a non-object (e.g. a stray scalar) is also unsafe to clobber.
135
+ if (parsed != null && typeof parsed !== 'object') return { rules: [], existed: true, malformed: true };
136
+ const rules = parsed && Array.isArray(parsed.rules) ? parsed.rules : [];
137
+ return { rules, existed: true };
138
+ }
139
+
140
+ function serialize(rules) {
141
+ // js-yaml dump; flow-style for the compact match arrays Kiro uses is optional —
142
+ // block style is equally valid YAML and is what Kiro itself wrote.
143
+ return yaml.dump({ rules }, { lineWidth: -1, noRefs: true });
144
+ }
145
+
146
+ export function installKiroPermissions({ projectRoot, env = process.env } = {}) {
147
+ if (!projectRoot) throw new Error('installKiroPermissions: projectRoot is required');
148
+ const path = permissionsPath(projectRoot, env);
149
+ const { rules: existing, malformed } = readRules(path);
150
+ if (malformed) return { action: 'skipped', changed: false, path, reason: 'malformed-permissions-yaml' };
151
+ const merged = withOurRules(existing); // per-entry merge — preserves order + user entries
152
+ const serialized = serialize(merged);
153
+
154
+ const prior = existsSync(path) ? readFileSync(path, 'utf8') : null;
155
+ let changed = false;
156
+ if (prior !== serialized) {
157
+ mkdirSync(dirname(path), { recursive: true });
158
+ writeFileSync(path, serialized, 'utf8');
159
+ changed = true;
160
+ }
161
+ return { action: 'installed', changed, path };
162
+ }
163
+
164
+ export function uninstallKiroPermissions({ projectRoot, env = process.env } = {}) {
165
+ if (!projectRoot) throw new Error('uninstallKiroPermissions: projectRoot is required');
166
+ const path = permissionsPath(projectRoot, env);
167
+ if (!existsSync(path)) return { action: 'uninstalled', changed: false };
168
+ const { rules: existing } = readRules(path);
169
+ const { rules: kept, changed: removed } = withoutOurRules(existing); // per-entry removal
170
+ if (!removed) return { action: 'uninstalled', changed: false }; // none of ours present
171
+ // preserve the user's rules + co-located entries; write back without ours (never
172
+ // delete the file — it may hold the user's own + Kiro's migration data).
173
+ writeFileSync(path, serialize(kept), 'utf8');
174
+ return { action: 'uninstalled', changed: true };
175
+ }
@@ -0,0 +1,96 @@
1
+ // kiro-skills.mjs — install the kit's memory skills into Kiro (Task 50.I, skills leg).
2
+ //
3
+ // Kiro's skill surface is Claude-Code-style <name>/SKILL.md with YAML frontmatter
4
+ // (verified from real installs: .kiro/skills/<name>/SKILL.md project-tier,
5
+ // ~/.kiro/skills/<name>/SKILL.md user-tier). The kit already ships memory-search
6
+ // + memory-write as SKILL.md under template/.claude/skills/ — they port directly.
7
+ // The only transform: drop the Claude-Code-specific frontmatter keys Kiro doesn't
8
+ // honor (`context`, `allowed-tools`) while keeping `name` + `description` + the
9
+ // instruction body. The cmk MCP tools the skills reference are wired by the MCP
10
+ // leg, so the skill body works unchanged.
11
+ //
12
+ // Public surface:
13
+ // installKiroSkills({ projectRoot, templateDir? }) → { action, changed, skills }
14
+ // uninstallKiroSkills({ projectRoot }) → { action, changed, skills }
15
+
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { resolveTemplateDir } from './install.mjs';
19
+
20
+ // The kit skills that map to Kiro. (memory-search + memory-write — the two the
21
+ // kit scaffolds for Claude Code.)
22
+ const KIT_SKILLS = ['memory-search', 'memory-write'];
23
+
24
+ // Frontmatter keys Kiro does NOT use — dropped on translation. Everything else
25
+ // in the frontmatter (name, description) + the whole body is preserved.
26
+ const DROP_KEYS = new Set(['context', 'allowed-tools']);
27
+
28
+ export function installKiroSkills({ projectRoot, templateDir } = {}) {
29
+ if (!projectRoot) throw new Error('installKiroSkills: projectRoot is required');
30
+ const tpl = templateDir || resolveTemplateDir();
31
+ const skillsRoot = join(projectRoot, '.kiro', 'skills');
32
+
33
+ let changed = false;
34
+ const installed = [];
35
+ for (const name of KIT_SKILLS) {
36
+ const src = join(tpl, '.claude', 'skills', name, 'SKILL.md');
37
+ if (!existsSync(src)) continue; // skill not in this template — skip, don't fail
38
+ const translated = translateFrontmatter(readFileSync(src, 'utf8'));
39
+ const destDir = join(skillsRoot, name);
40
+ const dest = join(destDir, 'SKILL.md');
41
+
42
+ const existing = existsSync(dest) ? readFileSync(dest, 'utf8') : null;
43
+ if (existing !== translated) {
44
+ mkdirSync(destDir, { recursive: true });
45
+ writeFileSync(dest, translated, 'utf8');
46
+ changed = true;
47
+ }
48
+ installed.push(name);
49
+ }
50
+ return { action: 'installed', changed, skills: installed };
51
+ }
52
+
53
+ export function uninstallKiroSkills({ projectRoot } = {}) {
54
+ if (!projectRoot) throw new Error('uninstallKiroSkills: projectRoot is required');
55
+ const skillsRoot = join(projectRoot, '.kiro', 'skills');
56
+ let changed = false;
57
+ const removed = [];
58
+ for (const name of KIT_SKILLS) {
59
+ const dir = join(skillsRoot, name);
60
+ if (existsSync(dir)) {
61
+ rmSync(dir, { recursive: true, force: true });
62
+ changed = true;
63
+ removed.push(name);
64
+ }
65
+ }
66
+ return { action: 'uninstalled', changed, skills: removed };
67
+ }
68
+
69
+ // ── internal ─────────────────────────────────────────────────────────────────
70
+
71
+ // Drop the Claude-Code-only frontmatter keys, keep the rest of the frontmatter +
72
+ // the entire body. Operates on the leading `---` … `---` block only; the body is
73
+ // byte-preserved. Multi-line values (e.g. a wrapped description) are kept whole —
74
+ // a dropped key's continuation lines (indented) are dropped with it.
75
+ function translateFrontmatter(content) {
76
+ const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
77
+ if (!m) return content; // no frontmatter — pass through unchanged
78
+ const [, fm, body] = m;
79
+
80
+ const lines = fm.split('\n');
81
+ const kept = [];
82
+ let dropping = false;
83
+ for (const line of lines) {
84
+ const keyMatch = line.match(/^([A-Za-z0-9_-]+):/);
85
+ if (keyMatch) {
86
+ // a new top-level key — decide whether to drop it (+ its continuation)
87
+ dropping = DROP_KEYS.has(keyMatch[1]);
88
+ if (!dropping) kept.push(line);
89
+ } else if (!dropping) {
90
+ // continuation line of a kept key (indented / blank) — preserve
91
+ kept.push(line);
92
+ }
93
+ // else: continuation of a dropped key — skip
94
+ }
95
+ return `---\n${kept.join('\n')}\n---\n${body}`;
96
+ }