@yugabytedb/meko-mcp 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +81 -23
  2. package/assets/hooks-handlers/before-submit-prompt.sh +44 -0
  3. package/assets/hooks-handlers/cursor-session-start.sh +42 -0
  4. package/assets/hooks-handlers/lib/capture.js +404 -88
  5. package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
  6. package/assets/hooks-handlers/session-start.sh +6 -2
  7. package/assets/scripts/meko-statusline.sh +119 -0
  8. package/assets/scripts/package-desktop-skill.sh +39 -0
  9. package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
  10. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  11. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
  12. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
  13. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
  14. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
  15. package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
  16. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
  17. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
  18. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
  19. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +244 -0
  20. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +180 -0
  21. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +24 -0
  22. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +99 -0
  23. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +327 -0
  24. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +63 -0
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +214 -0
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +105 -0
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +70 -0
  28. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +71 -0
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +75 -0
  30. package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
  31. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
  32. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
  33. package/bin/create-meko-setup.mjs +33 -4
  34. package/lib/clients/claude-code.mjs +133 -6
  35. package/lib/clients/claude-desktop.mjs +48 -20
  36. package/lib/clients/codex.mjs +538 -5
  37. package/lib/clients/common.mjs +122 -1
  38. package/lib/clients/cursor.mjs +120 -27
  39. package/lib/config.mjs +52 -0
  40. package/lib/installer.mjs +26 -2
  41. package/lib/mcp-transport.mjs +5 -0
  42. package/lib/validate.mjs +0 -1
  43. package/package.json +1 -1
  44. package/scripts/bundle-assets.mjs +16 -0
  45. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
  46. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
