@yugabytedb/meko-mcp 2.2.0 → 2.3.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 (63) hide show
  1. package/README.md +106 -38
  2. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
  3. package/assets/hooks-handlers/lib/capture-notices.js +131 -14
  4. package/assets/hooks-handlers/lib/capture-state.js +95 -1
  5. package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
  6. package/assets/hooks-handlers/lib/capture.js +1237 -137
  7. package/assets/hooks-handlers/lib/checkpoint-timer.js +51 -10
  8. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  9. package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
  10. package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
  11. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  12. package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
  13. package/assets/opencode-plugin/meko.js +213 -0
  14. package/assets/scripts/meko-statusline.sh +85 -18
  15. package/assets/skills/meko-mcp-tools/SKILL.md +57 -14
  16. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
  17. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
  18. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +86 -14
  19. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
  20. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +29 -6
  21. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +8 -4
  22. package/assets/skills/meko-mcp-tools/references/tools-overview.md +32 -22
  23. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
  24. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +45 -1
  25. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
  26. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +54 -18
  27. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
  28. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +8 -6
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +95 -33
  31. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +14 -10
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +35 -25
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
  36. package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
  37. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +5 -5
  38. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
  39. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
  40. package/bin/create-meko-setup.mjs +70 -7
  41. package/lib/capture-cleanup.mjs +63 -0
  42. package/lib/clients/claude-code.mjs +1 -0
  43. package/lib/clients/codex.mjs +80 -35
  44. package/lib/clients/common.mjs +7 -0
  45. package/lib/clients/index.mjs +2 -0
  46. package/lib/clients/kiro.mjs +5 -5
  47. package/lib/clients/opencode.mjs +532 -0
  48. package/lib/config.mjs +68 -1
  49. package/lib/installer.mjs +18 -2
  50. package/lib/migrate/adapters/claude-code.mjs +11 -2
  51. package/lib/migrate/adapters/codex.mjs +9 -2
  52. package/lib/migrate/adapters/memory-md.mjs +27 -8
  53. package/lib/migrate/adapters/opencode.mjs +148 -0
  54. package/lib/migrate/claude-parser.mjs +153 -9
  55. package/lib/migrate/id.mjs +195 -13
  56. package/lib/migrate/mcp-client.mjs +25 -1
  57. package/lib/migrate/opencode-normalizer.mjs +132 -0
  58. package/lib/migrate/orchestrator.mjs +293 -77
  59. package/lib/migrate/progress.mjs +239 -5
  60. package/lib/migrate/registry.mjs +72 -0
  61. package/lib/migrate.mjs +96 -8
  62. package/package.json +1 -1
  63. package/scripts/bundle-assets.mjs +4 -0
