@yugabytedb/meko-mcp 2.1.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 (70) hide show
  1. package/README.md +233 -40
  2. package/assets/hooks-handlers/codex-session-end.sh +4 -69
  3. package/assets/hooks-handlers/cursor-session-start.sh +19 -1
  4. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
  5. package/assets/hooks-handlers/lib/capture-notices.js +438 -0
  6. package/assets/hooks-handlers/lib/capture-state.js +2173 -0
  7. package/assets/hooks-handlers/lib/capture-status-cli.js +86 -0
  8. package/assets/hooks-handlers/lib/capture.js +2782 -403
  9. package/assets/hooks-handlers/lib/checkpoint-timer.js +485 -81
  10. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  11. package/assets/hooks-handlers/lib/kiro-handler.js +760 -0
  12. package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
  13. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  14. package/assets/hooks-handlers/lib/terminal-handoff.js +427 -0
  15. package/assets/hooks-handlers/session-end.sh +3 -34
  16. package/assets/opencode-plugin/meko.js +213 -0
  17. package/assets/scripts/meko-statusline.sh +123 -16
  18. package/assets/skills/meko-mcp-tools/SKILL.md +116 -126
  19. package/assets/skills/meko-mcp-tools/references/_sections.md +3 -3
  20. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +25 -18
  21. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +99 -24
  22. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
  23. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +53 -6
  24. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +25 -79
  25. package/assets/skills/meko-mcp-tools/references/tools-overview.md +37 -26
  26. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
  27. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +66 -1
  28. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +7 -40
  29. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +56 -20
  30. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
  31. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +15 -8
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +99 -36
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +18 -14
  36. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +39 -28
  37. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
  38. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
  39. package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
  40. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
  41. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
  42. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
  43. package/bin/create-meko-setup.mjs +113 -12
  44. package/lib/capture-cleanup.mjs +63 -0
  45. package/lib/capture-status.mjs +67 -0
  46. package/lib/clients/aliases.mjs +5 -0
  47. package/lib/clients/claude-code.mjs +1 -0
  48. package/lib/clients/claude-desktop.mjs +120 -5
  49. package/lib/clients/codex.mjs +85 -37
  50. package/lib/clients/common.mjs +7 -0
  51. package/lib/clients/index.mjs +4 -0
  52. package/lib/clients/kiro.mjs +705 -0
  53. package/lib/clients/opencode.mjs +532 -0
  54. package/lib/config.mjs +158 -1
  55. package/lib/installer.mjs +18 -1
  56. package/lib/migrate/adapters/claude-code.mjs +11 -2
  57. package/lib/migrate/adapters/codex.mjs +497 -0
  58. package/lib/migrate/adapters/memory-md.mjs +27 -8
  59. package/lib/migrate/adapters/opencode.mjs +148 -0
  60. package/lib/migrate/claude-parser.mjs +153 -9
  61. package/lib/migrate/id.mjs +217 -10
  62. package/lib/migrate/mcp-client.mjs +25 -1
  63. package/lib/migrate/opencode-normalizer.mjs +132 -0
  64. package/lib/migrate/orchestrator.mjs +299 -56
  65. package/lib/migrate/progress.mjs +239 -5
  66. package/lib/migrate/registry.mjs +72 -0
  67. package/lib/migrate.mjs +100 -10
  68. package/lib/prompt.mjs +16 -1
  69. package/package.json +4 -4
  70. package/scripts/bundle-assets.mjs +4 -0
@@ -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
  // ---------------------------------------------------------------------------
@@ -96,6 +96,163 @@ export function getClaudeCompatSettingsPath(scope, projectRoot) {
96
96
  return getClaudeCodeSettingsPath();
97
97
  }
98
98
 
