@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.
- package/README.md +183 -33
- package/assets/hooks-handlers/codex-session-end.sh +13 -0
- package/assets/hooks-handlers/cursor-session-start.sh +19 -1
- package/assets/hooks-handlers/lib/capture-notices.js +321 -0
- package/assets/hooks-handlers/lib/capture-state.js +2079 -0
- package/assets/hooks-handlers/lib/capture-status-cli.js +82 -0
- package/assets/hooks-handlers/lib/capture.js +1653 -225
- package/assets/hooks-handlers/lib/checkpoint-timer.js +455 -50
- package/assets/hooks-handlers/lib/kiro-handler.js +738 -0
- package/assets/hooks-handlers/lib/terminal-handoff.js +418 -0
- package/assets/hooks-handlers/session-end.sh +3 -24
- package/assets/hooks-handlers/session-start.sh +8 -1
- package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
- package/assets/scripts/meko-statusline.sh +41 -1
- package/assets/scripts/package-desktop-skill.sh +32 -1
- package/assets/skills/meko-mcp-tools/SKILL.md +90 -137
- package/assets/skills/meko-mcp-tools/references/_sections.md +3 -5
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +31 -28
- package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
- package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +69 -49
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -6
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +29 -5
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +29 -92
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +42 -40
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +1 -2
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +18 -11
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +10 -9
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +51 -44
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +6 -6
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +34 -25
- package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +63 -37
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +4 -4
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +19 -22
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +42 -40
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +1 -2
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -17
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +31 -21
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +1 -1
- package/bin/create-meko-setup.mjs +93 -16
- package/lib/capture-status.mjs +67 -0
- package/lib/clients/aliases.mjs +5 -0
- package/lib/clients/claude-code.mjs +8 -4
- package/lib/clients/claude-desktop.mjs +133 -4
- package/lib/clients/codex.mjs +29 -11
- package/lib/clients/common.mjs +2 -0
- package/lib/clients/cursor.mjs +7 -2
- package/lib/clients/index.mjs +2 -0
- package/lib/clients/kiro.mjs +705 -0
- package/lib/config.mjs +90 -0
- package/lib/detect.mjs +7 -2
- package/lib/installer.mjs +1 -0
- package/lib/migrate/adapters/codex.mjs +490 -0
- package/lib/migrate/id.mjs +28 -3
- package/lib/migrate/orchestrator.mjs +34 -7
- package/lib/migrate.mjs +4 -2
- package/lib/prompt.mjs +17 -2
- package/lib/url-selection.mjs +55 -0
- package/package.json +4 -4
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -71
- 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
|
+
}
|