@yugabytedb/meko-mcp 1.0.3

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 (51) hide show
  1. package/README.md +505 -0
  2. package/assets/hooks-handlers/lib/capture.js +863 -0
  3. package/assets/hooks-handlers/lib/checkpoint-timer.js +72 -0
  4. package/assets/hooks-handlers/pre-compact.sh +3 -0
  5. package/assets/hooks-handlers/session-end.sh +27 -0
  6. package/assets/hooks-handlers/session-start.sh +24 -0
  7. package/assets/hooks-handlers/test_single_conversation.sh +169 -0
  8. package/assets/skills/meko-mcp-tools/SKILL.md +192 -0
  9. package/assets/skills/meko-mcp-tools/references/_sections.md +31 -0
  10. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +39 -0
  11. package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +49 -0
  12. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +373 -0
  13. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +56 -0
  14. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +38 -0
  15. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +61 -0
  16. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +128 -0
  17. package/assets/skills/meko-mcp-tools/references/tools-overview.md +97 -0
  18. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +63 -0
  19. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +79 -0
  20. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +143 -0
  21. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +93 -0
  22. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +30 -0
  23. package/bin/create-meko-setup.mjs +499 -0
  24. package/lib/claude-code.mjs +14 -0
  25. package/lib/clients/claude-code.mjs +265 -0
  26. package/lib/clients/claude-desktop.mjs +256 -0
  27. package/lib/clients/common.mjs +107 -0
  28. package/lib/clients/cursor.mjs +332 -0
  29. package/lib/clients/index.mjs +14 -0
  30. package/lib/config.mjs +369 -0
  31. package/lib/detect.mjs +125 -0
  32. package/lib/installer.mjs +255 -0
  33. package/lib/logger.mjs +97 -0
  34. package/lib/mcp-transport.mjs +204 -0
  35. package/lib/migrate/adapters/claude-code.mjs +165 -0
  36. package/lib/migrate/adapters/cursor.mjs +221 -0
  37. package/lib/migrate/adapters/memory-md.mjs +230 -0
  38. package/lib/migrate/claude-parser.mjs +306 -0
  39. package/lib/migrate/daemon.mjs +67 -0
  40. package/lib/migrate/id.mjs +83 -0
  41. package/lib/migrate/lockfile.mjs +121 -0
  42. package/lib/migrate/logger.mjs +79 -0
  43. package/lib/migrate/mcp-client.mjs +224 -0
  44. package/lib/migrate/orchestrator.mjs +443 -0
  45. package/lib/migrate/progress.mjs +76 -0
  46. package/lib/migrate/state.mjs +177 -0
  47. package/lib/migrate.mjs +328 -0
  48. package/lib/prompt.mjs +212 -0
  49. package/lib/validate.mjs +259 -0
  50. package/package.json +49 -0
  51. package/scripts/bundle-assets.mjs +49 -0