99
+ /**
100
+ * Root of Kiro's global config tree.
101
+ *
102
+ * Kiro documents $KIRO_HOME as the override every global surface resolves
103
+ * against (agents, skills, steering, settings, sessions), so honoring it here
104
+ * keeps all Kiro path helpers consistent with the IDE and `kiro-cli`.
105
+ * @returns {string}
106
+ */
107
+ function getKiroHome() {
108
+ return process.env.KIRO_HOME || join(homedir(), ".kiro");
109
+ }
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
+
178
+ /**
179
+ * Return path to Kiro's MCP config file.
180
+ *
181
+ * Kiro merges MCP scopes agent -> project -> global, and both the IDE and
182
+ * `kiro-cli` read the same files, so one pair of paths covers both surfaces.
183
+ * @param {"user"|"project"} scope
184
+ * @param {string} projectRoot
185
+ * @returns {string}
186
+ */
187
+ export function getKiroMcpPath(scope, projectRoot) {
188
+ if (scope === "project") {
189
+ return join(projectRoot, ".kiro", "settings", "mcp.json");
190
+ }
191
+ return join(getKiroHome(), "settings", "mcp.json");
192
+ }
193
+
194
+ /** Return the directory containing standalone Kiro hook definitions. */
195
+ export function getKiroHooksDir(scope, projectRoot) {
196
+ if (scope === "project") {
197
+ return join(projectRoot, ".kiro", "hooks");
198
+ }
199
+ return join(getKiroHome(), "hooks");
200
+ }
201
+
202
+ /** Return the installer-owned, legacy CLI 2.x managed-agent path. */
203
+ export function getKiroManagedAgentPath(scope, projectRoot) {
204
+ if (scope === "project") {
205
+ return join(projectRoot, ".kiro", "agents", "meko-default.json");
206
+ }
207
+ return join(getKiroHome(), "agents", "meko-default.json");
208
+ }
209
+
210
+ /**
211
+ * Return Kiro's CLI settings path for the selected installer scope.
212
+ * CLI 2.x stores dotted setting names as top-level JSON keys.
213
+ */
214
+ export function getKiroCliSettingsPath(scope, projectRoot) {
215
+ if (scope === "project") {
216
+ return join(projectRoot, ".kiro", "settings", "cli.json");
217
+ }
218
+ return join(getKiroHome(), "settings", "cli.json");
219
+ }
220
+
221
+ /**
222
+ * Return path to the Meko-owned Kiro steering file.
223
+ *
224
+ * Kiro reads every file in `steering/` on each interaction, which makes it the
225
+ * analogue of Codex's AGENTS.md: the place to state behavior the agent must
226
+ * follow. Because steering is a *directory*, we own `meko.md` outright instead
227
+ * of upserting a fenced block into a file the user also edits.
228
+ * @param {"user"|"project"} scope
229
+ * @param {string} projectRoot
230
+ * @returns {string}
231
+ */
232
+ export function getKiroSteeringPath(scope, projectRoot) {
233
+ if (scope === "project") {
234
+ return join(projectRoot, ".kiro", "steering", "meko.md");
235
+ }
236
+ return join(getKiroHome(), "steering", "meko.md");
237
+ }
238
+
239
+ /**
240
+ * Return path to Kiro's skills directory.
241
+ *
242
+ * Kiro implements the Agent Skills standard (SKILL.md + name/description
243
+ * frontmatter), which is the same format our skills already ship in, so they
244
+ * install by copy with no transformation.
245
+ * @param {"user"|"project"} scope
246
+ * @param {string} projectRoot
247
+ * @returns {string}
248
+ */
249
+ export function getKiroSkillsDir(scope, projectRoot) {
250
+ if (scope === "project") {
251
+ return join(projectRoot, ".kiro", "skills");
252
+ }
253
+ return join(getKiroHome(), "skills");
254
+ }
255
+
99
256
  /**
100
257
  * Return path to Codex's user-level config (TOML).
101
258
  * Honors $CODEX_HOME if set, else defaults to ~/.codex/config.toml.