@@ -55,10 +55,10 @@ const STANDALONE_HOOKS = [
55
55
  * Kiro through the `mcp-remote` stdio bridge never presents the API key at all
56
56
  * (the bridge forwards only headers passed explicitly via `--header`), so it
57
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.
58
+ * client registration with a loopback callback. The DEV WAF refuses internal
59
+ * hosts in a redirect_uri with an opaque HTML 403; production accepts them as
60
+ * of 2026-09-24, but OAuth still needs a browser. Header auth works headless
61
+ * and sidesteps OAuth entirely.
62
62
  *
63
63
  * Kiro 2.18.1 contains two hook generations. CLI 2.x reads lowercase inline
64
64
  * hooks from the selected custom agent; CLI 3.x and IDE 1.x read standalone
@@ -631,7 +631,7 @@ export function renderSteeringDoc(version, name) {
631
631
  "",
632
632
  "Follow these rules:",
633
633
  "",
634
- `- Search before you answer. Call \`${toolPrefix}memory_search\` at the start of a`,
634
+ `- Search before you answer. Call \`${toolPrefix}context_search\` at the start of a`,
635
635
  " conversation, and again whenever the user asks what you know about something.",
636
636
  `- In fallback mode, call \`${toolPrefix}memory_add\` when the user`,
637
637
  " states something worth remembering later: their role, preferences, team",
@@ -0,0 +1,532 @@
1
+ import {
2
+ chmodSync,
3
+ closeSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readdirSync,
11
+ readFileSync,
12
+ readlinkSync,
13
+ renameSync,
14
+ rmdirSync,
15
+ rmSync,
16
+ unlinkSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { spawnSync } from "node:child_process";
20
+ import { dirname, join, resolve } from "node:path";
21
+ import {
22
+ getCaptureRoot,
23
+ getOpenCodeHookEnvPath,
24
+ getOpenCodeHooksInstallRoot,
25
+ getOpenCodeMekoRoot,
26
+ getOpenCodePluginPath,
27
+ getOpenCodeSkillsDir,
28
+ readJsonFile,
29
+ } from "../config.mjs";
30
+ import {
31
+ MEKO_MCP_USER_AGENT,
32
+ getInstallerVersion,
33
+ installStableTree,
34
+ isDanglingSymlink,
35
+ isPackagedInstall,
36
+ markerIsStale,
37
+ readInstallerMarker,
38
+ removePath,
39
+ resolveHooksRoot,
40
+ resolveOpenCodePluginSource,
41
+ resolveSkillSource,
42
+ symlinkOrCopy,
43
+ writeInstallerMarker,
44
+ } from "./common.mjs";
45
+ import {
46
+ isSupportedOpenCodeVersion,
47
+ probeOpenCode,
48
+ } from "../migrate/adapters/opencode.mjs";
49
+ const SKILLS = [{ name: "meko-mcp-tools" }];
50
+ const MANAGED_MARKER = "Managed by meko installer";
51
+ const OPENCODE_INSTALL_URL = "https://opencode.ai/docs/cli/";
52
+ export function createOpenCodeAdapter(customPaths = {}) {
53
+ const resolveHooksRootPath = customPaths.resolveHooksRoot ?? resolveHooksRoot;
54
+ const resolveSkillSourcePath = customPaths.resolveSkillSource ?? resolveSkillSource;
55
+ const resolvePluginSourcePath = customPaths.resolveOpenCodePluginSource ?? resolveOpenCodePluginSource;
56
+ const packagedInstallDetector = customPaths.isPackagedInstall ?? isPackagedInstall;
57
+ const probe = customPaths.probeOpenCode ?? probeOpenCode;
58
+ const readResolvedConfig =
59
+ customPaths.readResolvedConfig ?? defaultReadResolvedConfig;
60
+ const paths = {
61
+ getPluginPath: customPaths.getPluginPath ?? getOpenCodePluginPath,
62
+ getMekoRoot: customPaths.getMekoRoot ?? getOpenCodeMekoRoot,
63
+ getHookEnvPath: customPaths.getHookEnvPath ?? getOpenCodeHookEnvPath,
64
+ getHooksInstallRoot: customPaths.getHooksInstallRoot ?? getOpenCodeHooksInstallRoot,
65
+ getSkillsDir: customPaths.getSkillsDir ?? getOpenCodeSkillsDir,
66
+ getCaptureRoot: customPaths.getCaptureRoot ?? getCaptureRoot,
67
+ };
68
+ return {
69
+ id: "opencode",
70
+ displayName: "OpenCode",
71
+ settingsPath(scope, projectRoot) {
72
+ return paths.getPluginPath(scope, projectRoot);
73
+ },
74
+ async detectPrerequisites({ verbose, log, dryRun = false } = { verbose: false, log: console }) {
75
+ const found = probe();
76
+ if (!found?.found) {
77
+ if (dryRun) {
78
+ log.dim?.(" OpenCode is not on PATH; continuing because this is a dry run.");
79
+ return { found: false, path: null, version: null };
80
+ }
81
+ throw new Error(
82
+ `OpenCode not found on PATH. Install OpenCode CLI/TUI 1.18.x (${OPENCODE_INSTALL_URL}).`,
83
+ );
84
+ }
85
+ if (!isSupportedOpenCodeVersion(found.version)) {
86
+ throw new Error(
87
+ `Unsupported OpenCode version "${found.version || "unknown"}". ` +
88
+ "This installer supports OpenCode CLI/TUI 1.18.x only.",
89
+ );
90
+ }
91
+ if (verbose) {
92
+ log.dim?.(` OpenCode: ${found.path} (${found.version})`);
93
+ }
94
+ return found;
95
+ },
96
+ isInstalled() {
97
+ return Boolean(probe()?.found);
98
+ },
99
+ detectExisting(_name, { scope, projectRoot }) {
100
+ const pluginPath = paths.getPluginPath(scope, projectRoot);
101
+ const markerPath = join(paths.getHooksInstallRoot(scope, projectRoot), ".meko-installer");
102
+ const found = existsSync(pluginPath) && existsSync(markerPath);
103
+ return {
104
+ found,
105
+ status: found ? "Plugin overlay installed" : "",
106
+ };
107
+ },
108
+ removeRegistration(_name, { scope, projectRoot }) {
109
+ const pluginPath = paths.getPluginPath(scope, projectRoot);
110
+ if (existsSync(pluginPath) && isManagedPlugin(pluginPath)) {
111
+ rmSync(pluginPath, { force: true });
112
+ return { removed: true };
113
+ }
114
+ return { removed: false, present: existsSync(pluginPath) };
115
+ },
116
+ registerServer({ name, url, apiKey, dryRun, log, scope, projectRoot }) {
117
+ const pluginPath = paths.getPluginPath(scope, projectRoot);
118
+ const mekoRoot = paths.getMekoRoot(scope, projectRoot);
119
+ if (dryRun) {
120
+ log.dim(` Would install OpenCode plugin overlay at ${pluginPath}`);
121
+ log.dim(` Would write private environment at ${paths.getHookEnvPath(scope, projectRoot)}`);
122
+ log.dim(` mcp.${name}.url = ${url}`);
123
+ log.dim(` mcp.${name}.type = remote`);
124
+ log.dim(` mcp.${name}.oauth = false`);
125
+ if (apiKey) log.dim(" mcp.<name>.headers.Authorization = <set>");
126
+ return;
127
+ }
128
+ if (existsSync(pluginPath) && !isManagedPlugin(pluginPath)) {
129
+ log.warn?.(
130
+ ` OpenCode plugin at ${pluginPath} has no installer marker — leaving it untouched.`,
131
+ );
132
+ return;
133
+ }
134
+ mkdirSync(dirname(pluginPath), { recursive: true });
135
+ mkdirSync(mekoRoot, { recursive: true });
136
+ const tmpPlugin = `${pluginPath}.tmp`;
137
+ copyFileSync(resolvePluginSourcePath(), tmpPlugin);
138
+ renameSync(tmpPlugin, pluginPath);
139
+ const envPath = paths.getHookEnvPath(scope, projectRoot);
140
+ writePrivateHookEnv(envPath, {
141
+ MEKO_MCP_URL: url,
142
+ MEKO_API_KEY: apiKey || "",
143
+ MEKO_MCP_SERVER_NAME: name,
144
+ MEKO_MCP_USER_AGENT: MEKO_MCP_USER_AGENT,
145
+ MEKO_HOOK_CLIENT: "opencode",
146
+ MEKO_NODE_EXECUTABLE: process.execPath,
147
+ MEKO_WATERMARK_DIR: savedWatermarkDir(envPath) || paths.getCaptureRoot(),
148
+ });
149
+ },
150
+ setHookEnvironment({ url, apiKey, dryRun, log, scope, projectRoot, name }) {
151
+ const handlersRoot = paths.getHooksInstallRoot(scope, projectRoot);
152
+ if (dryRun) {
153
+ log.dim(` Would copy capture runtime to ${handlersRoot}`);
154
+ log.dim(` Would write private environment at ${paths.getHookEnvPath(scope, projectRoot)}`);
155
+ const legacyPreview = join(paths.getMekoRoot(scope, projectRoot), "snapshots");
156
+ const previewOwned = shouldMigrateLegacySnapshots(handlersRoot);
157
+ if (existsSync(legacyPreview) && previewOwned) {
158
+ log.dim(` Would migrate legacy staged snapshots from ${legacyPreview}`);
159
+ }
160
+ return;
161
+ }
162
+ const envPath = paths.getHookEnvPath(scope, projectRoot);
163
+ const captureRoot = savedWatermarkDir(envPath) || paths.getCaptureRoot();
164
+ const legacyDir = join(paths.getMekoRoot(scope, projectRoot), "snapshots");
165
+ const runtimeOwned = shouldMigrateLegacySnapshots(handlersRoot);
166
+ if (runtimeOwned) {
167
+ migrateLegacyOpenCodeSnapshots({
168
+ legacyDir,
169
+ durableDir: join(captureRoot, "opencode-snapshots"),
170
+ log,
171
+ });
172
+ } else if (existsSync(legacyDir)) {
173
+ log.dim?.(` Skipping legacy snapshot migration: ${handlersRoot} is user-managed.`);
174
+ }
175
+ if (existsSync(handlersRoot) && !readInstallerMarker(handlersRoot)) {
176
+ log.dim?.(` Skipping OpenCode capture install: ${handlersRoot} is user-managed.`);
177
+ return;
178
+ }
179
+ installStableTree(resolveHooksRootPath(), handlersRoot);
180
+ writeInstallerMarker(handlersRoot);
181
+ writePrivateHookEnv(envPath, {
182
+ MEKO_MCP_URL: url,
183
+ MEKO_API_KEY: apiKey || "",
184
+ MEKO_MCP_SERVER_NAME: resolveHookServerName(name, envPath),
185
+ MEKO_MCP_USER_AGENT: MEKO_MCP_USER_AGENT,
186
+ MEKO_HOOK_CLIENT: "opencode",
187
+ MEKO_NODE_EXECUTABLE: process.execPath,
188
+ MEKO_WATERMARK_DIR: captureRoot,
189
+ });
190
+ },
191
+ installSkills({ dryRun, log, scope, projectRoot }) {
192
+ const skillDir = paths.getSkillsDir(scope, projectRoot);
193
+ const currentVersion = getInstallerVersion();
194
+ if (dryRun) {
195
+ log.dim(` Would install skills into ${skillDir}`);
196
+ return { marketplace: "skipped", skills: "dry-run" };
197
+ }
198
+ mkdirSync(skillDir, { recursive: true });
199
+ for (const skill of SKILLS) {
200
+ const source = resolveSkillSourcePath(skill.name);
201
+ const destination = join(skillDir, skill.name);
202
+ if (existsSync(destination)) {
203
+ if (isOwnSymlink(destination, source)) continue;
204
+ const marker = readInstallerMarker(destination);
205
+ if (!marker) {
206
+ log.dim(` Skipping ${destination}: no .meko-installer marker (user-managed).`);
207
+ continue;
208
+ }
209
+ if (markerIsStale(destination, currentVersion)) {
210
+ removePath(destination);
211
+ } else {
212
+ continue;
213
+ }
214
+ } else if (isDanglingSymlink(destination)) {
215
+ removePath(destination);
216
+ }
217
+ const strategy = symlinkOrCopy(source, destination, {
218
+ preferCopy: packagedInstallDetector(),
219
+ });
220
+ if (strategy === "copy") writeInstallerMarker(destination);
221
+ }
222
+ return { marketplace: "skipped", skills: "ok" };
223
+ },
224
+ uninstall({ name, scope, projectRoot }) {
225
+ const results = [];
226
+ let captureRoot = paths.getCaptureRoot();
227
+ try {
228
+ const plugin = this.removeRegistration(name, { scope, projectRoot });
229
+ results.push({
230
+ key: "plugin",
231
+ ok: true,
232
+ message: plugin?.removed
233
+ ? "OpenCode plugin overlay removed."
234
+ : plugin?.present
235
+ ? "Plugin left in place because it is not installer-owned."
236
+ : "OpenCode plugin overlay not present.",
237
+ });
238
+ } catch (err) {
239
+ results.push({ key: "plugin", ok: false, message: `Could not remove plugin: ${err.message}` });
240
+ }
241
+ const envPath = paths.getHookEnvPath(scope, projectRoot);
242
+ try {
243
+ const saved = savedWatermarkDir(envPath);
244
+ if (saved) captureRoot = saved;
245
+ if (existsSync(envPath)) {
246
+ writePrivateHookEnv(envPath, { MEKO_WATERMARK_DIR: captureRoot });
247
+ }
248
+ results.push({ key: "credentials", ok: true, message: "OpenCode API key removed." });
249
+ } catch (err) {
250
+ results.push({ key: "credentials", ok: false, message: `Could not remove credentials: ${err.message}` });
251
+ }
252
+ try {
253
+ const handlersRoot = paths.getHooksInstallRoot(scope, projectRoot);
254
+ const runtimeOwned = Boolean(readInstallerMarker(handlersRoot));
255
+ let kept = 0;
256
+ if (runtimeOwned) {
257
+ const legacyDir = join(paths.getMekoRoot(scope, projectRoot), "snapshots");
258
+ if (existsSync(legacyDir)) {
259
+ const migrated = migrateLegacyOpenCodeSnapshots({
260
+ legacyDir,
261
+ durableDir: join(captureRoot, "opencode-snapshots"),
262
+ log: { dim() {}, warn() {} },
263
+ });
264
+ kept = Number(migrated?.kept || 0);
265
+ }
266
+ }
267
+ if (kept > 0) {
268
+ results.push({
269
+ key: "runtime",
270
+ ok: false,
271
+ message: `Could not migrate ${kept} legacy snapshot(s); left installer runtime in place.`,
272
+ });
273
+ } else {
274
+ if (runtimeOwned && existsSync(handlersRoot)) {
275
+ rmSync(handlersRoot, { recursive: true, force: true });
276
+ }
277
+ results.push({
278
+ key: "runtime",
279
+ ok: true,
280
+ message: runtimeOwned
281
+ ? "Installer-owned runtime removed; durable capture data (snapshots, journals, outboxes, quarantine) preserved."
282
+ : "Runtime left in place because it is not installer-owned; durable capture data preserved.",
283
+ });
284
+ }
285
+ } catch (err) {
286
+ results.push({ key: "runtime", ok: false, message: `Could not remove runtime: ${err.message}` });
287
+ }
288
+ try {
289
+ uninstallOpenCodeSkills({ scope, projectRoot, paths, resolveSkillSourcePath });
290
+ results.push({ key: "skills", ok: true, message: "OpenCode skills cleaned up." });
291
+ } catch (err) {
292
+ results.push({ key: "skills", ok: false, message: `Could not remove skills: ${err.message}` });
293
+ }
294
+ return results;
295
+ },
296
+ async validateConfig({ serverName, scope, projectRoot, log }) {
297
+ const pluginPath = paths.getPluginPath(scope, projectRoot);
298
+ const envPath = paths.getHookEnvPath(scope, projectRoot);
299
+ if (!existsSync(pluginPath)) {
300
+ return { ok: false, message: `OpenCode plugin not found at ${pluginPath}. Restart OpenCode after install.` };
301
+ }
302
+ if (!isManagedPlugin(pluginPath)) {
303
+ return { ok: false, message: `OpenCode plugin at ${pluginPath} is not installer-managed.` };
304
+ }
305
+ const env = readJsonFile(envPath);
306
+ if (!env?.MEKO_MCP_URL) {
307
+ return { ok: false, message: `Missing MEKO_MCP_URL in ${envPath}.` };
308
+ }
309
+ const resolvedName = env.MEKO_MCP_SERVER_NAME || serverName;
310
+ if (resolvedName !== serverName) {
311
+ log?.warn?.(` OpenCode env server name ${resolvedName} differs from requested ${serverName}.`);
312
+ }
313
+ if (!env.MEKO_MCP_USER_AGENT) {
314
+ return { ok: false, message: "OpenCode environment is missing MEKO_MCP_USER_AGENT." };
315
+ }
316
+ if (
317
+ typeof env.MEKO_NODE_EXECUTABLE !== "string" ||
318
+ !env.MEKO_NODE_EXECUTABLE ||
319
+ !existsSync(env.MEKO_NODE_EXECUTABLE)
320
+ ) {
321
+ return {
322
+ ok: false,
323
+ message:
324
+ "OpenCode environment is missing a valid stable Node executable; reinstall Meko.",
325
+ };
326
+ }
327
+ const found = probe();
328
+ if (!found?.found) {
329
+ return { ok: false, message: "OpenCode is not available on PATH for resolved-config validation." };
330
+ }
331
+ if (!isSupportedOpenCodeVersion(found.version)) {
332
+ return {
333
+ ok: false,
334
+ message:
335
+ `Unsupported OpenCode version "${found.version || "unknown"}"; ` +
336
+ "supported versions are 1.18.x.",
337
+ };
338
+ }
339
+ let resolved;
340
+ try {
341
+ resolved = await Promise.resolve(readResolvedConfig({
342
+ bin: found.path || "opencode",
343
+ cwd: scope === "project" ? projectRoot : undefined,
344
+ }));
345
+ } catch (err) {
346
+ return {
347
+ ok: false,
348
+ message: `Could not read OpenCode resolved configuration: ${err.message}`,
349
+ };
350
+ }
351
+ const entry = resolved?.mcp?.[resolvedName];
352
+ const hasAuthorization = Object.keys(entry?.headers || {}).some(
353
+ (key) => key.toLowerCase() === "authorization",
354
+ );
355
+ const hasUserAgent = Object.keys(entry?.headers || {}).some(
356
+ (key) => key.toLowerCase() === "user-agent",
357
+ );
358
+ if (
359
+ !entry ||
360
+ entry.type !== "remote" ||
361
+ entry.url !== env.MEKO_MCP_URL ||
362
+ entry.enabled !== true ||
363
+ entry.oauth !== false ||
364
+ !hasUserAgent ||
365
+ (env.MEKO_API_KEY && !hasAuthorization)
366
+ ) {
367
+ return {
368
+ ok: false,
369
+ message:
370
+ `OpenCode resolved configuration does not contain the complete ` +
371
+ `"${resolvedName}" Meko MCP overlay.`,
372
+ };
373
+ }
374
+ return {
375
+ ok: true,
376
+ message: `OpenCode resolved the Meko MCP overlay for "${resolvedName}".`,
377
+ };
378
+ },
379
+ };
380
+ }
381
+ function defaultReadResolvedConfig({ bin, cwd }) {
382
+ const result = spawnSync(bin, ["debug", "config"], {
383
+ cwd,
384
+ encoding: "utf8",
385
+ maxBuffer: 16 * 1024 * 1024,
386
+ timeout: 10_000,
387
+ });
388
+ if (result.error) throw result.error;
389
+ if (result.status !== 0) {
390
+ throw new Error(`opencode debug config failed with exit code ${result.status}`);
391
+ }
392
+ try {
393
+ return JSON.parse(String(result.stdout || ""));
394
+ } catch (err) {
395
+ throw new Error(`invalid JSON from opencode debug config: ${err.message}`);
396
+ }
397
+ }
398
+ function isManagedPlugin(pluginPath) {
399
+ try {
400
+ const text = readFileSync(pluginPath, "utf8");
401
+ return text.includes(MANAGED_MARKER);
402
+ } catch {
403
+ return false;
404
+ }
405
+ }
406
+ function resolveHookServerName(name, envPath) {
407
+ if (typeof name === "string" && name.trim()) return name.trim();
408
+ try {
409
+ const existing = JSON.parse(readFileSync(envPath, "utf8"));
410
+ if (typeof existing.MEKO_MCP_SERVER_NAME === "string" && existing.MEKO_MCP_SERVER_NAME.trim()) {
411
+ return existing.MEKO_MCP_SERVER_NAME.trim();
412
+ }
413
+ } catch {
414
+ // first write or unreadable env
415
+ }
416
+ return "meko";
417
+ }
418
+ function writePrivateHookEnv(filePath, value) {
419
+ mkdirSync(dirname(filePath), { recursive: true });
420
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
421
+ chmodSync(filePath, 0o600);
422
+ }
423
+ function savedWatermarkDir(envPath) {
424
+ try {
425
+ const v = JSON.parse(readFileSync(envPath, "utf8")).MEKO_WATERMARK_DIR;
426
+ if (typeof v === "string" && v) return v;
427
+ } catch {}
428
+ return null;
429
+ }
430
+ function isOwnSymlink(destination, source) {
431
+ try {
432
+ if (!lstatSync(destination).isSymbolicLink()) return false;
433
+ return resolve(readlinkSync(destination)) === resolve(source);
434
+ } catch {
435
+ return false;
436
+ }
437
+ }
438
+ /** Migrate leftover `meko/snapshots` if handlers are gone (#307 uninstall) or installer-owned. */
439
+ function shouldMigrateLegacySnapshots(handlersRoot) {
440
+ if (!existsSync(handlersRoot)) return true;
441
+ return Boolean(readInstallerMarker(handlersRoot));
442
+ }
443
+ /** Move leftover `meko/snapshots/*.json` into the durable capture root. */
444
+ export function migrateLegacyOpenCodeSnapshots({ legacyDir, durableDir, log = console } = {}) {
445
+ if (!legacyDir || !existsSync(legacyDir)) return { migrated: 0, kept: 0 };
446
+ let entries;
447
+ try {
448
+ entries = readdirSync(legacyDir).filter((name) => name.endsWith(".json"));
449
+ } catch {
450
+ return { migrated: 0, kept: 0 };
451
+ }
452
+ if (entries.length === 0) {
453
+ try { rmdirSync(legacyDir); } catch { /* best effort */ }
454
+ return { migrated: 0, kept: 0 };
455
+ }
456
+ mkdirSync(durableDir, { recursive: true, mode: 0o700 });
457
+ let migrated = 0;
458
+ let kept = 0;
459
+ for (const name of entries) {
460
+ const source = join(legacyDir, name);
461
+ let target = join(durableDir, name);
462
+ let suffix = 0;
463
+ const base = name.endsWith(".json") ? name.slice(0, -".json".length) : name;
464
+ if (existsSync(target)) {
465
+ try {
466
+ if (
467
+ lstatSync(source).size === lstatSync(target).size &&
468
+ readFileSync(source).equals(readFileSync(target))
469
+ ) {
470
+ unlinkSync(source);
471
+ migrated += 1;
472
+ continue;
473
+ }
474
+ } catch { /* unique name below */ }
475
+ while (existsSync(target)) {
476
+ suffix += 1;
477
+ target = join(durableDir, `${base}.legacy.${suffix}.json`);
478
+ }
479
+ }
480
+ try {
481
+ renameSync(source, target);
482
+ try { chmodSync(target, 0o600); } catch { /* best effort */ }
483
+ migrated += 1;
484
+ continue;
485
+ } catch (err) {
486
+ if (!err || err.code !== "EXDEV") {
487
+ log?.warn?.(` Could not migrate ${source}: ${err.message}`);
488
+ kept += 1;
489
+ continue;
490
+ }
491
+ }
492
+ const tmp = `${target}.tmp`;
493
+ try {
494
+ copyFileSync(source, tmp);
495
+ try { chmodSync(tmp, 0o600); } catch { /* best effort */ }
496
+ const fd = openSync(tmp, "r+");
497
+ try { fsyncSync(fd); } finally { closeSync(fd); }
498
+ renameSync(tmp, target);
499
+ migrated += 1;
500
+ try { unlinkSync(source); } catch { /* dest is in place; leftover source is a duplicate */ }
501
+ } catch (err) {
502
+ try { unlinkSync(tmp); } catch { /* nothing to clean */ }
503
+ log?.warn?.(` Could not migrate ${source}: ${err.message}`);
504
+ kept += 1;
505
+ }
506
+ }
507
+ try {
508
+ const remaining = readdirSync(legacyDir);
509
+ if (remaining.length === 0) rmdirSync(legacyDir);
510
+ } catch { /* best effort */ }
511
+ if (migrated > 0) {
512
+ log?.dim?.(` Migrated ${migrated} legacy OpenCode snapshot(s) to ${durableDir}.`);
513
+ }
514
+ if (kept > 0) {
515
+ log?.warn?.(` Left ${kept} legacy OpenCode snapshot(s) at ${legacyDir}; will retry next install.`);
516
+ }
517
+ return { migrated, kept };
518
+ }
519
+
520
+ function uninstallOpenCodeSkills({ scope, projectRoot, paths, resolveSkillSourcePath }) {
521
+ const skillDir = paths.getSkillsDir(scope, projectRoot);
522
+ for (const skill of SKILLS) {
523
+ const destination = join(skillDir, skill.name);
524
+ const source = resolveSkillSourcePath(skill.name);
525
+ if (
526
+ existsSync(destination) &&
527
+ (readInstallerMarker(destination) || isOwnSymlink(destination, source))
528
+ ) {
529
+ removePath(destination);
530
+ }
531
+ }
532
+ }
package/lib/config.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { readFileSync, writeFileSync, copyFileSync, mkdirSync } from "node:fs";
11
- import { dirname, join } from "node:path";
11
+ import { dirname, isAbsolute, join, resolve } from "node:path";
12
12
  import { homedir } from "node:os";
13
13
 
14
14
  // ---------------------------------------------------------------------------
@@ -108,6 +108,73 @@ function getKiroHome() {
108
108
  return process.env.KIRO_HOME || join(homedir(), ".kiro");
109
109
  }
110
110
 
111
+ /**
112
+ * Root of OpenCode's config tree. Honors $OPENCODE_CONFIG_DIR when set
113
+ * (OpenCode 1.18.x resolves plugins/skills against this root).
114
+ * @returns {string}
115
+ */
116
+ function getOpenCodeHome() {
117
+ return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
118
+ }
119
+
120
+ /** Installer-owned plugin path for the selected scope. */
121
+ export function getOpenCodePluginPath(scope, projectRoot) {
122
+ if (scope === "project") {
123
+ return join(projectRoot, ".opencode", "plugins", "meko.js");
124
+ }
125
+ return join(getOpenCodeHome(), "plugins", "meko.js");
126
+ }
127
+
128
+ /** Stable runtime directory copied by the installer. */
129
+ export function getOpenCodeMekoRoot(scope, projectRoot) {
130
+ if (scope === "project") {
131
+ return join(projectRoot, ".opencode", "meko");
132
+ }
133
+ return join(getOpenCodeHome(), "meko");
134
+ }
135
+
136
+ /** Private hook/capture environment (mode 0600). */
137
+ export function getOpenCodeHookEnvPath(scope, projectRoot) {
138
+ return join(getOpenCodeMekoRoot(scope, projectRoot), "opencode-hook-env.json");
139
+ }
140
+
141
+ /** OpenCode skills directory for the selected scope. */
142
+ export function getOpenCodeSkillsDir(scope, projectRoot) {
143
+ if (scope === "project") {
144
+ return join(projectRoot, ".opencode", "skills");
145
+ }
146
+ return join(getOpenCodeHome(), "skills");
147
+ }
148
+
149
+ /** Hooks-handlers copy root under the installer-owned runtime. */
150
+ export function getOpenCodeHooksInstallRoot(scope, projectRoot) {
151
+ return join(getOpenCodeMekoRoot(scope, projectRoot), "hooks-handlers");
152
+ }
153
+
154
+ /**
155
+ * Resolve the durable Capture v2 watermark root used to store staged
156
+ * snapshots, journals, outboxes, quarantine, and recovery metadata. Mirrors
157
+ * `watermarkDir()` in `skills/hooks-handlers/lib/capture-state.js`. Kept in
158
+ * lockstep so hooks and installer agree on where durable state lives.
159
+ *
160
+ * MEKO-635: OpenCode staged snapshots and journals live under this root, not
161
+ * under the installer-owned OpenCode Meko root. Uninstall preserves them.
162
+ * First install honors `MEKO_WATERMARK_DIR`; later installs reuse the env-file
163
+ * root (uninstall leaves a watermark-only env so reinstall can resume).
164
+ *
165
+ * @returns {string} Absolute path
166
+ */
167
+ export function getCaptureRoot() {
168
+ const configured = process.env.MEKO_WATERMARK_DIR;
169
+ if (configured && String(configured).length > 0) {
170
+ // MEKO-635: persist an absolute path so the installer, plugin, and handler
171
+ // all agree on it even when their cwds differ.
172
+ const value = String(configured);
173
+ return isAbsolute(value) ? value : resolve(value);
174
+ }
175
+ return join(homedir(), ".claude", "meko-capture");
176
+ }
177
+
111
178
  /**
112
179
  * Return path to Kiro's MCP config file.
113
180
  *
package/lib/installer.mjs CHANGED
@@ -67,7 +67,7 @@ export async function install(options) {
67
67
 
68
68
  info("Checking prerequisites...");
69
69
  try {
70
- await adapter.detectPrerequisites({ verbose, log: LOG, scope, projectRoot });
70
+ await adapter.detectPrerequisites({ verbose, log: LOG, scope, projectRoot, dryRun });
71
71
  } catch (err) {
72
72
  error(err.message);
73
73
  throw err;
@@ -134,9 +134,9 @@ export async function install(options) {
134
134
 
135
135
  try {
136
136
  adapter.setHookEnvironment({
137
+ name,
137
138
  url,
138
139
  apiKey,
139
- name,
140
140
  scope,
141
141
  dryRun,
142
142
  verbose,
@@ -153,6 +153,22 @@ export async function install(options) {
153
153
  }
154
154
  currentStep++;
155
155
 
156
+ // Runs even when MCP registration was skipped so a rotated key still
157
+ // reaches the launchd env / shell rc. See yugabyte/meko-mcp-server#337.
158
+ if (typeof adapter.syncCloudAuthEnvironment === "function") {
159
+ try {
160
+ await adapter.syncCloudAuthEnvironment({
161
+ apiKey,
162
+ dryRun,
163
+ verbose,
164
+ log: LOG,
165
+ yes,
166
+ });
167
+ } catch (err) {
168
+ warn(`Cloud auth environment sync failed (non-fatal): ${err.message}`);
169
+ }
170
+ }
171
+
156
172
  if (!skipSkills) {
157
173
  try {
158
174
  const result = adapter.installSkills({