@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,220 @@
|
|
|
1
|
+
// install-agent.mjs — wire a per-agent profile's legs into a project (Task 50.E/50.F).
|
|
2
|
+
//
|
|
3
|
+
// Given a profile (DATA from agent-profiles.mjs) + a project root, this lands the
|
|
4
|
+
// profile's three legs in its DECLARED paths, reusing the kit's shared primitives:
|
|
5
|
+
// - MCP registration → mutateAgentConfig (touch-only-our-keys, refuse-on-parse-error)
|
|
6
|
+
// - hook entry → mutateAgentConfig (the agent's hook-config file)
|
|
7
|
+
// - instruction file → a managed marker block (byte-preserving install/uninstall)
|
|
8
|
+
//
|
|
9
|
+
// This is the per-agent path. install.mjs keeps its existing Claude-Code wiring
|
|
10
|
+
// for the default `--ide claude-code` route (regression-proof, D-180 / 50.E); this
|
|
11
|
+
// module handles the OTHER agents (Kiro first). The kit's core — store, compression,
|
|
12
|
+
// search, CLI, MCP server — is identical across agents; only these legs differ.
|
|
13
|
+
//
|
|
14
|
+
// Public surface:
|
|
15
|
+
// installAgent({ projectRoot, profile }) → { action, agent, changed, legs, errors? }
|
|
16
|
+
// uninstallAgent({ projectRoot, profile }) → { action, agent, changed }
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { mutateAgentConfig, atomicWrite } from './mutate-agent-config.mjs';
|
|
21
|
+
import {
|
|
22
|
+
removeJsonKey,
|
|
23
|
+
pruneEmptyParent,
|
|
24
|
+
escapeRe,
|
|
25
|
+
trimLeadingNewlines,
|
|
26
|
+
trimTrailingNewlines,
|
|
27
|
+
} from './managed-block.mjs';
|
|
28
|
+
|
|
29
|
+
// The kit's MCP server entry — same shape settings-hooks.mjs writes for Claude
|
|
30
|
+
// Code (`cmk mcp serve` over stdio). Agent-neutral: every agent registers the
|
|
31
|
+
// same server; only the FILE it goes in differs (profile.mcp.path).
|
|
32
|
+
const MCP_ENTRY = Object.freeze({ type: 'stdio', command: 'cmk', args: ['mcp', 'serve'] });
|
|
33
|
+
const MCP_SERVER_NAME = 'claude-memory-kit';
|
|
34
|
+
|
|
35
|
+
// The kit's lifecycle-hook commands, keyed by the ABSTRACT event the profile's
|
|
36
|
+
// eventMap translates to the agent's concrete event name. Only the events an
|
|
37
|
+
// agent supports (present in its eventMap) get wired.
|
|
38
|
+
const HOOK_COMMANDS = Object.freeze({
|
|
39
|
+
sessionStart: 'cmk-inject-context',
|
|
40
|
+
promptSubmit: 'cmk-capture-prompt',
|
|
41
|
+
postEdit: 'cmk-observe-edit',
|
|
42
|
+
turnEnd: 'cmk-capture-turn',
|
|
43
|
+
sessionEnd: 'cmk-compress-session',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Steering / instruction-file body. Kiro reads `inclusion: always` frontmatter
|
|
47
|
+
// to keep this in context every session (kiro.dev primary-verified). The body is
|
|
48
|
+
// intentionally short — it points the agent at the kit's recall surface; the rich
|
|
49
|
+
// memory lives in context/.
|
|
50
|
+
const INSTRUCTION_BODY = [
|
|
51
|
+
'# claude-memory-kit',
|
|
52
|
+
'',
|
|
53
|
+
'This project uses claude-memory-kit for durable, in-repo memory across sessions.',
|
|
54
|
+
'Recall before re-deriving: run `cmk search "<topic>"` for prior decisions, preferences,',
|
|
55
|
+
'and project facts; the curated tiers live under `context/`. Capture durable facts with',
|
|
56
|
+
'`cmk remember` — never hand-edit the memory files.',
|
|
57
|
+
].join('\n');
|
|
58
|
+
|
|
59
|
+
const MARK_START = '<!-- claude-memory-kit:start -->';
|
|
60
|
+
const MARK_END = '<!-- claude-memory-kit:end -->';
|
|
61
|
+
|
|
62
|
+
export function installAgent({ projectRoot, profile }) {
|
|
63
|
+
if (!projectRoot) throw new Error('installAgent: projectRoot is required');
|
|
64
|
+
if (!profile || !profile.name) throw new Error('installAgent: a profile is required');
|
|
65
|
+
|
|
66
|
+
const legs = {};
|
|
67
|
+
const errors = [];
|
|
68
|
+
let changed = false;
|
|
69
|
+
|
|
70
|
+
// ── MCP leg ───────────────────────────────────────────────────────────────
|
|
71
|
+
if (profile.mcp) {
|
|
72
|
+
const r = mutateAgentConfig({
|
|
73
|
+
path: join(projectRoot, profile.mcp.path),
|
|
74
|
+
format: 'json',
|
|
75
|
+
keyPath: [profile.mcp.serversKey, MCP_SERVER_NAME],
|
|
76
|
+
entry: MCP_ENTRY,
|
|
77
|
+
});
|
|
78
|
+
legs.mcp = r.action;
|
|
79
|
+
if (r.action === 'error') errors.push({ leg: 'mcp', ...r });
|
|
80
|
+
else if (r.changed) changed = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── hooks leg ───────────────────────────────────────────────────────────────
|
|
84
|
+
// Only if MCP didn't already error (refuse-to-clobber means a corrupt config
|
|
85
|
+
// should halt this agent's install — report, don't push past it).
|
|
86
|
+
if (profile.hooks && errors.length === 0) {
|
|
87
|
+
const hookEntry = buildHookEntry(profile);
|
|
88
|
+
const r = mutateAgentConfig({
|
|
89
|
+
path: join(projectRoot, profile.hooks.path),
|
|
90
|
+
format: 'json',
|
|
91
|
+
keyPath: ['hooks'],
|
|
92
|
+
entry: hookEntry,
|
|
93
|
+
});
|
|
94
|
+
legs.hooks = r.action;
|
|
95
|
+
if (r.action === 'error') errors.push({ leg: 'hooks', ...r });
|
|
96
|
+
else if (r.changed) changed = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── instruction leg (managed marker block) ──────────────────────────────────
|
|
100
|
+
if (errors.length === 0) {
|
|
101
|
+
const instrPath = join(projectRoot, profile.instructionFile);
|
|
102
|
+
const r = writeInstructionFile(instrPath, profile);
|
|
103
|
+
legs.instruction = r.action;
|
|
104
|
+
if (r.changed) changed = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
return { action: 'error', agent: profile.name, changed, legs, errors };
|
|
109
|
+
}
|
|
110
|
+
return { action: 'installed', agent: profile.name, changed, legs };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function uninstallAgent({ projectRoot, profile }) {
|
|
114
|
+
if (!projectRoot) throw new Error('uninstallAgent: projectRoot is required');
|
|
115
|
+
if (!profile || !profile.name) throw new Error('uninstallAgent: a profile is required');
|
|
116
|
+
|
|
117
|
+
let changed = false;
|
|
118
|
+
|
|
119
|
+
// MCP: remove only our server key, preserve siblings.
|
|
120
|
+
if (profile.mcp) {
|
|
121
|
+
const p = join(projectRoot, profile.mcp.path);
|
|
122
|
+
if (removeJsonKey(p, [profile.mcp.serversKey, MCP_SERVER_NAME])) changed = true;
|
|
123
|
+
// prune an emptied servers object we leave behind (no kit-shaped residue).
|
|
124
|
+
pruneEmptyParent(p, [profile.mcp.serversKey]);
|
|
125
|
+
}
|
|
126
|
+
// hooks: remove ONLY the event keys WE wrote (symmetry with the MCP leg —
|
|
127
|
+
// remove our keys, preserve any the user added to the same `hooks` object).
|
|
128
|
+
// This is safe even if a future profile points hooks.path at a shared file.
|
|
129
|
+
if (profile.hooks) {
|
|
130
|
+
const p = join(projectRoot, profile.hooks.path);
|
|
131
|
+
const ourEvents = Object.keys(buildHookEntry(profile));
|
|
132
|
+
for (const ev of ourEvents) {
|
|
133
|
+
if (removeJsonKey(p, ['hooks', ev])) changed = true;
|
|
134
|
+
}
|
|
135
|
+
// prune an emptied `hooks` object we leave behind (no residue).
|
|
136
|
+
pruneEmptyParent(p, ['hooks']);
|
|
137
|
+
}
|
|
138
|
+
// instruction: strip our marker block, byte-preserve the rest.
|
|
139
|
+
const instrPath = join(projectRoot, profile.instructionFile);
|
|
140
|
+
if (removeInstructionBlock(instrPath)) changed = true;
|
|
141
|
+
|
|
142
|
+
return { action: 'uninstalled', agent: profile.name, changed };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── internal helpers ───────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
// Build the agent's hook object: { <concreteEvent>: [ {command} ] } for each
|
|
148
|
+
// abstract event the profile maps + the kit has a command for.
|
|
149
|
+
function buildHookEntry(profile) {
|
|
150
|
+
const hooks = {};
|
|
151
|
+
for (const [abstractEvent, concreteEvent] of Object.entries(profile.hooks.eventMap)) {
|
|
152
|
+
const command = HOOK_COMMANDS[abstractEvent];
|
|
153
|
+
if (command) hooks[concreteEvent] = [{ command }];
|
|
154
|
+
}
|
|
155
|
+
return hooks;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Write the instruction file with a managed marker block + agent-specific
|
|
159
|
+
// frontmatter (Kiro wants `inclusion: always`). Idempotent: re-writing identical
|
|
160
|
+
// content reports changed:false.
|
|
161
|
+
function writeInstructionFile(path, profile) {
|
|
162
|
+
const frontmatter = needsInclusionFrontmatter(profile) ? '---\ninclusion: always\n---\n\n' : '';
|
|
163
|
+
const block = `${MARK_START}\n${INSTRUCTION_BODY}\n${MARK_END}`;
|
|
164
|
+
const desired = `${frontmatter}${block}\n`;
|
|
165
|
+
|
|
166
|
+
let existing = '';
|
|
167
|
+
if (existsSync(path)) existing = readFileSync(path, 'utf8');
|
|
168
|
+
|
|
169
|
+
let next;
|
|
170
|
+
if (existing === '') {
|
|
171
|
+
next = desired;
|
|
172
|
+
} else if (existing.includes(MARK_START) && existing.includes(MARK_END)) {
|
|
173
|
+
// refresh in place — replace only the managed block, byte-preserve the rest.
|
|
174
|
+
next = existing.replace(
|
|
175
|
+
new RegExp(`${escapeRe(MARK_START)}[\\s\\S]*?${escapeRe(MARK_END)}`),
|
|
176
|
+
block,
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
// append our block to the user's existing file.
|
|
180
|
+
next = `${trimTrailingNewlines(existing)}\n\n${block}\n`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (next === existing) return { action: 'unchanged', changed: false };
|
|
184
|
+
atomicWrite(path, next);
|
|
185
|
+
return { action: existing === '' ? 'created' : 'updated', changed: true };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function removeInstructionBlock(path) {
|
|
189
|
+
if (!existsSync(path)) return false;
|
|
190
|
+
const existing = readFileSync(path, 'utf8');
|
|
191
|
+
if (!existing.includes(MARK_START)) return false;
|
|
192
|
+
// Strip our block (+ surrounding blank lines). The inner [\s\S]*? is lazy +
|
|
193
|
+
// bounded by two fixed literal delimiters (no nested quantifier) → linear, not
|
|
194
|
+
// ReDoS-prone. The newline trims are done WITHOUT regex (no `\n*$`/`^\n+`
|
|
195
|
+
// super-linear shapes) — see trimLeadingNewlines/trimTrailingNewlines.
|
|
196
|
+
const blockRe = new RegExp(`${escapeRe(MARK_START)}[\\s\\S]*?${escapeRe(MARK_END)}`);
|
|
197
|
+
const withoutBlock = existing.replace(blockRe, '');
|
|
198
|
+
const stripped = trimLeadingNewlines(collapseBlankRun(withoutBlock));
|
|
199
|
+
atomicWrite(path, stripped);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// removeJsonKey / pruneEmptyParent / escapeRe / trim{Leading,Trailing}Newlines
|
|
204
|
+
// are now shared from managed-block.mjs (deduped — the kit's shared-module
|
|
205
|
+
// discipline; install-kiro.mjs uses the same source).
|
|
206
|
+
|
|
207
|
+
function needsInclusionFrontmatter(profile) {
|
|
208
|
+
// Kiro steering files use `inclusion: always`. Driven by the profile's
|
|
209
|
+
// instruction path living under a steering dir — kept simple for v0.4.0
|
|
210
|
+
// (only Kiro needs it); generalize when a second steering-style agent lands.
|
|
211
|
+
return profile.instructionFile.includes('/steering/') || profile.name === 'kiro';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Collapse a run of 2+ blank lines left where our block was removed into a
|
|
215
|
+
// single newline, so an uninstall doesn't leave a widening gap.
|
|
216
|
+
function collapseBlankRun(s) {
|
|
217
|
+
// split on newlines, drop empty segments created by the removed block's
|
|
218
|
+
// surrounding blanks, rejoin — no regex, no backtracking.
|
|
219
|
+
return s.replace(/\n{3,}/g, '\n\n');
|
|
220
|
+
}
|
|
@@ -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 (
|
|
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 });
|
|
@@ -481,7 +494,12 @@ export async function install(options = {}) {
|
|
|
481
494
|
// the pin-off; checked first below).
|
|
482
495
|
let semantic = { action: 'skipped' };
|
|
483
496
|
if (options.withSemantic) {
|
|
484
|
-
semantic = await enableSemantic({
|
|
497
|
+
semantic = await enableSemantic({
|
|
498
|
+
projectRoot,
|
|
499
|
+
spawnNpm: options.spawnNpm,
|
|
500
|
+
warm: options.warmEmbedder,
|
|
501
|
+
probeEmbedder: options.probeEmbedder,
|
|
502
|
+
});
|
|
485
503
|
if (semantic.action === 'error') errors.push({ path: 'semantic', error: semantic.error });
|
|
486
504
|
} else if (options.noSemantic) {
|
|
487
505
|
const r = mergeProjectSettings(projectRoot, { search: { default_mode: 'keyword' } });
|
|
@@ -558,15 +576,43 @@ export function buildDefaultNpmRunner({ spawnSyncImpl = spawnSync } = {}) {
|
|
|
558
576
|
};
|
|
559
577
|
}
|
|
560
578
|
|
|
561
|
-
async function enableSemantic({ projectRoot, spawnNpm, warm }) {
|
|
579
|
+
async function enableSemantic({ projectRoot, spawnNpm, warm, probeEmbedder }) {
|
|
562
580
|
// 1. Install the optional embedder globally (it resolves as a sibling of
|
|
563
581
|
// the globally-installed kit). Injectable for tests.
|
|
564
582
|
const runNpm = spawnNpm ?? buildDefaultNpmRunner();
|
|
565
583
|
const npm = runNpm();
|
|
566
|
-
|
|
584
|
+
|
|
585
|
+
// Task 170 (the v0.4.1 cut-gate find): gate on whether the embedder ACTUALLY
|
|
586
|
+
// IMPORTS, NOT on npm's exit code — verify the thing worked, not the command's
|
|
587
|
+
// exit (the D-199 class). Two failure modes the exit code gets wrong, BOTH
|
|
588
|
+
// sides:
|
|
589
|
+
// - npm exits NON-ZERO but the package installed fine: on Windows a benign
|
|
590
|
+
// cleanup-EBUSY (npm failing to unlink a leftover temp DLL still locked by
|
|
591
|
+
// a running process — sharp-win32-x64 / libvips) makes npm exit non-zero
|
|
592
|
+
// AFTER a successful install. Trusting the exit FALSELY reported "NOT
|
|
593
|
+
// enabled" while the embedder was present + importable (the live find).
|
|
594
|
+
// - npm exits ZERO but the import is BROKEN (partial/corrupt native module):
|
|
595
|
+
// trusting the exit would wrongly write a hybrid default with no working
|
|
596
|
+
// embedder — every search would degrade to the fallback warning (the
|
|
597
|
+
// half-state this function exists to avoid).
|
|
598
|
+
// So PROBE the import once, and enable hybrid IFF it imports — regardless of
|
|
599
|
+
// npm's exit. The install must be AUTOMATIC: the user does nothing and never
|
|
600
|
+
// needs a manual `cmk config set` to recover from a benign npm warning.
|
|
601
|
+
const probe = probeEmbedder ?? (async () => {
|
|
602
|
+
const { checkEmbedderBinding } = await import('./native-binding.mjs');
|
|
603
|
+
return checkEmbedderBinding();
|
|
604
|
+
});
|
|
605
|
+
let imported;
|
|
606
|
+
try {
|
|
607
|
+
imported = await probe();
|
|
608
|
+
} catch {
|
|
609
|
+
imported = { ok: false };
|
|
610
|
+
}
|
|
611
|
+
if (!imported?.ok) {
|
|
612
|
+
const detail = npm.status !== 0 ? (npm.error ?? `exit ${npm.status}`) : (imported?.reason ?? 'embedder import failed');
|
|
567
613
|
return {
|
|
568
614
|
action: 'error',
|
|
569
|
-
error: `
|
|
615
|
+
error: `semantic embedder not usable after install (${detail}) — semantic recall NOT enabled; keyword search is unaffected`,
|
|
570
616
|
};
|
|
571
617
|
}
|
|
572
618
|
// 2. Flip the project default to hybrid ONLY after the dependency landed
|