@@ -0,0 +1,265 @@
1
+ import { askYesNo } from "../prompt.mjs";
2
+ import {
3
+ getClaudeCodeSettingsPath,
4
+ mergeAllowList,
5
+ mergeSettings,
6
+ readJsonFile,
7
+ removeFromAllowList,
8
+ removeKeys,
9
+ writeJsonFile,
10
+ } from "../config.mjs";
11
+ import { detectClaude, detectExistingMeko } from "../detect.mjs";
12
+ import { redactKey, run } from "./common.mjs";
13
+
14
+ const CLAUDE_INSTALL_URL =
15
+ "https://docs.anthropic.com/en/docs/claude-code/getting-started";
16
+
17
+ // Hook scripts produced by earlier installer versions that may have had inline
18
+ // MEKO_MCP_URL=... / MEKO_API_KEY=... env assignments baked into the command
19
+ // string. Used by stripLiteralUrlFromHooks to recognize and clean them up.
20
+ const MEKO_HOOK_SCRIPTS = ["session-start.sh", "pre-compact.sh", "session-end.sh"];
21
+
22
+ // Meko MCP tools to union into `permissions.allow` so Claude Code resolves
23
+ // them without the deferred-tools ToolSearch round-trip (issue #5). The set
24
+ // is intentionally tight — only tools the agent calls proactively per
25
+ // the meko-mcp-tools skill — to avoid bloating every user's settings.
26
+ //
27
+ // NOTE: `permissions.allow` is verified to cover user-prompt bypass. Whether
28
+ // it also bypasses the deferred-tools schema-fetch gate must be re-verified
29
+ // empirically before this flag is considered load-bearing for #5. Tracked
30
+ // as Phase 10.1 in skills/TEST_PLAN.md.
31
+ const MEKO_ALWAYS_AVAILABLE_TOOLS = [
32
+ "mcp__meko__db_summarize_database",
33
+ "mcp__meko__memory_add",
34
+ "mcp__meko__memory_search",
35
+ "mcp__meko__memory_get_all",
36
+ "mcp__meko__memory_get_by_id",
37
+ "mcp__meko__memory_update",
38
+ "mcp__meko__memory_delete_by_id",
39
+ "mcp__meko__conversation_create",
40
+ "mcp__meko__conversation_add_message",
41
+ ];
42
+
43
+ /**
44
+ * @param {{ getSettingsPath?: () => string }} [options]
45
+ * @returns {object}
46
+ */
47
+ export function createClaudeCodeAdapter(options = {}) {
48
+ const resolveSettingsPath = options.getSettingsPath ?? getClaudeCodeSettingsPath;
49
+
50
+ return {
51
+ id: "claude-code",
52
+ displayName: "Claude Code",
53
+ settingsPath(scope, projectRoot) { // eslint-disable-line no-unused-vars
54
+ return resolveSettingsPath();
55
+ },
56
+ async detectPrerequisites({ verbose, log }) {
57
+ const claude = detectClaude();
58
+ if (!claude.found) {
59
+ throw new Error(
60
+ `Claude Code CLI not found.\nInstall it first: ${CLAUDE_INSTALL_URL}`,
61
+ );
62
+ }
63
+ if (verbose) {
64
+ log.dim(` Claude CLI: ${claude.path} (${claude.version})`);
65
+ }
66
+ },
67
+ detectExisting(name) {
68
+ return detectExistingMeko(name);
69
+ },
70
+ removeRegistration(name) {
71
+ run(`claude mcp remove ${name}`);
72
+ },
73
+ registerServer({ name, url, scope, apiKey, dryRun, verbose, log }) {
74
+ const cmd = this.buildRegisterCommand({ name, url, scope, apiKey });
75
+ if (dryRun) {
76
+ log.dim(` Would run: ${this.buildRegisterDisplayCommand({ name, url, scope, apiKey })}`);
77
+ return;
78
+ }
79
+ const output = this.runRegisterCommand(cmd);
80
+ if (verbose && output) {
81
+ const safeOutput = apiKey ? output.replaceAll(apiKey, "<REDACTED>") : output;
82
+ log.dim(` ${safeOutput}`);
83
+ }
84
+ },
85
+ buildRegisterCommand({ name, url, scope, apiKey }) {
86
+ let cmd = `claude mcp add ${name} ${url} --transport http --scope ${scope}`;
87
+ if (apiKey) {
88
+ cmd += ` -H "API_KEY: ${apiKey}"`;
89
+ }
90
+ return cmd;
91
+ },
92
+ buildRegisterDisplayCommand({ name, url, scope, apiKey }) {
93
+ let displayCmd = `claude mcp add ${name} ${url} --transport http --scope ${scope}`;
94
+ if (apiKey) {
95
+ displayCmd += ` -H "API_KEY: ${redactKey(apiKey)}"`;
96
+ }
97
+ return displayCmd;
98
+ },
99
+ runRegisterCommand(cmd) {
100
+ return run(cmd);
101
+ },
102
+ async maybeConfirmOverwrite({ existingFound, name, yes }) {
103
+ if (!existingFound) return true;
104
+ if (yes) return true;
105
+ return askYesNo(
106
+ `${name} MCP is already configured. Overwrite?`,
107
+ false,
108
+ );
109
+ },
110
+ setHookEnvironment({ url, apiKey, dryRun, verbose, log, settingsPath }) {
111
+ const envPatch = { env: { MEKO_MCP_URL: url } };
112
+ if (apiKey) envPatch.env.MEKO_API_KEY = apiKey;
113
+
114
+ if (dryRun) {
115
+ log.dim(` Would set in ${settingsPath}:`);
116
+ log.dim(` MEKO_MCP_URL = ${url}`);
117
+ if (apiKey) {
118
+ log.dim(` MEKO_API_KEY = ${redactKey(apiKey)}`);
119
+ }
120
+ log.dim(
121
+ ` Would union ${MEKO_ALWAYS_AVAILABLE_TOOLS.length} Meko tools into permissions.allow`,
122
+ );
123
+ return;
124
+ }
125
+
126
+ mergeSettings(settingsPath, envPatch);
127
+ mergeAllowList(settingsPath, "permissions.allow", MEKO_ALWAYS_AVAILABLE_TOOLS);
128
+ const migrated = stripLiteralUrlFromHooks(settingsPath);
129
+ if (verbose) {
130
+ log.dim(` Updated ${settingsPath}`);
131
+ log.dim(` MEKO_MCP_URL = ${url}`);
132
+ if (apiKey) {
133
+ log.dim(` MEKO_API_KEY = ${redactKey(apiKey)}`);
134
+ }
135
+ log.dim(
136
+ ` permissions.allow += ${MEKO_ALWAYS_AVAILABLE_TOOLS.length} Meko tools`,
137
+ );
138
+ if (migrated) {
139
+ log.dim(" Migrated stale hook command literals to env-based form.");
140
+ }
141
+ }
142
+ },
143
+ installSkills({ scope, dryRun, verbose, log }) {
144
+ const marketplaceCmd =
145
+ `claude plugin marketplace add yugabyte/meko-mcp-server --sparse skills --scope ${scope}`;
146
+ const pluginCmd = `claude plugin install meko-agent-skills --scope ${scope}`;
147
+
148
+ if (dryRun) {
149
+ log.dim(` Would run: ${marketplaceCmd}`);
150
+ log.dim(` Would run: ${pluginCmd}`);
151
+ return { marketplace: "dry-run", skills: "dry-run" };
152
+ }
153
+
154
+ let marketplaceStatus = "ok";
155
+ try {
156
+ const output = run(marketplaceCmd);
157
+ if (verbose && output) log.dim(` ${output}`);
158
+ } catch (err) {
159
+ const msg = err.stderr || err.message || "";
160
+ if (msg.includes("already") || msg.includes("exists")) {
161
+ marketplaceStatus = "already";
162
+ } else {
163
+ marketplaceStatus = "error";
164
+ log.warn(`Non-critical: ${msg}`);
165
+ }
166
+ }
167
+
168
+ let skillStatus = "ok";
169
+ try {
170
+ const output = run(pluginCmd);
171
+ if (verbose && output) log.dim(` ${output}`);
172
+ } catch (err) {
173
+ const msg = err.stderr || err.message || "";
174
+ if (msg.includes("already") || msg.includes("installed")) {
175
+ skillStatus = "already";
176
+ } else {
177
+ skillStatus = "error";
178
+ log.warn(`Non-critical: ${msg}`);
179
+ }
180
+ }
181
+
182
+ return { marketplace: marketplaceStatus, skills: skillStatus };
183
+ },
184
+ uninstall({ name, settingsPath }) {
185
+ const results = [];
186
+ try {
187
+ run(`claude mcp remove ${name}`);
188
+ results.push({ key: "mcp", ok: true, message: `MCP server "${name}" removed.` });
189
+ } catch {
190
+ results.push({ key: "mcp", ok: false, message: `MCP server "${name}" was not registered (nothing to remove).` });
191
+ }
192
+
193
+ try {
194
+ removeKeys(settingsPath, ["env.MEKO_MCP_URL", "env.MEKO_API_KEY"]);
195
+ removeFromAllowList(settingsPath, "permissions.allow", MEKO_ALWAYS_AVAILABLE_TOOLS);
196
+ results.push({ key: "env", ok: true, message: "Environment variables and Meko allowlist removed from settings." });
197
+ } catch (err) {
198
+ results.push({ key: "env", ok: false, message: `Could not update settings: ${err.message}` });
199
+ }
200
+
201
+ try {
202
+ run("claude plugin uninstall meko-agent-skills");
203
+ results.push({ key: "skills", ok: true, message: "Skills plugin removed." });
204
+ } catch {
205
+ results.push({ key: "skills", ok: false, message: "Skills plugin was not installed (nothing to remove)." });
206
+ }
207
+
208
+ try {
209
+ run("claude plugin marketplace remove meko-agent-skills");
210
+ results.push({ key: "marketplace", ok: true, message: "Plugin marketplace entry removed." });
211
+ } catch {
212
+ results.push({ key: "marketplace", ok: false, message: "Plugin marketplace entry was not registered (nothing to remove)." });
213
+ }
214
+
215
+ return results;
216
+ },
217
+ async validateConfig({ serverName }) {
218
+ try {
219
+ const output = run("claude mcp list");
220
+ const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
221
+ const found = lines.some((line) => line.split(":")[0].trim() === serverName);
222
+ if (found) return { ok: true, message: `Server "${serverName}" is registered.` };
223
+ return {
224
+ ok: false,
225
+ message: `Server "${serverName}" not found in \`claude mcp list\` output. Run the installer again or add it manually.`,
226
+ };
227
+ } catch (err) {
228
+ return { ok: false, message: `Failed to run \`claude mcp list\`: ${err.message}` };
229
+ }
230
+ },
231
+ };
232
+ }
233
+
234
+ // Rewrites `MEKO_MCP_URL='...' [MEKO_API_KEY='...'] bash "<script>"` hook command
235
+ // strings to `bash "<script>"`, for the three meko hook scripts. Earlier installer
236
+ // versions (cursor.mjs buildScriptCommand) literalized URL+key into the command,
237
+ // which silently shadowed the env block at runtime — see MEKO-20. Runs on every
238
+ // Claude Code install as a one-time migration. Returns true if anything changed.
239
+ export function stripLiteralUrlFromHooks(settingsPath) {
240
+ const cfg = readJsonFile(settingsPath);
241
+ if (!cfg?.hooks || typeof cfg.hooks !== "object") return false;
242
+
243
+ let changed = false;
244
+ for (const eventArr of Object.values(cfg.hooks)) {
245
+ if (!Array.isArray(eventArr)) continue;
246
+ for (const group of eventArr) {
247
+ if (!Array.isArray(group?.hooks)) continue;
248
+ for (const h of group.hooks) {
249
+ if (typeof h?.command !== "string") continue;
250
+ if (!MEKO_HOOK_SCRIPTS.some((s) => h.command.includes(s))) continue;
251
+ const rewritten = h.command.replace(
252
+ /^\s*(?:MEKO_\w+=(?:'[^']*'|"[^"]*"|\S+)\s+)+/,
253
+ "",
254
+ );
255
+ if (rewritten !== h.command) {
256
+ h.command = rewritten;
257
+ changed = true;
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ if (changed) writeJsonFile(settingsPath, cfg);
264
+ return changed;
265
+ }
@@ -0,0 +1,256 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import {
6
+ getClaudeDesktopConfigPath,
7
+ mergeSettings,
8
+ readJsonFile,
9
+ removeKeys,
10
+ } from "../config.mjs";
11
+ import { getRepoRoot, run } from "./common.mjs";
12
+
13
+ /**
14
+ * Claude Desktop adapter.
15
+ *
16
+ * User-scope only. Writes to claude_desktop_config.json via mergeSettings so
17
+ * existing mcpServers entries (and unrelated preferences) are preserved and a
18
+ * .bak backup is left behind on every write.
19
+ *
20
+ * Claude Desktop does not load MCP servers via HTTP directly — its loader only
21
+ * spawns stdio subprocesses. We wrap the HTTP URL with the `mcp-remote` stdio
22
+ * bridge so Desktop treats it as a normal stdio server:
23
+ *
24
+ * "meko": {
25
+ * "command": "npx",
26
+ * "args": ["-y", "mcp-remote", "<url>", "--transport", "http-only",
27
+ * "--header", "API_KEY:${MEKO_API_KEY}"],
28
+ * "env": { "MEKO_API_KEY": "<key>" }
29
+ * }
30
+ *
31
+ * The header uses the `${ENV_VAR}` interpolation form (documented by
32
+ * mcp-remote) with no space around the colon — some clients mis-parse
33
+ * space-containing `--header` args.
34
+ *
35
+ * installSkills() runs skills/scripts/package-desktop-skill.sh to build the
36
+ * .skill bundle and prints import instructions. Desktop has no programmatic
37
+ * skill install API — the user drags the .skill into the app.
38
+ */
39
+ export function createClaudeDesktopAdapter(options = {}) {
40
+ const configPathResolver = options.getConfigPath ?? getClaudeDesktopConfigPath;
41
+ const repoRootResolver = options.getRepoRoot ?? getRepoRoot;
42
+ const packageRunner = options.runPackageScript ?? defaultRunPackageScript;
43
+ const npxProber = options.probeNpx ?? defaultProbeNpx;
44
+
45
+ return {
46
+ id: "claude-desktop",
47
+ displayName: "Claude Desktop",
48
+ settingsPath() {
49
+ return configPathResolver();
50
+ },
51
+ async detectPrerequisites({ log, verbose } = {}) {
52
+ const npx = npxProber();
53
+ if (!npx.found) {
54
+ throw new Error(
55
+ "npx (from Node.js) is required so Claude Desktop can launch the " +
56
+ "mcp-remote stdio bridge. Install Node.js 18+ (which includes npx) " +
57
+ "and re-run the installer.",
58
+ );
59
+ }
60
+ if (verbose && log?.dim) {
61
+ log.dim(` npx: ${npx.path}${npx.version ? ` (${npx.version})` : ""}`);
62
+ }
63
+ },
64
+ detectExisting(name) {
65
+ const cfg = readJsonFile(configPathResolver());
66
+ const entry = cfg?.mcpServers?.[name];
67
+ if (!entry) {
68
+ return { found: false, status: "" };
69
+ }
70
+ // If the existing entry's launcher path looks like a .mcpb extension
71
+ // install, surface that in the status so the installer prompts more
72
+ // loudly before overwriting — users who installed via Settings >
73
+ // Extensions usually don't expect `create-meko-setup` to touch the
74
+ // same config.
75
+ const argsJoined = Array.isArray(entry.args) ? entry.args.join(" ") : "";
76
+ const commandJoined = (entry.command || "") + " " + argsJoined;
77
+ const fromMcpb =
78
+ /\bClaude\b[^ ]*\bExtensions\b/i.test(commandJoined) ||
79
+ /mcp-remote-launcher\.js/.test(commandJoined);
80
+ return {
81
+ found: true,
82
+ status: fromMcpb ? "Configured (via .mcpb extension)" : "Configured",
83
+ };
84
+ },
85
+ removeRegistration(name) {
86
+ removeKeys(configPathResolver(), [`mcpServers.${name}`]);
87
+ },
88
+ registerServer({ name, url, apiKey, dryRun, log }) {
89
+ const path = configPathResolver();
90
+ const server = buildMcpRemoteEntry({ url, apiKey });
91
+
92
+ if (dryRun) {
93
+ log.dim(` Would update ${path}:`);
94
+ log.dim(` mcpServers.${name}.command = ${server.command}`);
95
+ log.dim(` mcpServers.${name}.args = ${JSON.stringify(server.args)}`);
96
+ if (apiKey) {
97
+ log.dim(` mcpServers.${name}.env.MEKO_API_KEY = <set>`);
98
+ }
99
+ return;
100
+ }
101
+
102
+ mergeSettings(path, { mcpServers: { [name]: server } });
103
+ },
104
+ setHookEnvironment({ dryRun, log }) {
105
+ // Claude Desktop has no hook API; the skill carries the proactive-memory
106
+ // behavior instead. Nothing to write here.
107
+ if (dryRun) {
108
+ log.dim(" No hook environment required for Claude Desktop (skill-driven).");
109
+ }
110
+ },
111
+ installSkills({ dryRun, log }) {
112
+ const repoRoot = repoRootResolver();
113
+ const script = join(repoRoot, "skills", "scripts", "package-desktop-skill.sh");
114
+ const output = join(repoRoot, "skills", "meko-mcp-tools-desktop.skill");
115
+
116
+ if (dryRun) {
117
+ log.dim(` Would run: bash ${script}`);
118
+ log.dim(` Would produce: ${output}`);
119
+ return { marketplace: "skipped", skills: "dry-run", output };
120
+ }
121
+
122
+ if (!existsSync(script)) {
123
+ log.warn(
124
+ `Skill packaging script not found at ${script}. ` +
125
+ "Skipping .skill bundle build; install the skill manually via Claude Desktop settings.",
126
+ );
127
+ return { marketplace: "skipped", skills: "script-missing" };
128
+ }
129
+
130
+ try {
131
+ packageRunner(script);
132
+ } catch (err) {
133
+ log.warn(`Skill packaging failed: ${err.message}`);
134
+ return { marketplace: "skipped", skills: "error", error: err.message };
135
+ }
136
+
137
+ if (!existsSync(output)) {
138
+ log.warn(`Expected ${output} after packaging but it was not produced.`);
139
+ return { marketplace: "skipped", skills: "error" };
140
+ }
141
+
142
+ log.info(`Built Meko skill bundle: ${output}`);
143
+ log.info(
144
+ "Install it in Claude Desktop: Settings > Extensions > Import skill, " +
145
+ "or drag the .skill file onto the Claude Desktop window.",
146
+ );
147
+ return { marketplace: "skipped", skills: "ok", output };
148
+ },
149
+ uninstall({ name }) {
150
+ const results = [];
151
+ try {
152
+ this.removeRegistration(name);
153
+ results.push({
154
+ key: "mcp",
155
+ ok: true,
156
+ message: `Claude Desktop MCP server "${name}" removed from ${configPathResolver()}.`,
157
+ });
158
+ } catch (err) {
159
+ results.push({
160
+ key: "mcp",
161
+ ok: false,
162
+ message: `Could not remove Claude Desktop MCP server: ${err.message}`,
163
+ });
164
+ }
165
+ results.push({
166
+ key: "skills",
167
+ ok: true,
168
+ message:
169
+ "Desktop skills are managed via the Desktop UI; " +
170
+ "remove the Meko skill manually if it was imported.",
171
+ });
172
+ return results;
173
+ },
174
+ async validateConfig({ serverName }) {
175
+ const path = configPathResolver();
176
+ const cfg = readJsonFile(path);
177
+ const entry = cfg?.mcpServers?.[serverName];
178
+ if (!entry) {
179
+ return { ok: false, message: `Server "${serverName}" not found in ${path}.` };
180
+ }
181
+
182
+ // Claude Desktop only accepts stdio entries. A top-level `url` field
183
+ // (the HTTP shape) is silently rejected by Desktop's loader with a
184
+ // "not valid MCP server configurations" warning. Reject it here so the
185
+ // installer catches the regression before the user restarts the app.
186
+ if (entry.url && !entry.command) {
187
+ return {
188
+ ok: false,
189
+ message:
190
+ `Server "${serverName}" in ${path} uses the unsupported HTTP shape ` +
191
+ `({ "url": ... }). Claude Desktop requires a stdio command entry. ` +
192
+ `Re-run the installer to rewrite it via mcp-remote.`,
193
+ };
194
+ }
195
+ if (typeof entry.command !== "string" || !Array.isArray(entry.args)) {
196
+ return {
197
+ ok: false,
198
+ message:
199
+ `Server "${serverName}" in ${path} is missing required "command" and "args" fields.`,
200
+ };
201
+ }
202
+
203
+ return { ok: true, message: `Server "${serverName}" is registered in ${path}.` };
204
+ },
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Build a Claude Desktop mcpServers entry that wraps an HTTP MCP URL with the
210
+ * mcp-remote stdio bridge.
211
+ *
212
+ * Exported for tests.
213
+ *
214
+ * @param {{url: string, apiKey: string|null}} params
215
+ * @returns {{command: string, args: string[], env?: Record<string,string>}}
216
+ */
217
+ export function buildMcpRemoteEntry({ url, apiKey }) {
218
+ const args = ["-y", "mcp-remote", url, "--transport", "http-only"];
219
+ const entry = { command: "npx", args };
220
+ if (apiKey) {
221
+ // `API_KEY:${MEKO_API_KEY}` with no space around the colon — mcp-remote
222
+ // docs call out a spacing bug in some clients' arg parsing. The header
223
+ // name matches what the Meko server expects (see capture.js mcpPost).
224
+ args.push("--header", "API_KEY:${MEKO_API_KEY}");
225
+ entry.env = { MEKO_API_KEY: apiKey };
226
+ }
227
+ return entry;
228
+ }
229
+
230
+ function defaultRunPackageScript(scriptPath) {
231
+ if (!statSync(scriptPath).isFile()) {
232
+ throw new Error(`Not a file: ${scriptPath}`);
233
+ }
234
+ execFileSync("bash", [scriptPath], { stdio: ["ignore", "pipe", "pipe"] });
235
+ }
236
+
237
+ /**
238
+ * Check whether `npx` is available on PATH.
239
+ * @returns {{found: boolean, path: string, version: string}}
240
+ */
241
+ function defaultProbeNpx() {
242
+ const result = { found: false, path: "", version: "" };
243
+ try {
244
+ result.path = run("command -v npx");
245
+ } catch {
246
+ return result;
247
+ }
248
+ if (!result.path) return result;
249
+ try {
250
+ result.version = run("npx --version");
251
+ } catch {
252
+ // Path found but version probe failed — still usable.
253
+ }
254
+ result.found = true;
255
+ return result;
256
+ }
@@ -0,0 +1,107 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, symlinkSync, cpSync, lstatSync, unlinkSync, rmSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ /**
7
+ * @returns {string} Absolute repository root path.
8
+ */
9
+ export function getRepoRoot() {
10
+ const libDir = dirname(fileURLToPath(import.meta.url));
11
+ return resolve(libDir, "..", "..", "..");
12
+ }
13
+
14
+ /**
15
+ * Locate the root of installable assets (skills, hook handlers).
16
+ *
17
+ * When the installer is published to npm, `prepack` copies the assets into
18
+ * `installer/assets/`; that path wins because it guarantees the shipped
19
+ * version of each skill/hook. When running from a source checkout without a
20
+ * packed `assets/` dir, fall back to the repo's `skills/` directory.
21
+ *
22
+ * Layout under both roots mirrors the source tree — callers use
23
+ * `resolveSkillSource`/`resolveHooksRoot` rather than building paths manually.
24
+ *
25
+ * @returns {string} Absolute path to the asset root.
26
+ */
27
+ export function getAssetRoot() {
28
+ const libDir = dirname(fileURLToPath(import.meta.url));
29
+ const packed = resolve(libDir, "..", "..", "assets");
30
+ if (existsSync(packed)) return packed;
31
+ return join(getRepoRoot(), "skills");
32
+ }
33
+
34
+ /**
35
+ * @param {string} name - skill directory name (e.g. "meko-mcp-tools")
36
+ * @returns {string} absolute path to the skill source directory
37
+ */
38
+ export function resolveSkillSource(name) {
39
+ return join(getAssetRoot(), "skills", name);
40
+ }
41
+
42
+ /**
43
+ * @returns {string} absolute path to the hooks-handlers directory
44
+ */
45
+ export function resolveHooksRoot() {
46
+ return join(getAssetRoot(), "hooks-handlers");
47
+ }
48
+
49
+ /**
50
+ * Run a shell command and return stdout.
51
+ * @param {string} cmd
52
+ * @returns {string}
53
+ */
54
+ export function run(cmd) {
55
+ return execSync(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
56
+ }
57
+
58
+ /**
59
+ * @param {string|null|undefined} apiKey
60
+ * @returns {string}
61
+ */
62
+ export function redactKey(apiKey) {
63
+ return apiKey ? "<set>" : "<not set>";
64
+ }
65
+
66
+ /**
67
+ * Safely quote a string for shell usage.
68
+ * @param {string} value
69
+ * @returns {string}
70
+ */
71
+ export function shellQuote(value) {
72
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
73
+ }
74
+
75
+ /**
76
+ * Create symlink when possible; fallback to copy.
77
+ * @param {string} source
78
+ * @param {string} destination
79
+ * @returns {"symlink"|"copy"}
80
+ */
81
+ export function symlinkOrCopy(source, destination) {
82
+ mkdirSync(dirname(destination), { recursive: true });
83
+ try {
84
+ symlinkSync(source, destination, "junction");
85
+ return "symlink";
86
+ } catch {
87
+ cpSync(source, destination, { recursive: true });
88
+ return "copy";
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Remove path if it is a symlink or directory/file.
94
+ * @param {string} targetPath
95
+ */
96
+ export function removePath(targetPath) {
97
+ try {
98
+ const stat = lstatSync(targetPath);
99
+ if (stat.isSymbolicLink()) {
100
+ unlinkSync(targetPath);
101
+ return;
102
+ }
103
+ rmSync(targetPath, { recursive: true, force: true });
104
+ } catch {
105
+ // no-op
106
+ }
107
+ }