@yugabytedb/meko-mcp 1.2.0 → 2.1.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 (57) hide show
  1. package/README.md +59 -25
  2. package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
  3. package/assets/hooks-handlers/codex-session-end.sh +78 -0
  4. package/assets/hooks-handlers/lib/capture.js +366 -152
  5. package/assets/hooks-handlers/lib/checkpoint-timer.js +67 -13
  6. package/assets/hooks-handlers/session-end.sh +10 -0
  7. package/assets/hooks-handlers/session-start.sh +14 -3
  8. package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
  9. package/assets/scripts/meko-statusline.sh +119 -0
  10. package/assets/scripts/package-desktop-skill.sh +64 -30
  11. package/assets/skills/meko-mcp-tools/SKILL.md +84 -86
  12. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -4
  13. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +29 -25
  14. package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
  15. package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
  16. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +126 -184
  17. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +31 -37
  18. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +10 -26
  19. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +26 -30
  20. package/assets/skills/meko-mcp-tools/references/tools-overview.md +60 -66
  21. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -93
  22. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +5 -99
  23. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +9 -8
  24. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +138 -138
  25. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +46 -16
  26. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +28 -24
  28. package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +120 -174
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +16 -22
  31. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +28 -34
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +60 -66
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -93
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +4 -98
  35. package/assets/skills/meko-select-datapack-desktop/SKILL.md +208 -0
  36. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
  37. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
  38. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
  39. package/bin/create-meko-setup.mjs +74 -6
  40. package/lib/clients/claude-code.mjs +137 -6
  41. package/lib/clients/claude-desktop.mjs +51 -12
  42. package/lib/clients/codex.mjs +329 -13
  43. package/lib/clients/common.mjs +43 -1
  44. package/lib/clients/cursor.mjs +82 -21
  45. package/lib/config.mjs +26 -0
  46. package/lib/detect.mjs +7 -2
  47. package/lib/installer.mjs +24 -0
  48. package/lib/mcp-transport.mjs +5 -0
  49. package/lib/prompt.mjs +1 -1
  50. package/lib/url-selection.mjs +55 -0
  51. package/lib/validate.mjs +0 -1
  52. package/package.json +1 -1
  53. package/scripts/bundle-assets.mjs +8 -0
  54. package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
  55. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -75
  56. package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
  57. package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +0 -75
@@ -7,9 +7,14 @@ import { dirname, join } from 'node:path';
7
7
 
8
8
  import { install, uninstall } from '../lib/installer.mjs';
9
9
  import { askChoice, askMultiChoice, askText, askSecret, askYesNo, close as closePrompts } from '../lib/prompt.mjs';
10
- import { banner, error, dim, info } from '../lib/logger.mjs';
11
- import { isPackagedInstall, selfInvocation } from '../lib/clients/common.mjs';
10
+ import { banner, error, dim, info, warn } from '../lib/logger.mjs';
11
+ import {
12
+ isPackagedInstall,
13
+ MEKO_PRODUCTION_CONSOLE_URL,
14
+ selfInvocation,
15
+ } from '../lib/clients/common.mjs';
12
16
  import { getClientAdapters } from '../lib/clients/index.mjs';
17
+ import { resolveSuggestedMcpUrl } from '../lib/url-selection.mjs';
13
18
  import { parseClientList } from '../lib/clients/aliases.mjs';
14
19
  import { startMigration, runChildMigration, readStatus, stopMigration } from '../lib/migrate.mjs';
15
20
  import { checkLatest } from '../lib/version-check.mjs';
