@yugabytedb/meko-mcp 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +183 -33
  2. package/assets/hooks-handlers/codex-session-end.sh +13 -0
  3. package/assets/hooks-handlers/cursor-session-start.sh +19 -1
  4. package/assets/hooks-handlers/lib/capture-notices.js +321 -0
  5. package/assets/hooks-handlers/lib/capture-state.js +2079 -0
  6. package/assets/hooks-handlers/lib/capture-status-cli.js +82 -0
  7. package/assets/hooks-handlers/lib/capture.js +1653 -225
  8. package/assets/hooks-handlers/lib/checkpoint-timer.js +455 -50
  9. package/assets/hooks-handlers/lib/kiro-handler.js +738 -0
  10. package/assets/hooks-handlers/lib/terminal-handoff.js +418 -0
  11. package/assets/hooks-handlers/session-end.sh +3 -24
  12. package/assets/hooks-handlers/session-start.sh +8 -1
  13. package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
  14. package/assets/scripts/meko-statusline.sh +41 -1
  15. package/assets/scripts/package-desktop-skill.sh +32 -1
  16. package/assets/skills/meko-mcp-tools/SKILL.md +90 -137
  17. package/assets/skills/meko-mcp-tools/references/_sections.md +3 -5
  18. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +31 -28
  19. package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
  20. package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
  21. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +69 -49
  22. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -6
  23. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +29 -5
  24. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +29 -92
  25. package/assets/skills/meko-mcp-tools/references/tools-overview.md +42 -40
  26. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +1 -2
  27. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +18 -11
  28. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +10 -9
  29. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +51 -44
  30. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +6 -6
  31. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +34 -25
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +63 -37
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +4 -4
  36. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +19 -22
  37. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +42 -40
  38. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +1 -2
  39. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -17
  40. package/assets/skills/meko-select-datapack-desktop/SKILL.md +31 -21
  41. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
  42. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
  43. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +1 -1
  44. package/bin/create-meko-setup.mjs +93 -16
  45. package/lib/capture-status.mjs +67 -0
  46. package/lib/clients/aliases.mjs +5 -0
  47. package/lib/clients/claude-code.mjs +8 -4
  48. package/lib/clients/claude-desktop.mjs +133 -4
  49. package/lib/clients/codex.mjs +29 -11
  50. package/lib/clients/common.mjs +2 -0
  51. package/lib/clients/cursor.mjs +7 -2
  52. package/lib/clients/index.mjs +2 -0
  53. package/lib/clients/kiro.mjs +705 -0
  54. package/lib/config.mjs +90 -0
  55. package/lib/detect.mjs +7 -2
  56. package/lib/installer.mjs +1 -0
  57. package/lib/migrate/adapters/codex.mjs +490 -0
  58. package/lib/migrate/id.mjs +28 -3
  59. package/lib/migrate/orchestrator.mjs +34 -7
  60. package/lib/migrate.mjs +4 -2
  61. package/lib/prompt.mjs +17 -2
  62. package/lib/url-selection.mjs +55 -0
  63. package/package.json +4 -4
  64. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -71
  65. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +0 -71
