@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,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
|
+
}
|