@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.
- package/README.md +106 -38
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
- package/assets/hooks-handlers/lib/capture-notices.js +131 -14
- package/assets/hooks-handlers/lib/capture-state.js +95 -1
- package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
- package/assets/hooks-handlers/lib/capture.js +1237 -137
- package/assets/hooks-handlers/lib/checkpoint-timer.js +51 -10
- package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
- package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
- package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
- package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
- package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
- package/assets/opencode-plugin/meko.js +213 -0
- package/assets/scripts/meko-statusline.sh +85 -18
- package/assets/skills/meko-mcp-tools/SKILL.md +57 -14
- package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +86 -14
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +29 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +8 -4
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +32 -22
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +45 -1
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +54 -18
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +8 -6
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +95 -33
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +14 -10
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +35 -25
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +5 -5
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
- package/bin/create-meko-setup.mjs +70 -7
- package/lib/capture-cleanup.mjs +63 -0
- package/lib/clients/claude-code.mjs +1 -0
- package/lib/clients/codex.mjs +80 -35
- package/lib/clients/common.mjs +7 -0
- package/lib/clients/index.mjs +2 -0
- package/lib/clients/kiro.mjs +5 -5
- package/lib/clients/opencode.mjs +532 -0
- package/lib/config.mjs +68 -1
- package/lib/installer.mjs +18 -2
- package/lib/migrate/adapters/claude-code.mjs +11 -2
- package/lib/migrate/adapters/codex.mjs +9 -2
- package/lib/migrate/adapters/memory-md.mjs +27 -8
- package/lib/migrate/adapters/opencode.mjs +148 -0
- package/lib/migrate/claude-parser.mjs +153 -9
- package/lib/migrate/id.mjs +195 -13
- package/lib/migrate/mcp-client.mjs +25 -1
- package/lib/migrate/opencode-normalizer.mjs +132 -0
- package/lib/migrate/orchestrator.mjs +293 -77
- package/lib/migrate/progress.mjs +239 -5
- package/lib/migrate/registry.mjs +72 -0
- package/lib/migrate.mjs +96 -8
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +4 -0
package/lib/clients/kiro.mjs
CHANGED
|
@@ -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
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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}
|
|
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({
|