@@ -0,0 +1,705 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ readlinkSync,
5
+ mkdirSync,
6
+ writeFileSync,
7
+ readFileSync,
8
+ rmSync,
9
+ chmodSync,
10
+ } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join, resolve } from "node:path";
13
+
14
+ import {
15
+ getKiroMcpPath,
16
+ getKiroSkillsDir,
17
+ getKiroSteeringPath,
18
+ getKiroHooksDir,
19
+ getKiroManagedAgentPath,
20
+ getKiroCliSettingsPath,
21
+ readJsonFile,
22
+ writeJsonFile,
23
+ } from "../config.mjs";
24
+ import {
25
+ MEKO_MCP_USER_AGENT,
26
+ getInstallerVersion,
27
+ installStableTree,
28
+ isDanglingSymlink,
29
+ isPackagedInstall,
30
+ markerIsStale,
31
+ readInstallerMarker,
32
+ resolveHooksRoot,
33
+ resolveSkillSource,
34
+ removePath,
35
+ symlinkOrCopy,
36
+ writeInstallerMarker,
37
+ } from "./common.mjs";
38
+
39
+ const SKILLS = [{ name: "meko-mcp-tools" }];
40
+ const MANAGED_AGENT_NAME = "meko-default";
41
+ const MANAGED_MARKER = "Managed by meko installer";
42
+ const STANDALONE_HOOKS = [
43
+ { file: "meko-session-start.json", trigger: "SessionStart", event: "SessionStart", timeout: 15 },
44
+ { file: "meko-user-prompt-submit.json", trigger: "UserPromptSubmit", event: "UserPromptSubmit", timeout: 5 },
45
+ { file: "meko-stop.json", trigger: "Stop", event: "Stop", timeout: 2 },
46
+ ];
47
+
48
+ /**
49
+ * Adapter for Kiro (AWS) — covers both the Kiro IDE and `kiro-cli`, which read
50
+ * the same `mcpServers` config files and the same skills directories.
51
+ *
52
+ * Kiro accepts the remote `{ url, headers }` MCP shape natively, so — unlike
53
+ * Claude Desktop — Meko registers the endpoint directly with an
54
+ * `Authorization: Bearer` header. That matters for more than tidiness: routing
55
+ * Kiro through the `mcp-remote` stdio bridge never presents the API key at all
56
+ * (the bridge forwards only headers passed explicitly via `--header`), so it
57
+ * connects unauthenticated, takes a 401, and falls through to OAuth dynamic
58
+ * client registration. Both the IDE and `kiro-cli` then die on an opaque HTML
59
+ * 403: each registers a loopback callback, and the Cloud WAF's SSRF ruleset
60
+ * refuses internal hosts (localhost, 127.0.0.1, RFC1918, link-local) in a
61
+ * redirect_uri regardless of scheme. Header auth sidesteps OAuth entirely.
62
+ *
63
+ * Kiro 2.18.1 contains two hook generations. CLI 2.x reads lowercase inline
64
+ * hooks from the selected custom agent; CLI 3.x and IDE 1.x read standalone
65
+ * PascalCase hook files. The installer writes both and routes them through one
66
+ * durable event journal. Duplicate delivery is expected; when one format omits
67
+ * `session_id`, the handler correlates it with the other format by host PID and
68
+ * cwd before deduplicating the turn and starting Capture v2.
69
+ *
70
+ * Steering remains as a safety net. The handler injects a
71
+ * MEKO_CAPTURE_SESSION_STARTED marker when hooks run; without that marker the
72
+ * user may be in an untrusted IDE workspace, have hooks disabled, or have
73
+ * selected a different CLI 2.x agent. In those cases steering asks the agent to
74
+ * use proactive memory writes instead of assuming capture is active.
75
+ *
76
+ * @returns {object}
77
+ */
78
+ export function createKiroAdapter(customPaths = {}) {
79
+ const resolveHooksRootPath = customPaths.resolveHooksRoot ?? resolveHooksRoot;
80
+ const resolveSkillSourcePath = customPaths.resolveSkillSource ?? resolveSkillSource;
81
+ const packagedInstallDetector = customPaths.isPackagedInstall ?? isPackagedInstall;
82
+ const platform = customPaths.platform ?? process.platform;
83
+ const paths = {
84
+ getMcpPath: customPaths.getMcpPath ?? getKiroMcpPath,
85
+ getSkillsDir: customPaths.getSkillsDir ?? getKiroSkillsDir,
86
+ getSteeringPath: customPaths.getSteeringPath ?? getKiroSteeringPath,
87
+ getHooksDir: customPaths.getHooksDir ?? getKiroHooksDir,
88
+ getManagedAgentPath:
89
+ customPaths.getManagedAgentPath ?? getKiroManagedAgentPath,
90
+ getCliSettingsPath:
91
+ customPaths.getCliSettingsPath ?? getKiroCliSettingsPath,
92
+ };
93
+
94
+ return {
95
+ id: "kiro",
96
+ displayName: "Kiro",
97
+ settingsPath() {
98
+ return "";
99
+ },
100
+ async detectPrerequisites() {
101
+ // Config-mode install: nothing is shelled out to, so there is no CLI to
102
+ // probe. Kiro reads mcp.json on save/launch.
103
+ },
104
+ /**
105
+ * Is Kiro actually installed? Both the IDE and `kiro-cli` create the global
106
+ * config tree on first run, so its existence means Kiro has run at least
107
+ * once here. $KIRO_HOME is honored because Kiro resolves every global
108
+ * surface against it. A user who installed Kiro but never opened it is a
109
+ * false negative — acceptable; they'd pass --client kiro explicitly.
110
+ */
111
+ isInstalled() {
112
+ return existsSync(process.env.KIRO_HOME || join(homedir(), ".kiro"));
113
+ },
114
+ detectExisting(name, { scope, projectRoot }) {
115
+ const path = paths.getMcpPath(scope, projectRoot);
116
+ const cfg = readJsonFile(path);
117
+ const entry = cfg?.mcpServers?.[name];
118
+ const found = Boolean(entry);
119
+ return {
120
+ found,
121
+ status: found ? "Configured" : "",
122
+ ...(typeof entry?.url === "string" ? { url: entry.url } : {}),
123
+ };
124
+ },
125
+ removeRegistration(name, { scope, projectRoot }) {
126
+ const mcpPath = paths.getMcpPath(scope, projectRoot);
127
+ const cfg = readJsonFile(mcpPath);
128
+ if (!cfg?.mcpServers?.[name]) return;
129
+ delete cfg.mcpServers[name];
130
+ writePrivateKiroConfig(mcpPath, cfg);
131
+ },
132
+ registerServer({ name, url, apiKey, dryRun, log, scope, projectRoot }) {
133
+ const mcpPath = paths.getMcpPath(scope, projectRoot);
134
+ if (dryRun) {
135
+ log.dim(` Would update ${mcpPath}:`);
136
+ log.dim(` mcpServers.${name}.url = ${url}`);
137
+ log.dim(` mcpServers.${name}.headers.User-Agent = ${MEKO_MCP_USER_AGENT}`);
138
+ if (apiKey) {
139
+ log.dim(" mcpServers.<name>.headers.Authorization = <set>");
140
+ }
141
+ return;
142
+ }
143
+
144
+ const cfg = readJsonFile(mcpPath);
145
+ if (!cfg.mcpServers || typeof cfg.mcpServers !== "object") {
146
+ cfg.mcpServers = {};
147
+ }
148
+ const server = { url, headers: { "User-Agent": MEKO_MCP_USER_AGENT } };
149
+ if (apiKey) {
150
+ server.headers.Authorization = `Bearer ${apiKey}`;
151
+ }
152
+ cfg.mcpServers[name] = server;
153
+ writePrivateKiroConfig(mcpPath, cfg);
154
+ },
155
+ setHookEnvironment({ url, apiKey, name = "meko", dryRun, log, scope, projectRoot }) {
156
+ const handlersRoot = getKiroHooksInstallRoot(paths, scope, projectRoot);
157
+ const hooksDir = paths.getHooksDir(scope, projectRoot);
158
+ const agentPath = paths.getManagedAgentPath(scope, projectRoot);
159
+ const cliSettingsPath = paths.getCliSettingsPath(scope, projectRoot);
160
+ if (dryRun) {
161
+ log.dim(` Would install Kiro capture assets into ${handlersRoot}`);
162
+ log.dim(` Would write standalone SessionStart/UserPromptSubmit/Stop hooks into ${hooksDir}`);
163
+ log.dim(` Would write managed CLI 2.x agent ${agentPath}`);
164
+ log.dim(` Would activate ${MANAGED_AGENT_NAME} in ${cliSettingsPath}`);
165
+ log.dim(" Would write hook environment with MEKO_MCP_URL and redacted credentials (mode 0600)");
166
+ return;
167
+ }
168
+
169
+ const priorInstallState = readJsonFile(join(handlersRoot, "kiro-install-state.json"));
170
+ if (existsSync(handlersRoot) && !readInstallerMarker(handlersRoot)) {
171
+ log.dim(` Skipping Kiro capture install: ${handlersRoot} is user-managed.`);
172
+ return;
173
+ }
174
+ installStableTree(resolveHooksRootPath(), handlersRoot);
175
+ writeInstallerMarker(handlersRoot);
176
+ writeKiroHookEnvironment(handlersRoot, { url, apiKey });
177
+ if (isKiroInstallState(priorInstallState)) {
178
+ // installStableTree replaces the runtime directory. Preserve the only
179
+ // copy of the original default even if an edited managed-agent file is
180
+ // now treated as a user-owned collision and cannot be reactivated.
181
+ writePrivateJson(join(handlersRoot, "kiro-install-state.json"), priorInstallState);
182
+ }
183
+ installStandaloneKiroHooks({ hooksDir, handlersRoot, log, platform });
184
+ const agentInstalled = installManagedKiroAgent({
185
+ agentPath,
186
+ handlersRoot,
187
+ log,
188
+ name,
189
+ platform,
190
+ });
191
+ if (agentInstalled) {
192
+ activateManagedKiroAgent({
193
+ cliSettingsPath,
194
+ handlersRoot,
195
+ priorInstallState,
196
+ });
197
+ }
198
+ },
199
+ installSkills({ dryRun, log, scope, projectRoot, name }) {
200
+ const skillDir = paths.getSkillsDir(scope, projectRoot);
201
+ const steeringPath = paths.getSteeringPath(scope, projectRoot);
202
+ const currentVersion = getInstallerVersion();
203
+ if (dryRun) {
204
+ log.dim(` Would install skills into ${skillDir}`);
205
+ log.dim(` Would write .meko-installer marker (version=${currentVersion})`);
206
+ log.dim(` Would write steering file ${steeringPath}`);
207
+ return { marketplace: "skipped", skills: "dry-run" };
208
+ }
209
+
210
+ mkdirSync(skillDir, { recursive: true });
211
+ for (const skill of SKILLS) {
212
+ const source = resolveSkillSourcePath(skill.name);
213
+ const destination = join(skillDir, skill.name);
214
+
215
+ if (existsSync(destination)) {
216
+ // A symlink into our own source tree is already live content.
217
+ if (isOwnSymlink(destination, source)) continue;
218
+
219
+ const marker = readInstallerMarker(destination);
220
+ if (!marker) {
221
+ // Not ours: a user-authored skill or a pre-marker install. Leave it
222
+ // rather than clobber local edits.
223
+ log.dim(
224
+ ` Skipping ${destination}: no .meko-installer marker (user-managed).`,
225
+ );
226
+ continue;
227
+ }
228
+ if (markerIsStale(destination, currentVersion)) {
229
+ const from = marker.version ?? "pre-1.0.9";
230
+ log.dim(` Upgrading kiro skill from ${from} -> ${currentVersion}`);
231
+ removePath(destination);
232
+ } else {
233
+ continue;
234
+ }
235
+ } else if (isDanglingSymlink(destination)) {
236
+ // Left by an earlier install whose target (e.g. a deleted worktree)
237
+ // is gone. existsSync is false for these, so the branch above misses
238
+ // them; clear it so the fresh-install path can replace it.
239
+ log.dim?.(
240
+ ` Replacing dangling symlink at ${destination} (target missing).`,
241
+ );
242
+ removePath(destination);
243
+ }
244
+
245
+ const strategy = symlinkOrCopy(source, destination, {
246
+ preferCopy: packagedInstallDetector(),
247
+ });
248
+ // Only stamp a marker on a real copy — writing into a symlinked source
249
+ // would mutate the shipped asset tree.
250
+ if (strategy === "copy") {
251
+ writeInstallerMarker(destination);
252
+ }
253
+ }
254
+
255
+ // Counterweight to the skill's hooks-assume-capture doctrine. Written
256
+ // last so a skill-copy failure doesn't leave steering claiming a skill
257
+ // that isn't there.
258
+ const existingSteering = existsSync(steeringPath)
259
+ ? readFileSync(steeringPath, "utf8")
260
+ : "";
261
+ if (existingSteering && !existingSteering.includes(MANAGED_MARKER)) {
262
+ log.dim(` Skipping ${steeringPath}: user-managed steering collision.`);
263
+ } else {
264
+ mkdirSync(dirname(steeringPath), { recursive: true });
265
+ writeFileSync(
266
+ steeringPath,
267
+ renderSteeringDoc(currentVersion, name ?? "meko"),
268
+ "utf8",
269
+ );
270
+ }
271
+
272
+ return { marketplace: "skipped", skills: "ok" };
273
+ },
274
+ uninstall({ name, scope, projectRoot }) {
275
+ const results = [];
276
+ try {
277
+ this.removeRegistration(name, { scope, projectRoot });
278
+ results.push({
279
+ key: "mcp",
280
+ ok: true,
281
+ message: `Kiro MCP server "${name}" removed.`,
282
+ });
283
+ } catch (err) {
284
+ results.push({
285
+ key: "mcp",
286
+ ok: false,
287
+ message: `Could not remove Kiro MCP server: ${err.message}`,
288
+ });
289
+ }
290
+
291
+ try {
292
+ uninstallKiroCapture({ scope, projectRoot, paths, resolveHooksRootPath });
293
+ results.push({
294
+ key: "hooks",
295
+ ok: true,
296
+ message: "Kiro hook configuration and managed agent cleaned up.",
297
+ });
298
+ } catch (err) {
299
+ results.push({
300
+ key: "hooks",
301
+ ok: false,
302
+ message: `Could not clean Kiro hooks: ${err.message}`,
303
+ });
304
+ }
305
+
306
+ try {
307
+ uninstallKiroSkills({ scope, projectRoot, paths, resolveSkillSourcePath });
308
+ results.push({ key: "skills", ok: true, message: "Kiro skills cleaned up." });
309
+ } catch (err) {
310
+ results.push({
311
+ key: "skills",
312
+ ok: false,
313
+ message: `Could not remove Kiro skills: ${err.message}`,
314
+ });
315
+ }
316
+
317
+ try {
318
+ removeKiroSteering(paths.getSteeringPath(scope, projectRoot));
319
+ results.push({
320
+ key: "steering",
321
+ ok: true,
322
+ message: "Kiro steering file cleaned up.",
323
+ });
324
+ } catch (err) {
325
+ results.push({
326
+ key: "steering",
327
+ ok: false,
328
+ message: `Could not remove Kiro steering file: ${err.message}`,
329
+ });
330
+ }
331
+
332
+ return results;
333
+ },
334
+ async validateConfig({ serverName, scope, projectRoot }) {
335
+ const mcpPath = paths.getMcpPath(scope, projectRoot);
336
+ const cfg = readJsonFile(mcpPath);
337
+ const entry = cfg?.mcpServers?.[serverName];
338
+ if (!entry) {
339
+ return { ok: false, message: `Server "${serverName}" not found in ${mcpPath}.` };
340
+ }
341
+ const hasUserAgent = Object.keys(entry.headers ?? {}).some(
342
+ (key) => key.toLowerCase() === "user-agent",
343
+ );
344
+ if (!hasUserAgent) {
345
+ return {
346
+ ok: false,
347
+ message: `Server "${serverName}" in ${mcpPath} is missing the User-Agent header required by the Cloud WAF.`,
348
+ };
349
+ }
350
+ return { ok: true, message: `Server "${serverName}" is registered in ${mcpPath}.` };
351
+ },
352
+ };
353
+ }
354
+
355
+ function shellQuote(value, platform = process.platform) {
356
+ if (platform === "win32") {
357
+ return `"${String(value).replace(/"/g, '\\"')}"`;
358
+ }
359
+ return `'${String(value).replace(/'/g, `'"'"'`)}'`;
360
+ }
361
+
362
+ function getKiroHooksInstallRoot(paths, scope, projectRoot) {
363
+ return join(paths.getHooksDir(scope, projectRoot), "meko-hooks", "hooks-handlers");
364
+ }
365
+
366
+ function buildKiroHookCommand(handlersRoot, event, platform) {
367
+ return `node ${shellQuote(join(handlersRoot, "lib", "kiro-handler.js"), platform)} ${shellQuote(event, platform)}`;
368
+ }
369
+
370
+ function writePrivateJson(filePath, value) {
371
+ mkdirSync(dirname(filePath), { recursive: true });
372
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, {
373
+ encoding: "utf8",
374
+ mode: 0o600,
375
+ });
376
+ chmodSync(filePath, 0o600);
377
+ }
378
+
379
+ function writePrivateKiroConfig(filePath, value) {
380
+ writeJsonFile(filePath, value);
381
+ chmodSync(filePath, 0o600);
382
+ }
383
+
384
+ function writeKiroHookEnvironment(handlersRoot, { url, apiKey }) {
385
+ writePrivateJson(join(handlersRoot, "kiro-hook-env.json"), {
386
+ MEKO_MCP_URL: url,
387
+ MEKO_API_KEY: apiKey || "",
388
+ MEKO_HOOK_CLIENT: "kiro",
389
+ });
390
+ }
391
+
392
+ function isManagedHookFile(filePath) {
393
+ const cfg = readJsonFile(filePath);
394
+ return Boolean(
395
+ Array.isArray(cfg?.hooks) &&
396
+ cfg.hooks.some(
397
+ (hook) =>
398
+ typeof hook?.description === "string" &&
399
+ hook.description.includes(MANAGED_MARKER) &&
400
+ hook?.action?.command?.includes("kiro-handler.js"),
401
+ ),
402
+ );
403
+ }
404
+
405
+ function installStandaloneKiroHooks({ hooksDir, handlersRoot, log, platform }) {
406
+ mkdirSync(hooksDir, { recursive: true });
407
+ for (const hook of STANDALONE_HOOKS) {
408
+ const filePath = join(hooksDir, hook.file);
409
+ if (existsSync(filePath) && !isManagedHookFile(filePath)) {
410
+ log.dim(` Skipping ${filePath}: user-managed hook collision.`);
411
+ continue;
412
+ }
413
+ writeJsonFile(filePath, {
414
+ version: "v1",
415
+ hooks: [
416
+ {
417
+ name: `Meko ${hook.trigger}`,
418
+ description: `${MANAGED_MARKER}; durable Meko Capture v2 lifecycle hook.`,
419
+ enabled: true,
420
+ trigger: hook.trigger,
421
+ action: {
422
+ type: "command",
423
+ command: buildKiroHookCommand(handlersRoot, hook.event, platform),
424
+ },
425
+ timeout: hook.timeout,
426
+ },
427
+ ],
428
+ });
429
+ }
430
+ }
431
+
432
+ function isManagedAgentFile(agentPath) {
433
+ const cfg = readJsonFile(agentPath);
434
+ return Boolean(
435
+ cfg?.name === MANAGED_AGENT_NAME &&
436
+ typeof cfg?.description === "string" &&
437
+ cfg.description.includes(MANAGED_MARKER),
438
+ );
439
+ }
440
+
441
+ function installManagedKiroAgent({ agentPath, handlersRoot, log, name, platform }) {
442
+ if (existsSync(agentPath) && !isManagedAgentFile(agentPath)) {
443
+ log.dim(` Skipping ${agentPath}: user-managed agent collision.`);
444
+ return false;
445
+ }
446
+ writeJsonFile(agentPath, {
447
+ name: MANAGED_AGENT_NAME,
448
+ description: `Default-like Kiro CLI 2.x agent with durable Meko capture. ${MANAGED_MARKER}.`,
449
+ prompt: null,
450
+ mcpServers: {},
451
+ tools: [
452
+ `@${name}`,
453
+ "read",
454
+ "write",
455
+ "shell",
456
+ "aws",
457
+ "report",
458
+ "introspect",
459
+ "knowledge",
460
+ "thinking",
461
+ "todo",
462
+ "delegate",
463
+ "grep",
464
+ "glob",
465
+ ],
466
+ toolAliases: {},
467
+ allowedTools: [],
468
+ resources: [],
469
+ toolsSettings: {},
470
+ includeMcpJson: true,
471
+ model: null,
472
+ hooks: {
473
+ agentSpawn: [{
474
+ command: buildKiroHookCommand(handlersRoot, "agentSpawn", platform),
475
+ }],
476
+ userPromptSubmit: [{
477
+ command: buildKiroHookCommand(handlersRoot, "userPromptSubmit", platform),
478
+ }],
479
+ stop: [{
480
+ command: buildKiroHookCommand(handlersRoot, "stop", platform),
481
+ }],
482
+ },
483
+ });
484
+ return true;
485
+ }
486
+
487
+ function isKiroInstallState(value) {
488
+ return Boolean(
489
+ value?.schema_version === 1 && value?.managed_agent === MANAGED_AGENT_NAME,
490
+ );
491
+ }
492
+
493
+ function activateManagedKiroAgent({
494
+ cliSettingsPath,
495
+ handlersRoot,
496
+ priorInstallState,
497
+ }) {
498
+ const cfg = readJsonFile(cliSettingsPath);
499
+ const hasDefault = Object.prototype.hasOwnProperty.call(cfg, "chat.defaultAgent");
500
+ const currentDefault = cfg["chat.defaultAgent"];
501
+ let state;
502
+ if (isKiroInstallState(priorInstallState)) {
503
+ state = priorInstallState;
504
+ } else {
505
+ state = {
506
+ schema_version: 1,
507
+ managed_agent: MANAGED_AGENT_NAME,
508
+ had_previous_default: hasDefault && currentDefault !== MANAGED_AGENT_NAME,
509
+ previous_default:
510
+ hasDefault && currentDefault !== MANAGED_AGENT_NAME ? currentDefault : null,
511
+ };
512
+ }
513
+ cfg["chat.defaultAgent"] = MANAGED_AGENT_NAME;
514
+ writeJsonFile(cliSettingsPath, cfg);
515
+ writePrivateJson(join(handlersRoot, "kiro-install-state.json"), state);
516
+ }
517
+
518
+ function restoreKiroDefaultAgent(cliSettingsPath, state) {
519
+ if (!state || state.managed_agent !== MANAGED_AGENT_NAME) return;
520
+ const cfg = readJsonFile(cliSettingsPath);
521
+ if (cfg["chat.defaultAgent"] !== MANAGED_AGENT_NAME) return;
522
+ if (state.had_previous_default) {
523
+ cfg["chat.defaultAgent"] = state.previous_default;
524
+ } else {
525
+ delete cfg["chat.defaultAgent"];
526
+ }
527
+ writeJsonFile(cliSettingsPath, cfg);
528
+ }
529
+
530
+ function configReferencesKiroRuntime(filePath, handlersRoot) {
531
+ if (!existsSync(filePath)) return false;
532
+ try {
533
+ const handlerPath = join(handlersRoot, "lib", "kiro-handler.js");
534
+ const cfg = readJsonFile(filePath);
535
+ const commands = Array.isArray(cfg?.hooks)
536
+ ? cfg.hooks.map((hook) => hook?.action?.command)
537
+ : Object.values(cfg?.hooks ?? {}).flatMap((hooks) =>
538
+ (Array.isArray(hooks) ? hooks : [hooks]).map((hook) => hook?.command),
539
+ );
540
+ return commands.some(
541
+ (command) => typeof command === "string" && command.includes(handlerPath),
542
+ );
543
+ } catch {
544
+ return false;
545
+ }
546
+ }
547
+
548
+ function uninstallKiroCapture({ scope, projectRoot, paths }) {
549
+ const hooksDir = paths.getHooksDir(scope, projectRoot);
550
+ const handlersRoot = getKiroHooksInstallRoot(paths, scope, projectRoot);
551
+ const runtimeOwned = Boolean(readInstallerMarker(handlersRoot));
552
+ const state = readJsonFile(join(handlersRoot, "kiro-install-state.json"));
553
+ restoreKiroDefaultAgent(paths.getCliSettingsPath(scope, projectRoot), state);
554
+
555
+ for (const hook of STANDALONE_HOOKS) {
556
+ const filePath = join(hooksDir, hook.file);
557
+ if (existsSync(filePath) && isManagedHookFile(filePath)) {
558
+ rmSync(filePath, { force: true });
559
+ }
560
+ }
561
+
562
+ const agentPath = paths.getManagedAgentPath(scope, projectRoot);
563
+ if (existsSync(agentPath) && isManagedAgentFile(agentPath)) {
564
+ rmSync(agentPath, { force: true });
565
+ }
566
+
567
+ const remainingHookDependsOnRuntime = STANDALONE_HOOKS.some((hook) =>
568
+ configReferencesKiroRuntime(join(hooksDir, hook.file), handlersRoot),
569
+ );
570
+ const remainingAgentDependsOnRuntime = configReferencesKiroRuntime(
571
+ agentPath,
572
+ handlersRoot,
573
+ );
574
+
575
+ // The runtime may survive when a user-edited hook or agent still points at
576
+ // it, but installer credentials must not. Remove the private environment
577
+ // file independently before deciding whether the handler tree is retained.
578
+ if (runtimeOwned) {
579
+ rmSync(join(handlersRoot, "kiro-hook-env.json"), { force: true });
580
+ }
581
+
582
+ // Preserve a still-referenced runtime when the user edited an installer hook
583
+ // or agent enough to take ownership of it. Otherwise uninstall would leave a
584
+ // surviving Kiro command pointing at a deleted handler. The event journal and
585
+ // Capture v2 watermark/outbox live elsewhere and are always retained.
586
+ if (
587
+ runtimeOwned &&
588
+ !remainingHookDependsOnRuntime &&
589
+ !remainingAgentDependsOnRuntime
590
+ ) {
591
+ removePath(handlersRoot);
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Steering text Kiro loads on every interaction.
597
+ *
598
+ * This makes the skill's capture assumption conditional. Kiro normally has
599
+ * hooks, but workspace trust, disabled hooks, or a different selected CLI 2.x
600
+ * agent can prevent them from running. The session-start marker distinguishes
601
+ * normal hook capture from that fallback without causing duplicate writes.
602
+ *
603
+ * Keep this short. Steering is prepended to every interaction, so it competes
604
+ * with the user's own context budget; the skill still owns tool-by-tool detail.
605
+ *
606
+ * @param {string} version - installer version, stamped so a re-run can tell staleness
607
+ * @param {string} name - MCP server name, which prefixes the tool names
608
+ * @returns {string}
609
+ */
610
+ export function renderSteeringDoc(version, name) {
611
+ const toolPrefix = `mcp__${name}__`;
612
+ return [
613
+ "---",
614
+ "inclusion: always",
615
+ "---",
616
+ "",
617
+ `<!-- Managed by meko installer v${version}. Re-running the installer overwrites this file. -->`,
618
+ "",
619
+ "# Meko memory",
620
+ "",
621
+ `You have a persistent memory store through the \`${name}\` MCP server.`,
622
+ "",
623
+ "Kiro normally starts durable hook capture and injects the marker",
624
+ "`MEKO_CAPTURE_SESSION_STARTED` into session context.",
625
+ "",
626
+ "- If that marker is present, follow the `meko-mcp-tools` hook-capture rules.",
627
+ " Do not duplicate conversational facts with proactive memory writes.",
628
+ "- If that marker is absent, hooks are disabled, the IDE workspace may be",
629
+ " untrusted, or a different CLI 2.x agent is selected. In that fallback",
630
+ " mode, save durable facts proactively as described below.",
631
+ "",
632
+ "Follow these rules:",
633
+ "",
634
+ `- Search before you answer. Call \`${toolPrefix}memory_search\` at the start of a`,
635
+ " conversation, and again whenever the user asks what you know about something.",
636
+ `- In fallback mode, call \`${toolPrefix}memory_add\` when the user`,
637
+ " states something worth remembering later: their role, preferences, team",
638
+ " conventions, tools, or a decision and its rationale. Prefer saving to asking.",
639
+ "- Skip the ephemeral. Don't store one-off task instructions, transient state,",
640
+ " or anything already in the repository.",
641
+ `- Correct, don't duplicate. When the user contradicts a stored fact, find it with`,
642
+ ` \`${toolPrefix}memory_search\` and fix it with \`${toolPrefix}memory_update\` or`,
643
+ ` \`${toolPrefix}memory_delete_by_id\` — memory is additive, so a stale entry survives`,
644
+ " until you remove it.",
645
+ "",
646
+ "For tool arguments, `agent_id` conventions, and datapack selection, see the",
647
+ "`meko-mcp-tools` skill.",
648
+ "",
649
+ ].join("\n");
650
+ }
651
+
652
+ /**
653
+ * True when `destination` is a symlink resolving to `expectedSource` — an
654
+ * in-place symlink install whose content is already current.
655
+ */
656
+ function isOwnSymlink(destination, expectedSource) {
657
+ try {
658
+ const stat = lstatSync(destination);
659
+ if (!stat.isSymbolicLink()) return false;
660
+ const linkTarget = readlinkSync(destination);
661
+ return resolve(dirname(destination), linkTarget) === expectedSource;
662
+ } catch {
663
+ return false;
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Remove the steering file, but only if we wrote it.
669
+ *
670
+ * We own the `meko.md` filename, yet a user could still have created one by
671
+ * hand. The managed marker comment is the discriminator — no marker, no delete.
672
+ */
673
+ function removeKiroSteering(steeringPath) {
674
+ if (!existsSync(steeringPath)) return;
675
+ const raw = readFileSync(steeringPath, "utf8");
676
+ if (!raw.includes("Managed by meko installer")) return;
677
+ rmSync(steeringPath, { force: true });
678
+ }
679
+
680
+ function uninstallKiroSkills({ scope, projectRoot, paths, resolveSkillSourcePath }) {
681
+ const skillsDir = paths.getSkillsDir(scope, projectRoot);
682
+
683
+ for (const skill of SKILLS) {
684
+ const dest = join(skillsDir, skill.name);
685
+ if (!existsSync(dest)) continue;
686
+
687
+ // A marker means we materialized this copy, so it is ours to delete.
688
+ if (existsSync(join(dest, ".meko-installer"))) {
689
+ removePath(dest);
690
+ continue;
691
+ }
692
+
693
+ // Otherwise only remove it if it is a symlink we created into our own
694
+ // asset tree — never touch a user-authored skill of the same name.
695
+ try {
696
+ if (!lstatSync(dest).isSymbolicLink()) continue;
697
+ const resolvedTarget = resolve(dirname(dest), readlinkSync(dest));
698
+ if (resolvedTarget === resolveSkillSourcePath(skill.name)) {
699
+ removePath(dest);
700
+ }
701
+ } catch {
702
+ // no-op
703
+ }
704
+ }
705
+ }