@yugabytedb/meko-mcp 1.0.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -44
- package/assets/hooks-handlers/before-submit-prompt.sh +44 -0
- package/assets/hooks-handlers/cursor-session-start.sh +42 -0
- package/assets/hooks-handlers/lib/capture.js +457 -107
- package/assets/hooks-handlers/lib/package.json +1 -0
- package/assets/hooks-handlers/test_single_conversation.sh +1 -1
- package/assets/scripts/package-desktop-skill.sh +36 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +167 -53
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +84 -22
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +89 -56
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +17 -9
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +94 -5
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +52 -37
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +108 -37
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +6 -10
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +30 -18
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +55 -69
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +251 -0
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +150 -0
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +24 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +99 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +406 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +69 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +217 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +112 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +134 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +75 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +155 -0
- package/bin/create-meko-setup.mjs +151 -22
- package/lib/clients/claude-code.mjs +378 -68
- package/lib/clients/claude-desktop.mjs +45 -8
- package/lib/clients/codex.mjs +1209 -0
- package/lib/clients/common.mjs +223 -1
- package/lib/clients/cursor.mjs +118 -13
- package/lib/clients/index.mjs +2 -0
- package/lib/config.mjs +56 -0
- package/lib/detect.mjs +52 -44
- package/lib/installer.mjs +11 -4
- package/lib/mcp-transport.mjs +61 -2
- package/lib/migrate/adapters/memory-md.mjs +13 -6
- package/lib/migrate/datapack.mjs +167 -0
- package/lib/migrate/id.mjs +127 -9
- package/lib/migrate/orchestrator.mjs +68 -10
- package/lib/migrate.mjs +346 -3
- package/lib/prompt.mjs +22 -4
- package/lib/validate.mjs +313 -9
- package/lib/version-check.mjs +178 -0
- package/package.json +2 -1
- package/scripts/bundle-assets.mjs +8 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
1
5
|
import { askYesNo } from "../prompt.mjs";
|
|
2
6
|
import {
|
|
3
7
|
getClaudeCodeSettingsPath,
|
|
@@ -8,17 +12,36 @@ import {
|
|
|
8
12
|
removeKeys,
|
|
9
13
|
writeJsonFile,
|
|
10
14
|
} from "../config.mjs";
|
|
11
|
-
import { detectClaude, detectExistingMeko } from "../detect.mjs";
|
|
12
|
-
import {
|
|
15
|
+
import { detectClaude, detectExistingMeko, getClaudeCodeConfigPath } from "../detect.mjs";
|
|
16
|
+
import {
|
|
17
|
+
redactKey,
|
|
18
|
+
resolveHooksRoot,
|
|
19
|
+
run,
|
|
20
|
+
shellQuote,
|
|
21
|
+
writeInstallerMarker,
|
|
22
|
+
} from "./common.mjs";
|
|
13
23
|
|
|
14
24
|
const CLAUDE_INSTALL_URL =
|
|
15
25
|
"https://docs.anthropic.com/en/docs/claude-code/getting-started";
|
|
16
26
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
27
|
+
// Filenames of the Meko-owned hook scripts. The meko-agent-skills plugin
|
|
28
|
+
// registers these at runtime via ${CLAUDE_PLUGIN_ROOT} (skills/hooks/hooks.json),
|
|
29
|
+
// so any entry still present in ~/.claude/settings.json that references one of
|
|
30
|
+
// these filenames is stale — either a pre-plugin absolute-path install that
|
|
31
|
+
// accumulated across install locations (issue #60), or a legacy env-prefixed
|
|
32
|
+
// command rewritten from MEKO-20. pruneMekoHookEntries uses this list to
|
|
33
|
+
// identify and remove those stale entries.
|
|
20
34
|
const MEKO_HOOK_SCRIPTS = ["session-start.sh", "pre-compact.sh", "session-end.sh"];
|
|
21
35
|
|
|
36
|
+
// Qualified `plugin@marketplace` form. CLI 2.1.x's `plugin update` and
|
|
37
|
+
// `plugin uninstall` require this exact shape — the bare name fails with
|
|
38
|
+
// `Plugin "meko-agent-skills" not found` even when the plugin is installed
|
|
39
|
+
// (issues #59 / #63). `plugin install` fuzzy-resolves the bare name but we
|
|
40
|
+
// keep the shape consistent so every call site agrees. The marketplace happens
|
|
41
|
+
// to share the plugin's name (see extraKnownMarketplaces in settings.json).
|
|
42
|
+
const MEKO_PLUGIN_REF = "meko-agent-skills@meko-agent-skills";
|
|
43
|
+
const MEKO_MARKETPLACE_NAME = "meko-agent-skills";
|
|
44
|
+
|
|
22
45
|
// Meko MCP tools to union into `permissions.allow` so Claude Code resolves
|
|
23
46
|
// them without the deferred-tools ToolSearch round-trip (issue #5). The set
|
|
24
47
|
// is intentionally tight — only tools the agent calls proactively per
|
|
@@ -41,11 +64,24 @@ const MEKO_ALWAYS_AVAILABLE_TOOLS = [
|
|
|
41
64
|
];
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
|
-
* @param {{
|
|
67
|
+
* @param {{
|
|
68
|
+
* getSettingsPath?: () => string,
|
|
69
|
+
* getConfigPath?: () => string,
|
|
70
|
+
* getMekoCaptureDir?: () => string,
|
|
71
|
+
* runCommand?: (cmd: string) => string,
|
|
72
|
+
* }} [options]
|
|
45
73
|
* @returns {object}
|
|
46
74
|
*/
|
|
47
75
|
export function createClaudeCodeAdapter(options = {}) {
|
|
48
76
|
const resolveSettingsPath = options.getSettingsPath ?? getClaudeCodeSettingsPath;
|
|
77
|
+
const resolveConfigPath = options.getConfigPath ?? getClaudeCodeConfigPath;
|
|
78
|
+
const resolveMekoCaptureDir =
|
|
79
|
+
options.getMekoCaptureDir ??
|
|
80
|
+
(() => join(homedir(), ".claude", "meko-capture"));
|
|
81
|
+
// DI hook: tests substitute a fake shell so installSkills() branching
|
|
82
|
+
// (probe -> update vs install, fallback on old CLIs) is verifiable
|
|
83
|
+
// without invoking a real `claude` binary.
|
|
84
|
+
const runCommand = options.runCommand ?? run;
|
|
49
85
|
|
|
50
86
|
return {
|
|
51
87
|
id: "claude-code",
|
|
@@ -68,11 +104,11 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
68
104
|
isInstalled() {
|
|
69
105
|
return detectClaude().found;
|
|
70
106
|
},
|
|
71
|
-
detectExisting(name) {
|
|
72
|
-
return detectExistingMeko(name);
|
|
107
|
+
detectExisting(name, { scope = "user", projectRoot } = {}) {
|
|
108
|
+
return detectExistingMeko(name, { scope, projectRoot });
|
|
73
109
|
},
|
|
74
|
-
removeRegistration(name) {
|
|
75
|
-
|
|
110
|
+
removeRegistration(name, { scope = "user" } = {}) {
|
|
111
|
+
runCommand(`claude mcp remove --scope ${shellQuote(scope)} ${shellQuote(name)}`);
|
|
76
112
|
},
|
|
77
113
|
registerServer({ name, url, scope, apiKey, dryRun, verbose, log }) {
|
|
78
114
|
const cmd = this.buildRegisterCommand({ name, url, scope, apiKey });
|
|
@@ -87,18 +123,21 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
87
123
|
}
|
|
88
124
|
},
|
|
89
125
|
buildRegisterCommand({ name, url, scope, apiKey }) {
|
|
90
|
-
let cmd =
|
|
126
|
+
let cmd =
|
|
127
|
+
`claude mcp add --transport http --scope ${shellQuote(scope)} ` +
|
|
128
|
+
`${shellQuote(name)} ${shellQuote(url)}`;
|
|
91
129
|
if (apiKey) {
|
|
92
|
-
cmd += `
|
|
130
|
+
cmd += ` --header ${shellQuote(`Authorization: Bearer ${apiKey}`)}`;
|
|
93
131
|
}
|
|
94
132
|
return cmd;
|
|
95
133
|
},
|
|
96
134
|
buildRegisterDisplayCommand({ name, url, scope, apiKey }) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
return this.buildRegisterCommand({
|
|
136
|
+
name,
|
|
137
|
+
url,
|
|
138
|
+
scope,
|
|
139
|
+
apiKey: apiKey ? redactKey(apiKey) : null,
|
|
140
|
+
});
|
|
102
141
|
},
|
|
103
142
|
runRegisterCommand(cmd) {
|
|
104
143
|
return run(cmd);
|
|
@@ -129,7 +168,34 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
129
168
|
|
|
130
169
|
mergeSettings(settingsPath, envPatch);
|
|
131
170
|
mergeAllowList(settingsPath, "permissions.allow", MEKO_ALWAYS_AVAILABLE_TOOLS);
|
|
132
|
-
const
|
|
171
|
+
const pruned = pruneMekoHookEntries(settingsPath);
|
|
172
|
+
if (pruned) {
|
|
173
|
+
// The prune removed at least one stale Meko hook entry. On the common
|
|
174
|
+
// path these were frozen capture.js copies from prior install roots
|
|
175
|
+
// (issue #60). On the edge case where Cursor claude-compat installed
|
|
176
|
+
// from a DIFFERENT root than this Claude Code install, those Cursor
|
|
177
|
+
// entries are indistinguishable from stale Claude Code entries and
|
|
178
|
+
// were also removed — warn the user so they can re-run the Cursor
|
|
179
|
+
// installer if they notice Cursor hooks missing.
|
|
180
|
+
log.warn(
|
|
181
|
+
"Removed stale Meko hook entries from settings.json. If you use Cursor claude-compat from a different install path, re-run the Cursor installer to restore its hooks.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Drop an installer-version marker in the meko-capture watermark dir so
|
|
186
|
+
// future installer runs can tell which version last touched this
|
|
187
|
+
// machine. This is purely for diagnostics / future upgrade logic —
|
|
188
|
+
// capture.js itself ignores the marker.
|
|
189
|
+
const mekoCaptureDir = resolveMekoCaptureDir();
|
|
190
|
+
try {
|
|
191
|
+
mkdirSync(mekoCaptureDir, { recursive: true });
|
|
192
|
+
writeInstallerMarker(mekoCaptureDir);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (verbose) {
|
|
195
|
+
log.dim(` Could not write meko-capture marker: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
133
199
|
if (verbose) {
|
|
134
200
|
log.dim(` Updated ${settingsPath}`);
|
|
135
201
|
log.dim(` MEKO_MCP_URL = ${url}`);
|
|
@@ -139,9 +205,6 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
139
205
|
log.dim(
|
|
140
206
|
` permissions.allow += ${MEKO_ALWAYS_AVAILABLE_TOOLS.length} Meko tools`,
|
|
141
207
|
);
|
|
142
|
-
if (migrated) {
|
|
143
|
-
log.dim(" Migrated stale hook command literals to env-based form.");
|
|
144
|
-
}
|
|
145
208
|
}
|
|
146
209
|
},
|
|
147
210
|
installSkills({ scope, dryRun, verbose, log }) {
|
|
@@ -152,17 +215,28 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
152
215
|
// succeeds today.
|
|
153
216
|
const marketplaceCmd =
|
|
154
217
|
`claude plugin marketplace add yugabyte/meko-mcp-server --scope ${scope}`;
|
|
155
|
-
|
|
218
|
+
// `claude plugin list` dropped --scope in CLI 2.1.x; it prints all
|
|
219
|
+
// scopes. The match-by-token regex used below is scope-agnostic by
|
|
220
|
+
// design — `plugin update` failing with "not found" on a
|
|
221
|
+
// different-scope install is handled in the fallback chain.
|
|
222
|
+
const listCmd = `claude plugin list`;
|
|
223
|
+
// Qualified `plugin@marketplace` form — see MEKO_PLUGIN_REF comment
|
|
224
|
+
// at the top of this file.
|
|
225
|
+
const installCmd = `claude plugin install ${MEKO_PLUGIN_REF} --scope ${scope}`;
|
|
226
|
+
const updateCmd = `claude plugin update ${MEKO_PLUGIN_REF} --scope ${scope}`;
|
|
227
|
+
const uninstallCmd = `claude plugin uninstall ${MEKO_PLUGIN_REF} --scope ${scope}`;
|
|
156
228
|
|
|
157
229
|
if (dryRun) {
|
|
158
230
|
log.dim(` Would run: ${marketplaceCmd}`);
|
|
159
|
-
log.dim(` Would run: ${
|
|
231
|
+
log.dim(` Would run: ${listCmd}`);
|
|
232
|
+
log.dim(` Would run: ${updateCmd} (if already installed)`);
|
|
233
|
+
log.dim(` Would run: ${installCmd} (if not yet installed)`);
|
|
160
234
|
return { marketplace: "dry-run", skills: "dry-run" };
|
|
161
235
|
}
|
|
162
236
|
|
|
163
237
|
let marketplaceStatus = "ok";
|
|
164
238
|
try {
|
|
165
|
-
const output =
|
|
239
|
+
const output = runCommand(marketplaceCmd);
|
|
166
240
|
if (verbose && output) log.dim(` ${output}`);
|
|
167
241
|
} catch (err) {
|
|
168
242
|
const msg = err.stderr || err.message || "";
|
|
@@ -175,27 +249,130 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
175
249
|
}
|
|
176
250
|
}
|
|
177
251
|
|
|
178
|
-
|
|
252
|
+
// Probe for an existing install so we can call `plugin update` instead
|
|
253
|
+
// of `plugin install` (the latter no-ops on newer Claude CLIs when the
|
|
254
|
+
// plugin is already present — which means users running
|
|
255
|
+
// `npx @yugabytedb/meko-mcp@latest` never pull updated skill content).
|
|
256
|
+
let alreadyInstalled = false;
|
|
179
257
|
try {
|
|
180
|
-
const
|
|
181
|
-
if (verbose &&
|
|
258
|
+
const listOutput = runCommand(listCmd);
|
|
259
|
+
if (verbose && listOutput) log.dim(` ${listOutput}`);
|
|
260
|
+
// Match only when "meko-agent-skills" appears as a standalone token.
|
|
261
|
+
// Leading anchor (^|\s) prevents false positives on substrings like
|
|
262
|
+
// `not-meko-agent-skills`. Trailing anchor accepts whitespace, EOL,
|
|
263
|
+
// or `@` — CLI 2.1.x formats plugins as `name@marketplace`, so the
|
|
264
|
+
// plugin appears as `meko-agent-skills@meko-agent-skills` in the
|
|
265
|
+
// probe output. `\b` is too loose because `-` counts as a word
|
|
266
|
+
// boundary, so `\bmeko-agent-skills\b` would match inside
|
|
267
|
+
// `not-meko-agent-skills`.
|
|
268
|
+
//
|
|
269
|
+
// This is deliberately scope-agnostic — `plugin list` prints all
|
|
270
|
+
// scopes. If the plugin exists at a scope other than the one we're
|
|
271
|
+
// installing into, `plugin update --scope ${scope}` will fail with
|
|
272
|
+
// "Plugin not found" and the `not found` branch in the update
|
|
273
|
+
// catch-block below falls through to `plugin install`.
|
|
274
|
+
alreadyInstalled = /(^|\s)meko-agent-skills(\s|@|$)/m.test(listOutput);
|
|
182
275
|
} catch (err) {
|
|
276
|
+
// If the probe fails we fall through to `plugin install`, which is
|
|
277
|
+
// the original behavior — safer than assuming a state we can't see.
|
|
183
278
|
const msg = err.stderr || err.message || "";
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
279
|
+
if (verbose) log.dim(` plugin list probe failed: ${msg.trim()}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let skillStatus = "ok";
|
|
283
|
+
if (alreadyInstalled) {
|
|
284
|
+
try {
|
|
285
|
+
const output = runCommand(updateCmd);
|
|
286
|
+
if (verbose && output) log.dim(` ${output}`);
|
|
287
|
+
log.dim(" Updated meko-agent-skills plugin");
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const msg = err.stderr || err.message || "";
|
|
290
|
+
// Older Claude CLIs don't know `plugin update`. The exact wording
|
|
291
|
+
// varies (`unknown command`, `unrecognized option`, `invalid`,
|
|
292
|
+
// `command not found`) so we match a family of signals and fall
|
|
293
|
+
// back to uninstall + install, which achieves the same refresh.
|
|
294
|
+
if (
|
|
295
|
+
msg.includes("unknown command") ||
|
|
296
|
+
msg.includes("unrecognized") ||
|
|
297
|
+
msg.includes("invalid command") ||
|
|
298
|
+
msg.includes("command not found") ||
|
|
299
|
+
msg.includes("Unknown subcommand")
|
|
300
|
+
) {
|
|
301
|
+
log.warn(" claude plugin update not supported — falling back to uninstall + install");
|
|
302
|
+
try {
|
|
303
|
+
const unOutput = runCommand(uninstallCmd);
|
|
304
|
+
if (verbose && unOutput) log.dim(` ${unOutput}`);
|
|
305
|
+
} catch (unErr) {
|
|
306
|
+
if (verbose) {
|
|
307
|
+
const unMsg = unErr.stderr || unErr.message || "";
|
|
308
|
+
log.dim(` plugin uninstall (fallback) failed: ${unMsg.trim()}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const inOutput = runCommand(installCmd);
|
|
313
|
+
if (verbose && inOutput) log.dim(` ${inOutput}`);
|
|
314
|
+
log.dim(" Installed meko-agent-skills plugin");
|
|
315
|
+
} catch (inErr) {
|
|
316
|
+
const inMsg = inErr.stderr || inErr.message || "";
|
|
317
|
+
skillStatus = "error";
|
|
318
|
+
log.warn("Skills plugin install skipped (non-critical).");
|
|
319
|
+
log.dim(inMsg.trim());
|
|
320
|
+
}
|
|
321
|
+
} else if (msg.includes("not found")) {
|
|
322
|
+
// `plugin list` matched the plugin at some scope, but
|
|
323
|
+
// `plugin update --scope ${scope}` can't see it — the install
|
|
324
|
+
// lives at a different scope. Fall through to `plugin install`
|
|
325
|
+
// at the requested scope, which is what the user actually
|
|
326
|
+
// asked for. (Also covers rare probe false-positives where
|
|
327
|
+
// the regex matched but the plugin isn't really installed
|
|
328
|
+
// anywhere visible to update.)
|
|
329
|
+
log.dim(` plugin update reports not found at scope ${scope} — installing instead`);
|
|
330
|
+
try {
|
|
331
|
+
const inOutput = runCommand(installCmd);
|
|
332
|
+
if (verbose && inOutput) log.dim(` ${inOutput}`);
|
|
333
|
+
log.dim(" Installed meko-agent-skills plugin");
|
|
334
|
+
} catch (inErr) {
|
|
335
|
+
const inMsg = inErr.stderr || inErr.message || "";
|
|
336
|
+
if (inMsg.includes("already") || inMsg.includes("installed")) {
|
|
337
|
+
skillStatus = "already";
|
|
338
|
+
} else {
|
|
339
|
+
skillStatus = "error";
|
|
340
|
+
log.warn("Skills plugin install skipped (non-critical).");
|
|
341
|
+
log.dim(inMsg.trim());
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
skillStatus = "error";
|
|
346
|
+
log.warn("Skills plugin update skipped (non-critical).");
|
|
347
|
+
log.dim(msg.trim());
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
try {
|
|
352
|
+
const output = runCommand(installCmd);
|
|
353
|
+
if (verbose && output) log.dim(` ${output}`);
|
|
354
|
+
log.dim(" Installed meko-agent-skills plugin");
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const msg = err.stderr || err.message || "";
|
|
357
|
+
if (msg.includes("already") || msg.includes("installed")) {
|
|
358
|
+
skillStatus = "already";
|
|
359
|
+
} else {
|
|
360
|
+
skillStatus = "error";
|
|
361
|
+
log.warn("Skills plugin install skipped (non-critical).");
|
|
362
|
+
log.dim(msg.trim());
|
|
363
|
+
}
|
|
190
364
|
}
|
|
191
365
|
}
|
|
192
366
|
|
|
193
367
|
return { marketplace: marketplaceStatus, skills: skillStatus };
|
|
194
368
|
},
|
|
195
|
-
uninstall({ name, settingsPath }) {
|
|
369
|
+
uninstall({ name, scope = "user", settingsPath }) {
|
|
196
370
|
const results = [];
|
|
197
371
|
try {
|
|
198
|
-
|
|
372
|
+
// Route through runCommand (DI) so unit tests that stub the shell
|
|
373
|
+
// don't accidentally invoke the real `claude mcp remove` against
|
|
374
|
+
// the developer's or CI runner's own config.
|
|
375
|
+
runCommand(`claude mcp remove ${name}`);
|
|
199
376
|
results.push({ key: "mcp", ok: true, message: `MCP server "${name}" removed.` });
|
|
200
377
|
} catch {
|
|
201
378
|
results.push({ key: "mcp", ok: false, message: `MCP server "${name}" was not registered (nothing to remove).` });
|
|
@@ -210,67 +387,200 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
210
387
|
}
|
|
211
388
|
|
|
212
389
|
try {
|
|
213
|
-
|
|
390
|
+
pruneMekoHookEntries(settingsPath);
|
|
391
|
+
results.push({ key: "hooks", ok: true, message: "Stale Meko hook entries pruned from settings." });
|
|
392
|
+
} catch (err) {
|
|
393
|
+
results.push({ key: "hooks", ok: false, message: `Could not prune Meko hook entries: ${err.message}` });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Qualified plugin@marketplace form — CLI 2.1.x rejects the bare
|
|
398
|
+
// name (issue #63). Before: `catch` swallowed every failure as
|
|
399
|
+
// "not installed", so a CLI shape mismatch reported success while
|
|
400
|
+
// the plugin stayed registered. Distinguish real "not installed"
|
|
401
|
+
// from other errors by inspecting stderr (case-insensitive, since
|
|
402
|
+
// wording varies across CLI versions).
|
|
403
|
+
//
|
|
404
|
+
// Scope must match the installSkills() call site — a project-scope
|
|
405
|
+
// install is invisible to a user-scope uninstall on CLI 2.1.x, so
|
|
406
|
+
// without --scope the plugin stays registered after `--uninstall`.
|
|
407
|
+
runCommand(`claude plugin uninstall ${MEKO_PLUGIN_REF} --scope ${scope}`);
|
|
214
408
|
results.push({ key: "skills", ok: true, message: "Skills plugin removed." });
|
|
215
|
-
} catch {
|
|
216
|
-
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const msg = err.stderr || err.message || "";
|
|
411
|
+
const lower = msg.toLowerCase();
|
|
412
|
+
if (lower.includes("not found") || lower.includes("not installed")) {
|
|
413
|
+
results.push({ key: "skills", ok: false, message: "Skills plugin was not installed (nothing to remove)." });
|
|
414
|
+
} else {
|
|
415
|
+
results.push({ key: "skills", ok: false, message: `Skills plugin uninstall failed: ${msg.trim()}` });
|
|
416
|
+
}
|
|
217
417
|
}
|
|
218
418
|
|
|
219
419
|
try {
|
|
220
|
-
|
|
420
|
+
runCommand(`claude plugin marketplace remove ${MEKO_MARKETPLACE_NAME}`);
|
|
221
421
|
results.push({ key: "marketplace", ok: true, message: "Plugin marketplace entry removed." });
|
|
222
|
-
} catch {
|
|
223
|
-
|
|
422
|
+
} catch (err) {
|
|
423
|
+
// Mirror the skills-uninstall treatment: a legitimate "never
|
|
424
|
+
// registered" state is different from a CLI shape mismatch or a
|
|
425
|
+
// missing binary. Without this, a broken CLI contract looks
|
|
426
|
+
// identical to success.
|
|
427
|
+
const msg = err.stderr || err.message || "";
|
|
428
|
+
const lower = msg.toLowerCase();
|
|
429
|
+
if (lower.includes("not found") || lower.includes("not registered") || lower.includes("not installed")) {
|
|
430
|
+
results.push({ key: "marketplace", ok: false, message: "Plugin marketplace entry was not registered (nothing to remove)." });
|
|
431
|
+
} else {
|
|
432
|
+
results.push({ key: "marketplace", ok: false, message: `Plugin marketplace remove failed: ${msg.trim()}` });
|
|
433
|
+
}
|
|
224
434
|
}
|
|
225
435
|
|
|
226
436
|
return results;
|
|
227
437
|
},
|
|
228
|
-
async validateConfig({ serverName }) {
|
|
438
|
+
async validateConfig({ serverName, scope = "user", projectRoot, url, apiKey }) {
|
|
439
|
+
const configPath = resolveConfigPath();
|
|
440
|
+
let cfg;
|
|
229
441
|
try {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
442
|
+
cfg = readJsonFile(configPath);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return { ok: false, message: `Could not read ${configPath}: ${err.message}` };
|
|
445
|
+
}
|
|
446
|
+
const entry =
|
|
447
|
+
scope === "project"
|
|
448
|
+
? cfg?.projects?.[projectRoot]?.mcpServers?.[serverName]
|
|
449
|
+
: cfg?.mcpServers?.[serverName];
|
|
450
|
+
|
|
451
|
+
if (!entry) {
|
|
452
|
+
return { ok: false, message: `Server "${serverName}" not found in ${configPath}.` };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (url && entry.url !== url) {
|
|
234
456
|
return {
|
|
235
457
|
ok: false,
|
|
236
|
-
message: `Server "${serverName}"
|
|
458
|
+
message: `Server "${serverName}" in ${configPath} still points at ${entry.url ?? "<missing url>"}.`,
|
|
237
459
|
};
|
|
238
|
-
} catch (err) {
|
|
239
|
-
return { ok: false, message: `Failed to run \`claude mcp list\`: ${err.message}` };
|
|
240
460
|
}
|
|
461
|
+
|
|
462
|
+
const authHeader = getHeader(entry.headers, "Authorization");
|
|
463
|
+
if (apiKey) {
|
|
464
|
+
const expected = `Bearer ${apiKey}`;
|
|
465
|
+
if (authHeader !== expected) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
message: `Server "${serverName}" in ${configPath} does not contain the current Authorization header.`,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
} else if (authHeader) {
|
|
472
|
+
return {
|
|
473
|
+
ok: false,
|
|
474
|
+
message: `Server "${serverName}" in ${configPath} still contains an Authorization header.`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return { ok: true, message: `Server "${serverName}" is registered in ${configPath}.` };
|
|
241
479
|
},
|
|
242
480
|
};
|
|
243
481
|
}
|
|
244
482
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Remove stale Meko hook entries from a Claude Code settings.json —
|
|
485
|
+
* keeping any entry that points at the CURRENT install's hooks root.
|
|
486
|
+
*
|
|
487
|
+
* The meko-agent-skills plugin registers hooks at runtime via
|
|
488
|
+
* ${CLAUDE_PLUGIN_ROOT} (skills/hooks/hooks.json), so a Meko entry in
|
|
489
|
+
* settings.json.hooks.* pointing at SOME OTHER install path is a
|
|
490
|
+
* pre-plugin absolute-path entry left over from a prior install location
|
|
491
|
+
* (issue #60) or a legacy env-prefixed command (MEKO-20). Those fire
|
|
492
|
+
* frozen copies of capture.js on every session event and throw against
|
|
493
|
+
* the current contract.
|
|
494
|
+
*
|
|
495
|
+
* Cursor's claude-compat mode (cursor.mjs installClaudeCompatHooks)
|
|
496
|
+
* shares this file on scope=user and writes entries in exactly the same
|
|
497
|
+
* shape as stale Claude Code entries — both are absolute-path
|
|
498
|
+
* `bash "<hooks-handlers-root>/<script>"` commands. There's no
|
|
499
|
+
* schema-safe signal in the command string that distinguishes the two,
|
|
500
|
+
* so prune uses the current install root as a best-effort exemption:
|
|
501
|
+
*
|
|
502
|
+
* GUARANTEE (narrow): a Cursor claude-compat entry SURVIVES when its
|
|
503
|
+
* command references the SAME resolveHooksRoot() this Claude Code
|
|
504
|
+
* install resolved to. Cursor installed from the same path (e.g. both
|
|
505
|
+
* via homebrew, or both from the same repo clone) will keep its hooks.
|
|
506
|
+
*
|
|
507
|
+
* KNOWN TRADE-OFF (issue #60 comment thread): a Cursor claude-compat
|
|
508
|
+
* entry written from a DIFFERENT install root (e.g. Cursor installed
|
|
509
|
+
* from the repo clone, Claude Code installed from npx cache) WILL be
|
|
510
|
+
* pruned — it's indistinguishable from the stale-Claude-Code entries
|
|
511
|
+
* that issue #60 is about. Callers should warn the user, and users
|
|
512
|
+
* recover by re-running the Cursor installer.
|
|
513
|
+
*
|
|
514
|
+
* Match rule for pruning: command is a string that contains BOTH
|
|
515
|
+
* `hooks-handlers/` AND one of MEKO_HOOK_SCRIPTS, AND the command does
|
|
516
|
+
* NOT contain the current install's hooks root path. The `hooks-handlers/`
|
|
517
|
+
* fence keeps unrelated user scripts that happen to share a filename
|
|
518
|
+
* (e.g. their own `session-start.sh`) from being pruned.
|
|
519
|
+
*
|
|
520
|
+
* Prunes empty group.hooks arrays, empty groups, empty event arrays, and
|
|
521
|
+
* deletes cfg.hooks entirely when it ends up empty — so an untouched file
|
|
522
|
+
* stays byte-identical. Only writes when something actually changed.
|
|
523
|
+
*
|
|
524
|
+
* @param {string} settingsPath
|
|
525
|
+
* @returns {boolean} true iff the file was rewritten
|
|
526
|
+
*/
|
|
527
|
+
export function pruneMekoHookEntries(settingsPath) {
|
|
251
528
|
const cfg = readJsonFile(settingsPath);
|
|
252
529
|
if (!cfg?.hooks || typeof cfg.hooks !== "object") return false;
|
|
253
530
|
|
|
531
|
+
const currentHooksRoot = resolveHooksRoot();
|
|
532
|
+
|
|
533
|
+
const isStaleMekoHookCommand = (cmd) =>
|
|
534
|
+
typeof cmd === "string"
|
|
535
|
+
&& cmd.includes("hooks-handlers/")
|
|
536
|
+
&& MEKO_HOOK_SCRIPTS.some((s) => cmd.includes(s))
|
|
537
|
+
&& !cmd.includes(currentHooksRoot);
|
|
538
|
+
|
|
539
|
+
// Only cascade empty-array cleanup on events where we actually removed a
|
|
540
|
+
// Meko entry — otherwise a user's pre-existing empty hook array unrelated
|
|
541
|
+
// to Meko (e.g. { hooks: { Notification: [] } }) would get silently
|
|
542
|
+
// collapsed and trigger a file rewrite, breaking the "untouched file
|
|
543
|
+
// stays byte-identical" guarantee.
|
|
254
544
|
let changed = false;
|
|
255
|
-
for (const
|
|
545
|
+
for (const event of Object.keys(cfg.hooks)) {
|
|
546
|
+
const eventArr = cfg.hooks[event];
|
|
256
547
|
if (!Array.isArray(eventArr)) continue;
|
|
548
|
+
|
|
549
|
+
let eventChanged = false;
|
|
257
550
|
for (const group of eventArr) {
|
|
258
551
|
if (!Array.isArray(group?.hooks)) continue;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
552
|
+
const before = group.hooks.length;
|
|
553
|
+
group.hooks = group.hooks.filter((h) => !isStaleMekoHookCommand(h?.command));
|
|
554
|
+
if (group.hooks.length !== before) eventChanged = true;
|
|
555
|
+
}
|
|
556
|
+
if (!eventChanged) continue;
|
|
557
|
+
|
|
558
|
+
const prunedEventArr = eventArr.filter(
|
|
559
|
+
(group) => Array.isArray(group?.hooks) && group.hooks.length > 0,
|
|
560
|
+
);
|
|
561
|
+
if (prunedEventArr.length !== eventArr.length) {
|
|
562
|
+
cfg.hooks[event] = prunedEventArr;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (cfg.hooks[event].length === 0) {
|
|
566
|
+
delete cfg.hooks[event];
|
|
271
567
|
}
|
|
568
|
+
changed = true;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (changed && Object.keys(cfg.hooks).length === 0) {
|
|
572
|
+
delete cfg.hooks;
|
|
272
573
|
}
|
|
273
574
|
|
|
274
575
|
if (changed) writeJsonFile(settingsPath, cfg);
|
|
275
576
|
return changed;
|
|
276
577
|
}
|
|
578
|
+
|
|
579
|
+
function getHeader(headers, name) {
|
|
580
|
+
if (!headers || typeof headers !== "object") return undefined;
|
|
581
|
+
const wanted = name.toLowerCase();
|
|
582
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
583
|
+
if (key.toLowerCase() === wanted) return value;
|
|
584
|
+
}
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
@@ -8,7 +8,14 @@ import {
|
|
|
8
8
|
readJsonFile,
|
|
9
9
|
removeKeys,
|
|
10
10
|
} from "../config.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getAssetRoot,
|
|
13
|
+
getInstallerVersion,
|
|
14
|
+
markerIsStale,
|
|
15
|
+
readInstallerMarker,
|
|
16
|
+
run,
|
|
17
|
+
writeInstallerMarker,
|
|
18
|
+
} from "./common.mjs";
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* Claude Desktop adapter.
|
|
@@ -32,13 +39,16 @@ import { getRepoRoot, run } from "./common.mjs";
|
|
|
32
39
|
* mcp-remote) with no space around the colon after the header name —
|
|
33
40
|
* some clients mis-parse space-containing `--header` args.
|
|
34
41
|
*
|
|
35
|
-
* installSkills() runs
|
|
36
|
-
* .skill bundle and prints import instructions.
|
|
37
|
-
* skill install API — the user drags the .skill
|
|
42
|
+
* installSkills() runs scripts/package-desktop-skill.sh under the resolved
|
|
43
|
+
* asset root to build the .skill bundle and prints import instructions.
|
|
44
|
+
* Desktop has no programmatic skill install API — the user drags the .skill
|
|
45
|
+
* into the app. The asset root is `installer/assets/` for packaged installs
|
|
46
|
+
* (npx) and `<repo>/skills/` for source checkouts; both layouts share the
|
|
47
|
+
* same shape so the script can be invoked the same way.
|
|
38
48
|
*/
|
|
39
49
|
export function createClaudeDesktopAdapter(options = {}) {
|
|
40
50
|
const configPathResolver = options.getConfigPath ?? getClaudeDesktopConfigPath;
|
|
41
|
-
const
|
|
51
|
+
const assetRootResolver = options.getAssetRoot ?? getAssetRoot;
|
|
42
52
|
const packageRunner = options.runPackageScript ?? defaultRunPackageScript;
|
|
43
53
|
const npxProber = options.probeNpx ?? defaultProbeNpx;
|
|
44
54
|
|
|
@@ -119,13 +129,20 @@ export function createClaudeDesktopAdapter(options = {}) {
|
|
|
119
129
|
}
|
|
120
130
|
},
|
|
121
131
|
installSkills({ dryRun, log }) {
|
|
122
|
-
const
|
|
123
|
-
const script = join(
|
|
124
|
-
const output = join(
|
|
132
|
+
const assetRoot = assetRootResolver();
|
|
133
|
+
const script = join(assetRoot, "scripts", "package-desktop-skill.sh");
|
|
134
|
+
const output = join(assetRoot, "meko-mcp-tools-desktop.skill");
|
|
135
|
+
// The .skill output is a ZIP file, so we can't drop a marker inside it.
|
|
136
|
+
// Stamp the marker in the parent dir instead — it signals which
|
|
137
|
+
// installer version produced the bundle sitting alongside it, so a
|
|
138
|
+
// re-run can tell whether the existing .skill is stale.
|
|
139
|
+
const markerDir = dirname(output);
|
|
140
|
+
const currentVersion = getInstallerVersion();
|
|
125
141
|
|
|
126
142
|
if (dryRun) {
|
|
127
143
|
log.dim(` Would run: bash ${script}`);
|
|
128
144
|
log.dim(` Would produce: ${output}`);
|
|
145
|
+
log.dim(` Would write .meko-installer marker (version=${currentVersion}) in ${markerDir}`);
|
|
129
146
|
return { marketplace: "skipped", skills: "dry-run", output };
|
|
130
147
|
}
|
|
131
148
|
|
|
@@ -137,6 +154,17 @@ export function createClaudeDesktopAdapter(options = {}) {
|
|
|
137
154
|
return { marketplace: "skipped", skills: "script-missing" };
|
|
138
155
|
}
|
|
139
156
|
|
|
157
|
+
// If a previous build's marker is stale, warn visibly — the user has to
|
|
158
|
+
// re-drag the .skill into Claude Desktop's Extensions panel; we can't do
|
|
159
|
+
// that step for them. Keep rebuilding either way.
|
|
160
|
+
const existingMarker = readInstallerMarker(markerDir);
|
|
161
|
+
if (existsSync(output) && existingMarker && markerIsStale(markerDir, currentVersion)) {
|
|
162
|
+
log.warn(
|
|
163
|
+
`Claude Desktop .skill bundle was produced by installer ${existingMarker.version}. ` +
|
|
164
|
+
`Rebuilding — you will need to re-drag it into Claude Desktop > Settings > Extensions.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
try {
|
|
141
169
|
packageRunner(script);
|
|
142
170
|
} catch (err) {
|
|
@@ -149,6 +177,15 @@ export function createClaudeDesktopAdapter(options = {}) {
|
|
|
149
177
|
return { marketplace: "skipped", skills: "error" };
|
|
150
178
|
}
|
|
151
179
|
|
|
180
|
+
// Stamp the marker after a successful build so the next run knows what
|
|
181
|
+
// version the sibling .skill was produced by.
|
|
182
|
+
try {
|
|
183
|
+
writeInstallerMarker(markerDir);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// Marker is best-effort — a failure here doesn't invalidate the build.
|
|
186
|
+
log.warn(`Could not write .skill installer marker: ${err.message}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
152
189
|
log.info(`Built Meko skill bundle: ${output}`);
|
|
153
190
|
log.info(
|
|
154
191
|
"Install it in Claude Desktop: Settings > Extensions > Import skill, " +
|