@@ -0,0 +1,66 @@
1
+ # Troubleshooting — meko-select-datapack-desktop
2
+
3
+ ## Symptom: I pinned a datapack but the next Meko call doesn't use it
4
+
5
+ **Likely causes**
6
+
7
+ 1. **You forgot the per-turn `memory_search`.** Desktop has no SessionStart hook, so the skill teaches you to read the pin yourself at the top of every Meko-touching turn. If you skip that lookup, you'll send Meko calls with no `datapack_id` and the server resolves the default. Add the lookup; cache the result for the rest of the turn.
8
+ 2. **You used `agent_id="meko_agent"` for the pin write or read.** The pin lives under `agent_id="claude_desktop"`, exclusively. Cross-project facts go under `meko_agent`, but the pin is a Desktop-specific preference and should stay scoped.
9
+ 3. **The pin write failed silently.** Always verify-after-write — call `memory_search` for `meko_active_datapack` immediately after `memory_add` and confirm the row appears. If it doesn't, the write was lost (latency, server error, wrong scope) and the pin doesn't actually exist.
10
+
11
+ ## Symptom: `memory_search` returns multiple pin memories
12
+
13
+ You (or an earlier run) created duplicates by using `memory_add` instead of `memory_update` on a switch. Resolution:
14
+
15
+ 1. Pick the most recent (highest `selected_at` in the text body).
16
+ 2. `memory_delete_by_id` the others.
17
+ 3. From now on, switch via `memory_update` on the surviving id — never `memory_add`.
18
+
19
+ The cookbook's Flow 2 demonstrates the correct switch path.
20
+
21
+ ## Symptom: A pinned datapack returns "not found" when used
22
+
23
+ The pin became stale — the datapack was deleted in another client. Tell the user:
24
+
25
+ ```
26
+ The pinned datapack `<name>` (`<id>`) no longer exists on the server.
27
+ Run `meko-select-datapack-desktop` to pick a fresh one, or `clear` to unpin.
28
+ ```
29
+
30
+ Then either re-run the skill or delete the pin memory. Do not silently fall back to the default — the user explicitly asked for a specific datapack.
31
+
32
+ ## Symptom: Substring search matches multiple datapacks
33
+
34
+ Never auto-pick when there's ambiguity. Re-render just the matching rows numbered from 1, and ask the user to pick a number from the filtered subset. Same as the Claude Code variant.
35
+
36
+ ## Symptom: `datapack_list` returns an empty array
37
+
38
+ The user has zero datapacks. Print:
39
+
40
+ ```
41
+ You don't have any datapacks yet. Create one with:
42
+
43
+ datapack_create(scope="admin", name="<your-datapack-name>")
44
+
45
+ Or visit the Meko Cloud console (Datapacks → New datapack).
46
+ ```
47
+
48
+ Stop. Don't `memory_add` an empty pin.
49
+
50
+ ## Symptom: User typed `cancel` after the table rendered
51
+
52
+ Leave the existing pin memory alone. Print:
53
+
54
+ ```
55
+ Cancelled. Active datapack unchanged.
56
+ ```
57
+
58
+ Do NOT delete on `cancel` — that's `clear`'s job.
59
+
60
+ ## Symptom: `memory_search` for the pin times out
61
+
62
+ Memory tools are slow on prod (search 2-6s observed). If the search exceeds a reasonable budget, fall back to the server default for that single call and tell the user *"Couldn't read the active-datapack pin within budget — using server default for this Meko call."* Don't guess a UUID; don't silently retry indefinitely.
63
+
64
+ ## Symptom: The pin survived a `claude_desktop` profile reset / app reinstall
65
+
66
+ That's expected — the pin is a server-side memory, not local state. To explicitly forget across reinstalls, run the skill with `clear` BEFORE wiping local state, or `memory_delete_by_id` the pin row directly.
@@ -25,9 +25,11 @@ const options = {
25
25
  scope: { type: 'string', default: 'user' },
26
26
  name: { type: 'string', default: 'meko' },
27
27
  client: { type: 'string', default: 'claude-code' },
28
- 'cursor-hook-mode': { type: 'string', default: 'hybrid' },
28
+ 'cursor-hook-mode': { type: 'string', default: 'native' },
29
29
  'skip-skills': { type: 'boolean', default: false },
30
30
  'skip-validation': { type: 'boolean', default: false },
31
+ statusline: { type: 'boolean', default: false },
32
+ 'no-statusline': { type: 'boolean', default: false },
31
33
  'no-canary': { type: 'boolean', default: false },
32
34
  'dry-run': { type: 'boolean', default: false },
33
35
  'check-updates': { type: 'boolean', default: false },
@@ -109,8 +111,13 @@ Options:
109
111
  codex always installs at user scope (no project-level
110
112
  config); --scope=project degrades with a warning.
111
113
  --name <name> MCP server name in config (default: meko)
112
- --cursor-hook-mode Cursor hook mode: native, claude-compat, hybrid (default: hybrid)
114
+ --cursor-hook-mode Cursor hook mode: native or claude-compat (default: native)
113
115
  --skip-skills Skip plugin and skills installation
116
+ --statusline Wire the Meko datapack-pin indicator into Claude Code's
117
+ statusline (composes with any existing statusLine).
118
+ Default: ON under --yes, prompted otherwise.
119
+ Claude Code only.
120
+ --no-statusline Skip statusline wiring even under --yes.
114
121
  --skip-validation Skip post-install validation
115
122
  --no-canary Skip the canary datapack_list call. The
116
123
  connectivity + tools/list checks still run.
@@ -218,7 +225,7 @@ function detectInstalledClients() {
218
225
  }
219
226
 
220
227
  function isValidCursorHookMode(mode) {
221
- return ['native', 'claude-compat', 'hybrid'].includes(mode);
228
+ return ['native', 'claude-compat'].includes(mode);
222
229
  }
223
230
 
224
231
  /**
@@ -391,13 +398,18 @@ async function main() {
391
398
  process.exit(0);
392
399
  }
393
400
  if (!isValidCursorHookMode(flags['cursor-hook-mode'])) {
394
- error(`Unknown --cursor-hook-mode "${flags['cursor-hook-mode']}". Valid values: native, claude-compat, hybrid.`);
401
+ error(`Unknown --cursor-hook-mode "${flags['cursor-hook-mode']}". Valid values: native, claude-compat.`);
395
402
  process.exit(1);
396
403
  }
397
404
 
398
405
  let url = flags.url ?? null;
399
406
  let apiKey = normalizeApiKey(flags['api-key']);
400
407
  let skipSkills = flags['skip-skills'];
408
+ // Statusline: --no-statusline always wins. --statusline forces on. Otherwise
409
+ // default to on under --yes (programmatic), and prompt with Y default
410
+ // interactively. Resolved later after the interactive prompts since the
411
+ // flag may flip in the interactive flow.
412
+ let installStatusline = !flags['no-statusline'] && (flags.statusline || flags.yes);
401
413
  // Tracks whether the full interactive flow already prompted for the API
402
414
  // key. The Cloud-URL fallback below should only run for the
403
415
  // semi-interactive `--url ... --client ...` paste path — otherwise
@@ -498,6 +510,19 @@ async function main() {
498
510
  if (!installSkills) {
499
511
  skipSkills = true;
500
512
  }
513
+
514
+ // Step 5 — statusline (Claude Code only). --no-statusline / --statusline
515
+ // skip the prompt; otherwise ask with Y default. The prompt only matters
516
+ // when at least one selected client is claude-code; for desktop / cursor /
517
+ // codex the install() orchestrator simply skips the step (the adapters
518
+ // don't expose installStatusline()).
519
+ if (!flags['no-statusline'] && !flags.statusline && clients.includes('claude-code')) {
520
+ installStatusline = await askYesNo(
521
+ 'Show the active Meko datapack in Claude Code\'s statusline? ' +
522
+ '(composes with any existing statusLine — non-destructive)',
523
+ true,
524
+ );
525
+ }
501
526
  }
502
527
 
503
528
  // Validation — URL must be set by now
@@ -544,6 +569,10 @@ async function main() {
544
569
  skipSkills,
545
570
  skipValidation: flags['skip-validation'],
546
571
  skipCanary: flags['no-canary'],
572
+ // Statusline is Claude Code only. For other clients the orchestrator
573
+ // skips the step regardless of the flag because the adapter doesn't
574
+ // expose installStatusline(); explicit guard keeps step counts honest.
575
+ installStatusline: client === 'claude-code' && installStatusline,
547
576
  dryRun: flags['dry-run'],
548
577
  verbose: flags.verbose,
549
578
  yes: flags.yes,
@@ -1,4 +1,4 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { chmodSync, copyFileSync, mkdirSync, unlinkSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -14,8 +14,10 @@ import {
14
14
  } from "../config.mjs";
15
15
  import { detectClaude, detectExistingMeko, getClaudeCodeConfigPath } from "../detect.mjs";
16
16
  import {
17
+ MEKO_MCP_USER_AGENT,
17
18
  redactKey,
18
19
  resolveHooksRoot,
20
+ resolveStatuslineSource,
19
21
  run,
20
22
  shellQuote,
21
23
  writeInstallerMarker,
@@ -44,15 +46,23 @@ const MEKO_MARKETPLACE_NAME = "meko-agent-skills";
44
46
 
45
47
  // Meko MCP tools to union into `permissions.allow` so Claude Code resolves
46
48
  // them without the deferred-tools ToolSearch round-trip (issue #5). The set
47
- // is intentionally tight — only tools the agent calls proactively per
48
- // the meko-mcp-tools skill — to avoid bloating every user's settings.
49
+ // is intentionally tight — the memory/conversation tools the agent actually
50
+ // reaches for per the meko-mcp-tools skill — to avoid bloating every user's
51
+ // settings.
52
+ //
53
+ // `memory_add` stays on this list even though the skill no longer tells the
54
+ // agent to fire it proactively (server-side extraction now captures
55
+ // conversationally-stated facts automatically). It's kept because the skill
56
+ // still uses memory_add for the narrow explicit-save cases — "remember this",
57
+ // output-only/tool-derived facts, and overwriting corrections — and pre-
58
+ // allowing it keeps those calls prompt-free. `conversation_add_message` is the
59
+ // tool that now drives capture-plus-extraction, so it must resolve instantly.
49
60
  //
50
61
  // NOTE: `permissions.allow` is verified to cover user-prompt bypass. Whether
51
62
  // it also bypasses the deferred-tools schema-fetch gate must be re-verified
52
63
  // empirically before this flag is considered load-bearing for #5. Tracked
53
64
  // as Phase 10.1 in skills/TEST_PLAN.md.
54
65
  const MEKO_ALWAYS_AVAILABLE_TOOLS = [
55
- "mcp__meko__db_summarize_database",
56
66
  "mcp__meko__memory_add",
57
67
  "mcp__meko__memory_search",
58
68
  "mcp__meko__memory_get_all",
@@ -125,7 +135,8 @@ export function createClaudeCodeAdapter(options = {}) {
125
135
  buildRegisterCommand({ name, url, scope, apiKey }) {
126
136
  let cmd =
127
137
  `claude mcp add --transport http --scope ${shellQuote(scope)} ` +
128
- `${shellQuote(name)} ${shellQuote(url)}`;
138
+ `${shellQuote(name)} ${shellQuote(url)} ` +
139
+ `--header ${shellQuote(`User-Agent: ${MEKO_MCP_USER_AGENT}`)}`;
129
140
  if (apiKey) {
130
141
  cmd += ` --header ${shellQuote(`Authorization: Bearer ${apiKey}`)}`;
131
142
  }
@@ -207,14 +218,85 @@ export function createClaudeCodeAdapter(options = {}) {
207
218
  );
208
219
  }
209
220
  },
221
+ /**
222
+ * Wire the Meko datapack-pin statusline contribution into ~/.claude/settings.json.
223
+ *
224
+ * Composable: if the user already has a `statusLine.command`, that command
225
+ * is preserved as the inner command and our wrapper appends `Meko: <name>`
226
+ * after its output. Idempotent: if our wrapper is already wired, no-op.
227
+ *
228
+ * Restore-on-uninstall: the original command is preserved in
229
+ * settings.json under `_meko.statusLine.previousCommand` so `uninstall()`
230
+ * can restore it. Absent that key (fresh install), uninstall removes the
231
+ * whole `statusLine` block.
232
+ */
233
+ installStatusline({ dryRun, verbose, log, settingsPath }) {
234
+ const scriptSource = resolveStatuslineSource();
235
+ const scriptDest = join(homedir(), ".claude", "meko-statusline.sh");
236
+
237
+ const existing = readJsonFile(settingsPath) ?? {};
238
+ const existingCmd =
239
+ existing?.statusLine?.type === "command" && typeof existing?.statusLine?.command === "string"
240
+ ? existing.statusLine.command.trim()
241
+ : "";
242
+
243
+ // Detect prior install of our wrapper. Re-wiring is a no-op for the
244
+ // command (already points at the wrapper); we still re-copy the script
245
+ // so a refreshed installer ships an updated copy.
246
+ const alreadyWired = existingCmd.includes("meko-statusline.sh");
247
+ const innerCmd =
248
+ alreadyWired
249
+ ? (existing?._meko?.statusLine?.previousCommand ?? "")
250
+ : existingCmd;
251
+
252
+ const wrapperCmd = innerCmd
253
+ ? `${shellQuote(scriptDest)} --inner-cmd ${shellQuote(innerCmd)}`
254
+ : shellQuote(scriptDest);
255
+
256
+ if (dryRun) {
257
+ log.dim(` Would copy ${scriptSource} → ${scriptDest}`);
258
+ log.dim(` Would set ${settingsPath} statusLine.command = ${wrapperCmd}`);
259
+ if (innerCmd) log.dim(` (preserving prior inner command: ${innerCmd})`);
260
+ return;
261
+ }
262
+
263
+ mkdirSync(join(homedir(), ".claude"), { recursive: true });
264
+ copyFileSync(scriptSource, scriptDest);
265
+ chmodSync(scriptDest, 0o755);
266
+
267
+ const patch = {
268
+ statusLine: { type: "command", command: wrapperCmd },
269
+ };
270
+ // Stash the inner command so uninstall can restore it. Only write this
271
+ // namespace on the first install (when alreadyWired is false), so a
272
+ // re-install never overwrites the genuine original with our wrapper.
273
+ if (!alreadyWired) {
274
+ patch._meko = { statusLine: { previousCommand: innerCmd || null } };
275
+ }
276
+ mergeSettings(settingsPath, patch);
277
+
278
+ if (verbose) {
279
+ log.dim(` Statusline script: ${scriptDest}`);
280
+ log.dim(` Wrapper command: ${wrapperCmd}`);
281
+ if (innerCmd) log.dim(` Inner command preserved: ${innerCmd}`);
282
+ if (alreadyWired) log.dim(` (refreshed existing wrapper, no settings change beyond script copy)`);
283
+ }
284
+ },
210
285
  installSkills({ scope, dryRun, verbose, log }) {
211
286
  // NB: we deliberately do NOT pass --sparse. Claude CLI's sparse
212
287
  // path filter silently produces an empty clone for our layout,
213
288
  // leaving the marketplace.json missing. Full clone is small
214
289
  // (~few MB, mostly markdown) and is the only way that reliably
215
290
  // succeeds today.
291
+ // Marketplace source is normally the upstream GitHub repo. A dev/e2e
292
+ // override lets a test install the plugin from a LOCAL working-tree
293
+ // checkout (a directory path) so unreleased skill changes can be
294
+ // exercised end-to-end before they land on `yugabyte/meko-mcp-server`.
295
+ // Default is unchanged; only set MEKO_PLUGIN_MARKETPLACE_SOURCE in tests.
296
+ const marketplaceSource =
297
+ process.env.MEKO_PLUGIN_MARKETPLACE_SOURCE || "yugabyte/meko-mcp-server";
216
298
  const marketplaceCmd =
217
- `claude plugin marketplace add yugabyte/meko-mcp-server --scope ${scope}`;
299
+ `claude plugin marketplace add ${shellQuote(marketplaceSource)} --scope ${shellQuote(scope)}`;
218
300
  // `claude plugin list` dropped --scope in CLI 2.1.x; it prints all
219
301
  // scopes. The match-by-token regex used below is scope-agnostic by
220
302
  // design — `plugin update` failing with "not found" on a
@@ -393,6 +475,44 @@ export function createClaudeCodeAdapter(options = {}) {
393
475
  results.push({ key: "hooks", ok: false, message: `Could not prune Meko hook entries: ${err.message}` });
394
476
  }
395
477
 
478
+ try {
479
+ // Restore the user's prior statusLine.command (if any) and remove our
480
+ // bookkeeping. If no prior command was preserved, remove the whole
481
+ // statusLine block — installStatusline() only added it on first
482
+ // install, so we own it cleanly.
483
+ const cfg = readJsonFile(settingsPath) ?? {};
484
+ const wrapperWired =
485
+ cfg?.statusLine?.type === "command" &&
486
+ typeof cfg?.statusLine?.command === "string" &&
487
+ cfg.statusLine.command.includes("meko-statusline.sh");
488
+ if (wrapperWired) {
489
+ const prev = cfg?._meko?.statusLine?.previousCommand;
490
+ if (typeof prev === "string" && prev.length > 0) {
491
+ cfg.statusLine = { type: "command", command: prev };
492
+ results.push({ key: "statusline", ok: true, message: "Restored prior statusLine.command." });
493
+ } else {
494
+ delete cfg.statusLine;
495
+ results.push({ key: "statusline", ok: true, message: "Removed Meko statusLine wrapper." });
496
+ }
497
+ if (cfg?._meko?.statusLine) {
498
+ delete cfg._meko.statusLine;
499
+ if (cfg._meko && Object.keys(cfg._meko).length === 0) delete cfg._meko;
500
+ }
501
+ writeJsonFile(settingsPath, cfg);
502
+ // Remove the copied script too — installStatusline() owns it, so
503
+ // leaving it behind would be a stale orphan after uninstall.
504
+ try {
505
+ unlinkSync(join(homedir(), ".claude", "meko-statusline.sh"));
506
+ } catch {
507
+ // Best-effort: absent file (ENOENT) or unreadable dir is fine.
508
+ }
509
+ } else {
510
+ results.push({ key: "statusline", ok: false, message: "Statusline wrapper was not installed (nothing to remove)." });
511
+ }
512
+ } catch (err) {
513
+ results.push({ key: "statusline", ok: false, message: `Could not restore statusLine: ${err.message}` });
514
+ }
515
+
396
516
  try {
397
517
  // Qualified plugin@marketplace form — CLI 2.1.x rejects the bare
398
518
  // name (issue #63). Before: `catch` swallowed every failure as
@@ -460,6 +580,13 @@ export function createClaudeCodeAdapter(options = {}) {
460
580
  }
461
581
 
462
582
  const authHeader = getHeader(entry.headers, "Authorization");
583
+ const userAgent = getHeader(entry.headers, "User-Agent");
584
+ if (!userAgent) {
585
+ return {
586
+ ok: false,
587
+ message: `Server "${serverName}" in ${configPath} is missing the User-Agent header required by the Cloud WAF.`,
588
+ };
589
+ }
463
590
  if (apiKey) {
464
591
  const expected = `Bearer ${apiKey}`;
465
592
  if (authHeader !== expected) {
@@ -9,8 +9,9 @@ import {
9
9
  removeKeys,
10
10
  } from "../config.mjs";
11
11
  import {
12
+ MEKO_MCP_USER_AGENT,
13
+ getAssetRoot,
12
14
  getInstallerVersion,
13
- getRepoRoot,
14
15
  markerIsStale,
15
16
  readInstallerMarker,
16
17
  run,
@@ -31,6 +32,7 @@ import {
31
32
  * "meko": {
32
33
  * "command": "npx",
33
34
  * "args": ["-y", "mcp-remote", "<url>", "--transport", "http-only",
35
+ * "--header", "User-Agent:meko-mcp-installer/1.0 (...)",
34
36
  * "--header", "Authorization:Bearer ${MEKO_API_KEY}"],
35
37
  * "env": { "MEKO_API_KEY": "<key>" }
36
38
  * }
@@ -39,13 +41,16 @@ import {
39
41
  * mcp-remote) with no space around the colon after the header name —
40
42
  * some clients mis-parse space-containing `--header` args.
41
43
  *
42
- * installSkills() runs skills/scripts/package-desktop-skill.sh to build the
43
- * .skill bundle and prints import instructions. Desktop has no programmatic
44
- * skill install API — the user drags the .skill into the app.
44
+ * installSkills() runs scripts/package-desktop-skill.sh under the resolved
45
+ * asset root to build the .skill bundle and prints import instructions.
46
+ * Desktop has no programmatic skill install API — the user drags the .skill
47
+ * into the app. The asset root is `installer/assets/` for packaged installs
48
+ * (npx) and `<repo>/skills/` for source checkouts; both layouts share the
49
+ * same shape so the script can be invoked the same way.
45
50
  */
46
51
  export function createClaudeDesktopAdapter(options = {}) {
47
52
  const configPathResolver = options.getConfigPath ?? getClaudeDesktopConfigPath;
48
- const repoRootResolver = options.getRepoRoot ?? getRepoRoot;
53
+ const assetRootResolver = options.getAssetRoot ?? getAssetRoot;
49
54
  const packageRunner = options.runPackageScript ?? defaultRunPackageScript;
50
55
  const npxProber = options.probeNpx ?? defaultProbeNpx;
51
56
 
@@ -119,16 +124,20 @@ export function createClaudeDesktopAdapter(options = {}) {
119
124
  mergeSettings(path, { mcpServers: { [name]: server } });
120
125
  },
121
126
  setHookEnvironment({ dryRun, log }) {
122
- // Claude Desktop has no hook API; the skill carries the proactive-memory
123
- // behavior instead. Nothing to write here.
127
+ // Claude Desktop has no hook API, so the skill mandates per-turn
128
+ // conversation_add_message posting (the server extracts memories from
129
+ // those posts); proactive memory_add is no longer the model. Nothing to
130
+ // write here.
124
131
  if (dryRun) {
125
132
  log.dim(" No hook environment required for Claude Desktop (skill-driven).");
126
133
  }
127
134
  },
128
135
  installSkills({ dryRun, log }) {
129
- const repoRoot = repoRootResolver();
130
- const script = join(repoRoot, "skills", "scripts", "package-desktop-skill.sh");
131
- const output = join(repoRoot, "skills", "meko-mcp-tools-desktop.skill");
136
+ const assetRoot = assetRootResolver();
137
+ const script = join(assetRoot, "scripts", "package-desktop-skill.sh");
138
+ const output = join(assetRoot, "meko-mcp-tools-desktop.skill");
139
+ const selectOutput = join(assetRoot, "meko-select-datapack-desktop.skill");
140
+ const outputs = [output, selectOutput];
132
141
  // The .skill output is a ZIP file, so we can't drop a marker inside it.
133
142
  // Stamp the marker in the parent dir instead — it signals which
134
143
  // installer version produced the bundle sitting alongside it, so a
@@ -138,9 +147,11 @@ export function createClaudeDesktopAdapter(options = {}) {
138
147
 
139
148
  if (dryRun) {
140
149
  log.dim(` Would run: bash ${script}`);
141
- log.dim(` Would produce: ${output}`);
150
+ for (const skillOutput of outputs) {
151
+ log.dim(` Would produce: ${skillOutput}`);
152
+ }
142
153
  log.dim(` Would write .meko-installer marker (version=${currentVersion}) in ${markerDir}`);
143
- return { marketplace: "skipped", skills: "dry-run", output };
154
+ return { marketplace: "skipped", skills: "dry-run", output, outputs };
144
155
  }
145
156
 
146
157
  if (!existsSync(script)) {
@@ -155,7 +166,7 @@ export function createClaudeDesktopAdapter(options = {}) {
155
166
  // re-drag the .skill into Claude Desktop's Extensions panel; we can't do
156
167
  // that step for them. Keep rebuilding either way.
157
168
  const existingMarker = readInstallerMarker(markerDir);
158
- if (existsSync(output) && existingMarker && markerIsStale(markerDir, currentVersion)) {
169
+ if (outputs.some((skillOutput) => existsSync(skillOutput)) && existingMarker && markerIsStale(markerDir, currentVersion)) {
159
170
  log.warn(
160
171
  `Claude Desktop .skill bundle was produced by installer ${existingMarker.version}. ` +
161
172
  `Rebuilding — you will need to re-drag it into Claude Desktop > Settings > Extensions.`,
@@ -169,8 +180,9 @@ export function createClaudeDesktopAdapter(options = {}) {
169
180
  return { marketplace: "skipped", skills: "error", error: err.message };
170
181
  }
171
182
 
172
- if (!existsSync(output)) {
173
- log.warn(`Expected ${output} after packaging but it was not produced.`);
183
+ const missingOutputs = outputs.filter((skillOutput) => !existsSync(skillOutput));
184
+ if (missingOutputs.length) {
185
+ log.warn(`Expected skill bundle(s) were not produced: ${missingOutputs.join(", ")}`);
174
186
  return { marketplace: "skipped", skills: "error" };
175
187
  }
176
188
 
@@ -183,12 +195,14 @@ export function createClaudeDesktopAdapter(options = {}) {
183
195
  log.warn(`Could not write .skill installer marker: ${err.message}`);
184
196
  }
185
197
 
186
- log.info(`Built Meko skill bundle: ${output}`);
198
+ for (const skillOutput of outputs) {
199
+ log.info(`Built Meko skill bundle: ${skillOutput}`);
200
+ }
187
201
  log.info(
188
- "Install it in Claude Desktop: Settings > Extensions > Import skill, " +
189
- "or drag the .skill file onto the Claude Desktop window.",
202
+ "Install both in Claude Desktop: Settings > Extensions > Import skill, " +
203
+ "or drag each .skill file onto the Claude Desktop window.",
190
204
  );
191
- return { marketplace: "skipped", skills: "ok", output };
205
+ return { marketplace: "skipped", skills: "ok", output, outputs };
192
206
  },
193
207
  uninstall({ name }) {
194
208
  const results = [];
@@ -243,6 +257,12 @@ export function createClaudeDesktopAdapter(options = {}) {
243
257
  `Server "${serverName}" in ${path} is missing required "command" and "args" fields.`,
244
258
  };
245
259
  }
260
+ if (!entry.args.includes(`User-Agent:${MEKO_MCP_USER_AGENT}`)) {
261
+ return {
262
+ ok: false,
263
+ message: `Server "${serverName}" in ${path} is missing the User-Agent header required by the Cloud WAF.`,
264
+ };
265
+ }
246
266
 
247
267
  return { ok: true, message: `Server "${serverName}" is registered in ${path}.` };
248
268
  },
@@ -259,7 +279,15 @@ export function createClaudeDesktopAdapter(options = {}) {
259
279
  * @returns {{command: string, args: string[], env?: Record<string,string>}}
260
280
  */
261
281
  export function buildMcpRemoteEntry({ url, apiKey }) {
262
- const args = ["-y", "mcp-remote", url, "--transport", "http-only"];
282
+ const args = [
283
+ "-y",
284
+ "mcp-remote",
285
+ url,
286
+ "--transport",
287
+ "http-only",
288
+ "--header",
289
+ `User-Agent:${MEKO_MCP_USER_AGENT}`,
290
+ ];
263
291
  const entry = { command: "npx", args };
264
292
  if (apiKey) {
265
293
  // `Authorization:Bearer ${MEKO_API_KEY}` with no space around the colon