@@ -28,6 +33,8 @@ const options = {
28
33
  'cursor-hook-mode': { type: 'string', default: 'native' },
29
34
  'skip-skills': { type: 'boolean', default: false },
30
35
  'skip-validation': { type: 'boolean', default: false },
36
+ statusline: { type: 'boolean', default: false },
37
+ 'no-statusline': { type: 'boolean', default: false },
31
38
  'no-canary': { type: 'boolean', default: false },
32
39
  'dry-run': { type: 'boolean', default: false },
33
40
  'check-updates': { type: 'boolean', default: false },
@@ -79,7 +86,7 @@ Examples:
79
86
  create-meko-setup
80
87
 
81
88
  # Cloud Meko for Claude Code (non-interactive):
82
- create-meko-setup --url https://your-mcp.mekodev.com/mcp --api-key "$MEKO_API_KEY" --yes
89
+ create-meko-setup --url https://mcp.mekodata.ai/mcp --api-key "$MEKO_API_KEY" --yes
83
90
 
84
91
  # Local Meko with Cursor config:
85
92
  create-meko-setup --client cursor --local --scope project --yes
@@ -102,7 +109,7 @@ Options:
102
109
  Comma-separated, or "all" to target every supported client (default: claude-code)
103
110
  codex covers BOTH the OpenAI Codex CLI and the Codex Desktop App
104
111
  (they share ~/.codex/config.toml).
105
- --url <url> MCP server URL (from app.mekodev.com > Datapacks > Connect)
112
+ --url <url> MCP server URL (from cloud.mekodata.ai > Datapacks > Connect)
106
113
  --api-key <key> API key for Cloud Meko authentication
107
114
  --local Use local Meko at http://localhost:8000/mcp
108
115
  --scope <scope> Config scope: user or project (default: user)
@@ -111,6 +118,11 @@ Options:
111
118
  --name <name> MCP server name in config (default: meko)
112
119
  --cursor-hook-mode Cursor hook mode: native or claude-compat (default: native)
113
120
  --skip-skills Skip plugin and skills installation
121
+ --statusline Wire the Meko datapack-pin indicator into Claude Code's
122
+ statusline (composes with any existing statusLine).
123
+ Default: ON under --yes, prompted otherwise.
124
+ Claude Code only.
125
+ --no-statusline Skip statusline wiring even under --yes.
114
126
  --skip-validation Skip post-install validation
115
127
  --no-canary Skip the canary datapack_list call. The
116
128
  connectivity + tools/list checks still run.
@@ -398,6 +410,11 @@ async function main() {
398
410
  let url = flags.url ?? null;
399
411
  let apiKey = normalizeApiKey(flags['api-key']);
400
412
  let skipSkills = flags['skip-skills'];
413
+ // Statusline: --no-statusline always wins. --statusline forces on. Otherwise
414
+ // default to on under --yes (programmatic), and prompt with Y default
415
+ // interactively. Resolved later after the interactive prompts since the
416
+ // flag may flip in the interactive flow.
417
+ let installStatusline = !flags['no-statusline'] && (flags.statusline || flags.yes);
401
418
  // Tracks whether the full interactive flow already prompted for the API
402
419
  // key. The Cloud-URL fallback below should only run for the
403
420
  // semi-interactive `--url ... --client ...` paste path — otherwise
@@ -453,6 +470,28 @@ async function main() {
453
470
  }
454
471
  }
455
472
 
473
+ let suggestedUrl = null;
474
+ if (!url && !flags.local) {
475
+ suggestedUrl = resolveSuggestedMcpUrl({
476
+ adapters: getClientAdapters(),
477
+ clients,
478
+ name: flags.name,
479
+ scope: flags.scope,
480
+ projectRoot,
481
+ });
482
+ if (flags.yes) {
483
+ if (suggestedUrl.source === 'conflict') {
484
+ const conflicts = suggestedUrl.existingUrls.map((existingUrl) => `\n - ${existingUrl}`).join('');
485
+ error(
486
+ `Selected clients have different existing Meko URLs:${conflicts}\n` +
487
+ 'Pass --url explicitly to choose which endpoint to install.',
488
+ );
489
+ process.exit(1);
490
+ }
491
+ url = suggestedUrl.url;
492
+ }
493
+ }
494
+
456
495
  // Interactive mode: URL not provided and not auto-accepting defaults
457
496
  if (!url && !flags.yes) {
458
497
  banner('Meko Setup');
@@ -463,7 +502,7 @@ async function main() {
463
502
  // When only Cloud is available, skip the prompt entirely — asking to
464
503
  // pick between one option is pointless UX.
465
504
  const connectionOptions = [
466
- { label: 'Cloud Meko', description: 'Hosted at mekodev.com (requires URL + API key)' },
505
+ { label: 'Cloud Meko', description: 'Hosted at mekodata.ai (requires an API key)' },
467
506
  ];
468
507
  if (!isPackagedInstall()) {
469
508
  connectionOptions.push({
@@ -478,7 +517,19 @@ async function main() {
478
517
 
479
518
  if (connectionType === 'Cloud Meko') {
480
519
  // Step 2 — MCP URL
481
- url = await askText('MCP server URL (from app.mekodev.com > Datapacks > Connect):');
520
+ if (suggestedUrl?.source === 'existing') {
521
+ dim(`Using the existing Meko endpoint as the default: ${suggestedUrl.url}`);
522
+ } else if (suggestedUrl?.source === 'conflict') {
523
+ const conflicts = suggestedUrl.existingUrls.map((existingUrl) => `\n - ${existingUrl}`).join('');
524
+ warn(
525
+ `Selected clients use different Meko endpoints:${conflicts}\n` +
526
+ 'Enter the endpoint to use for this install.',
527
+ );
528
+ }
529
+ url = await askText(
530
+ `MCP server URL (from ${MEKO_PRODUCTION_CONSOLE_URL} > Datapacks > Connect):`,
531
+ suggestedUrl?.url ?? undefined,
532
+ );
482
533
 
483
534
  // Step 3 — API key. Normalize: terminal paste can drag in trailing
484
535
  // whitespace/CR or accidental surrounding quotes, all of which would
@@ -498,6 +549,19 @@ async function main() {
498
549
  if (!installSkills) {
499
550
  skipSkills = true;
500
551
  }
552
+
553
+ // Step 5 — statusline (Claude Code only). --no-statusline / --statusline
554
+ // skip the prompt; otherwise ask with Y default. The prompt only matters
555
+ // when at least one selected client is claude-code; for desktop / cursor /
556
+ // codex the install() orchestrator simply skips the step (the adapters
557
+ // don't expose installStatusline()).
558
+ if (!flags['no-statusline'] && !flags.statusline && clients.includes('claude-code')) {
559
+ installStatusline = await askYesNo(
560
+ 'Show the active Meko datapack in Claude Code\'s statusline? ' +
561
+ '(composes with any existing statusLine — non-destructive)',
562
+ true,
563
+ );
564
+ }
501
565
  }
502
566
 
503
567
  // Validation — URL must be set by now
@@ -544,6 +608,10 @@ async function main() {
544
608
  skipSkills,
545
609
  skipValidation: flags['skip-validation'],
546
610
  skipCanary: flags['no-canary'],
611
+ // Statusline is Claude Code only. For other clients the orchestrator
612
+ // skips the step regardless of the flag because the adapter doesn't
613
+ // expose installStatusline(); explicit guard keeps step counts honest.
614
+ installStatusline: client === 'claude-code' && installStatusline,
547
615
  dryRun: flags['dry-run'],
548
616
  verbose: flags.verbose,
549
617
  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,89 @@ 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 public generated skills repo. Use
292
+ // an explicit HTTPS URL: GitHub's owner/repo shorthand makes Claude Code
293
+ // clone over SSH, which fails for users without GitHub SSH credentials.
294
+ // A dev/e2e
295
+ // override lets a test install the plugin from a LOCAL working-tree
296
+ // checkout (a directory path) so unreleased skill changes can be
297
+ // exercised end-to-end before they are published to `yugabyte/meko-skills`.
298
+ // Only set MEKO_PLUGIN_MARKETPLACE_SOURCE in tests.
299
+ const marketplaceSource =
300
+ process.env.MEKO_PLUGIN_MARKETPLACE_SOURCE
301
+ || "https://github.com/yugabyte/meko-skills.git";
216
302
  const marketplaceCmd =
217
- `claude plugin marketplace add yugabyte/meko-mcp-server --scope ${scope}`;
303
+ `claude plugin marketplace add ${shellQuote(marketplaceSource)} --scope ${shellQuote(scope)}`;
218
304
  // `claude plugin list` dropped --scope in CLI 2.1.x; it prints all
219
305
  // scopes. The match-by-token regex used below is scope-agnostic by
220
306
  // design — `plugin update` failing with "not found" on a
@@ -393,6 +479,44 @@ export function createClaudeCodeAdapter(options = {}) {
393
479
  results.push({ key: "hooks", ok: false, message: `Could not prune Meko hook entries: ${err.message}` });
394
480
  }
395
481
 
482
+ try {
483
+ // Restore the user's prior statusLine.command (if any) and remove our
484
+ // bookkeeping. If no prior command was preserved, remove the whole
485
+ // statusLine block — installStatusline() only added it on first
486
+ // install, so we own it cleanly.
487
+ const cfg = readJsonFile(settingsPath) ?? {};
488
+ const wrapperWired =
489
+ cfg?.statusLine?.type === "command" &&
490
+ typeof cfg?.statusLine?.command === "string" &&
491
+ cfg.statusLine.command.includes("meko-statusline.sh");
492
+ if (wrapperWired) {
493
+ const prev = cfg?._meko?.statusLine?.previousCommand;
494
+ if (typeof prev === "string" && prev.length > 0) {
495
+ cfg.statusLine = { type: "command", command: prev };
496
+ results.push({ key: "statusline", ok: true, message: "Restored prior statusLine.command." });
497
+ } else {
498
+ delete cfg.statusLine;
499
+ results.push({ key: "statusline", ok: true, message: "Removed Meko statusLine wrapper." });
500
+ }
501
+ if (cfg?._meko?.statusLine) {
502
+ delete cfg._meko.statusLine;
503
+ if (cfg._meko && Object.keys(cfg._meko).length === 0) delete cfg._meko;
504
+ }
505
+ writeJsonFile(settingsPath, cfg);
506
+ // Remove the copied script too — installStatusline() owns it, so
507
+ // leaving it behind would be a stale orphan after uninstall.
508
+ try {
509
+ unlinkSync(join(homedir(), ".claude", "meko-statusline.sh"));
510
+ } catch {
511
+ // Best-effort: absent file (ENOENT) or unreadable dir is fine.
512
+ }
513
+ } else {
514
+ results.push({ key: "statusline", ok: false, message: "Statusline wrapper was not installed (nothing to remove)." });
515
+ }
516
+ } catch (err) {
517
+ results.push({ key: "statusline", ok: false, message: `Could not restore statusLine: ${err.message}` });
518
+ }
519
+
396
520
  try {
397
521
  // Qualified plugin@marketplace form — CLI 2.1.x rejects the bare
398
522
  // name (issue #63). Before: `catch` swallowed every failure as
@@ -460,6 +584,13 @@ export function createClaudeCodeAdapter(options = {}) {
460
584
  }
461
585
 
462
586
  const authHeader = getHeader(entry.headers, "Authorization");
587
+ const userAgent = getHeader(entry.headers, "User-Agent");
588
+ if (!userAgent) {
589
+ return {
590
+ ok: false,
591
+ message: `Server "${serverName}" in ${configPath} is missing the User-Agent header required by the Cloud WAF.`,
592
+ };
593
+ }
463
594
  if (apiKey) {
464
595
  const expected = `Bearer ${apiKey}`;
465
596
  if (authHeader !== expected) {
@@ -9,6 +9,7 @@ import {
9
9
  removeKeys,
10
10
  } from "../config.mjs";
11
11
  import {
12
+ MEKO_MCP_USER_AGENT,
12
13
  getAssetRoot,
13
14
  getInstallerVersion,
14
15
  markerIsStale,
@@ -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
  * }
@@ -97,9 +99,11 @@ export function createClaudeDesktopAdapter(options = {}) {
97
99
  const fromMcpb =
98
100
  /\bClaude\b[^ ]*\bExtensions\b/i.test(commandJoined) ||
99
101
  /mcp-remote-launcher\.js/.test(commandJoined);
102
+ const url = getMcpRemoteEntryUrl(entry);
100
103
  return {
101
104
  found: true,
102
105
  status: fromMcpb ? "Configured (via .mcpb extension)" : "Configured",
106
+ ...(url ? { url } : {}),
103
107
  };
104
108
  },
105
109
  removeRegistration(name) {
@@ -122,8 +126,10 @@ export function createClaudeDesktopAdapter(options = {}) {
122
126
  mergeSettings(path, { mcpServers: { [name]: server } });
123
127
  },
124
128
  setHookEnvironment({ dryRun, log }) {
125
- // Claude Desktop has no hook API; the skill carries the proactive-memory
126
- // behavior instead. Nothing to write here.
129
+ // Claude Desktop has no hook API, so the skill mandates per-turn
130
+ // conversation_add_message posting (the server extracts memories from
131
+ // those posts); proactive memory_add is no longer the model. Nothing to
132
+ // write here.
127
133
  if (dryRun) {
128
134
  log.dim(" No hook environment required for Claude Desktop (skill-driven).");
129
135
  }
@@ -132,6 +138,8 @@ export function createClaudeDesktopAdapter(options = {}) {
132
138
  const assetRoot = assetRootResolver();
133
139
  const script = join(assetRoot, "scripts", "package-desktop-skill.sh");
134
140
  const output = join(assetRoot, "meko-mcp-tools-desktop.skill");
141
+ const selectOutput = join(assetRoot, "meko-select-datapack-desktop.skill");
142
+ const outputs = [output, selectOutput];
135
143
  // The .skill output is a ZIP file, so we can't drop a marker inside it.
136
144
  // Stamp the marker in the parent dir instead — it signals which
137
145
  // installer version produced the bundle sitting alongside it, so a
@@ -141,9 +149,11 @@ export function createClaudeDesktopAdapter(options = {}) {
141
149
 
142
150
  if (dryRun) {
143
151
  log.dim(` Would run: bash ${script}`);
144
- log.dim(` Would produce: ${output}`);
152
+ for (const skillOutput of outputs) {
153
+ log.dim(` Would produce: ${skillOutput}`);
154
+ }
145
155
  log.dim(` Would write .meko-installer marker (version=${currentVersion}) in ${markerDir}`);
146
- return { marketplace: "skipped", skills: "dry-run", output };
156
+ return { marketplace: "skipped", skills: "dry-run", output, outputs };
147
157
  }
148
158
 
149
159
  if (!existsSync(script)) {
@@ -158,7 +168,7 @@ export function createClaudeDesktopAdapter(options = {}) {
158
168
  // re-drag the .skill into Claude Desktop's Extensions panel; we can't do
159
169
  // that step for them. Keep rebuilding either way.
160
170
  const existingMarker = readInstallerMarker(markerDir);
161
- if (existsSync(output) && existingMarker && markerIsStale(markerDir, currentVersion)) {
171
+ if (outputs.some((skillOutput) => existsSync(skillOutput)) && existingMarker && markerIsStale(markerDir, currentVersion)) {
162
172
  log.warn(
163
173
  `Claude Desktop .skill bundle was produced by installer ${existingMarker.version}. ` +
164
174
  `Rebuilding — you will need to re-drag it into Claude Desktop > Settings > Extensions.`,
@@ -172,8 +182,9 @@ export function createClaudeDesktopAdapter(options = {}) {
172
182
  return { marketplace: "skipped", skills: "error", error: err.message };
173
183
  }
174
184
 
175
- if (!existsSync(output)) {
176
- log.warn(`Expected ${output} after packaging but it was not produced.`);
185
+ const missingOutputs = outputs.filter((skillOutput) => !existsSync(skillOutput));
186
+ if (missingOutputs.length) {
187
+ log.warn(`Expected skill bundle(s) were not produced: ${missingOutputs.join(", ")}`);
177
188
  return { marketplace: "skipped", skills: "error" };
178
189
  }
179
190
 
@@ -186,12 +197,14 @@ export function createClaudeDesktopAdapter(options = {}) {
186
197
  log.warn(`Could not write .skill installer marker: ${err.message}`);
187
198
  }
188
199
 
189
- log.info(`Built Meko skill bundle: ${output}`);
200
+ for (const skillOutput of outputs) {
201
+ log.info(`Built Meko skill bundle: ${skillOutput}`);
202
+ }
190
203
  log.info(
191
- "Install it in Claude Desktop: Settings > Extensions > Import skill, " +
192
- "or drag the .skill file onto the Claude Desktop window.",
204
+ "Install both in Claude Desktop: Settings > Extensions > Import skill, " +
205
+ "or drag each .skill file onto the Claude Desktop window.",
193
206
  );
194
- return { marketplace: "skipped", skills: "ok", output };
207
+ return { marketplace: "skipped", skills: "ok", output, outputs };
195
208
  },
196
209
  uninstall({ name }) {
197
210
  const results = [];
@@ -246,6 +259,12 @@ export function createClaudeDesktopAdapter(options = {}) {
246
259
  `Server "${serverName}" in ${path} is missing required "command" and "args" fields.`,
247
260
  };
248
261
  }
262
+ if (!entry.args.includes(`User-Agent:${MEKO_MCP_USER_AGENT}`)) {
263
+ return {
264
+ ok: false,
265
+ message: `Server "${serverName}" in ${path} is missing the User-Agent header required by the Cloud WAF.`,
266
+ };
267
+ }
249
268
 
250
269
  return { ok: true, message: `Server "${serverName}" is registered in ${path}.` };
251
270
  },
@@ -262,7 +281,15 @@ export function createClaudeDesktopAdapter(options = {}) {
262
281
  * @returns {{command: string, args: string[], env?: Record<string,string>}}
263
282
  */
264
283
  export function buildMcpRemoteEntry({ url, apiKey }) {
265
- const args = ["-y", "mcp-remote", url, "--transport", "http-only"];
284
+ const args = [
285
+ "-y",
286
+ "mcp-remote",
287
+ url,
288
+ "--transport",
289
+ "http-only",
290
+ "--header",
291
+ `User-Agent:${MEKO_MCP_USER_AGENT}`,
292
+ ];
266
293
  const entry = { command: "npx", args };
267
294
  if (apiKey) {
268
295
  // `Authorization:Bearer ${MEKO_API_KEY}` with no space around the colon
@@ -275,6 +302,18 @@ export function buildMcpRemoteEntry({ url, apiKey }) {
275
302
  return entry;
276
303
  }
277
304
 
305
+ /** Read the remote URL from installer-managed or MCPB Claude Desktop entries. */
306
+ export function getMcpRemoteEntryUrl(entry) {
307
+ if (typeof entry?.env?.MEKO_MCP_URL === "string") {
308
+ return entry.env.MEKO_MCP_URL;
309
+ }
310
+ if (typeof entry?.url === "string") return entry.url;
311
+ if (!Array.isArray(entry?.args)) return undefined;
312
+ const remoteIndex = entry.args.indexOf("mcp-remote");
313
+ const candidate = remoteIndex >= 0 ? entry.args[remoteIndex + 1] : undefined;
314
+ return typeof candidate === "string" ? candidate : undefined;
315
+ }
316
+
278
317
  function defaultRunPackageScript(scriptPath) {
279
318
  if (!statSync(scriptPath).isFile()) {
280
319
  throw new Error(`Not a file: ${scriptPath}`);