@yugabytedb/meko-mcp 1.0.8 → 1.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 (36) hide show
  1. package/README.md +181 -32
  2. package/assets/hooks-handlers/lib/capture.js +176 -77
  3. package/assets/hooks-handlers/lib/package.json +1 -0
  4. package/assets/hooks-handlers/test_single_conversation.sh +1 -1
  5. package/assets/skills/meko-mcp-tools/SKILL.md +167 -53
  6. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  7. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +84 -22
  8. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +89 -56
  9. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +15 -7
  10. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +17 -9
  11. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +94 -5
  12. package/assets/skills/meko-mcp-tools/references/tools-overview.md +52 -37
  13. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +108 -37
  14. package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +6 -10
  15. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +30 -18
  16. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +55 -69
  17. package/bin/create-meko-setup.mjs +147 -18
  18. package/lib/clients/claude-code.mjs +378 -68
  19. package/lib/clients/claude-desktop.mjs +35 -1
  20. package/lib/clients/codex.mjs +977 -0
  21. package/lib/clients/common.mjs +142 -1
  22. package/lib/clients/cursor.mjs +71 -3
  23. package/lib/clients/index.mjs +2 -0
  24. package/lib/config.mjs +30 -0
  25. package/lib/detect.mjs +52 -44
  26. package/lib/installer.mjs +9 -2
  27. package/lib/mcp-transport.mjs +61 -2
  28. package/lib/migrate/adapters/memory-md.mjs +13 -6
  29. package/lib/migrate/datapack.mjs +167 -0
  30. package/lib/migrate/id.mjs +127 -9
  31. package/lib/migrate/orchestrator.mjs +68 -10
  32. package/lib/migrate.mjs +346 -3
  33. package/lib/prompt.mjs +22 -4
  34. package/lib/validate.mjs +313 -9
  35. package/lib/version-check.mjs +178 -0
  36. package/package.json +2 -1
@@ -1,93 +1,79 @@
1
- # user_id Conventions
1
+ # user_id — identity scoping on Cloud Meko
2
2
 
3
- `user_id` identifies the **human** whose data is being stored or retrieved. It is the user-scoping counterpart to `agent_id`.
3
+ On Cloud Meko, every write and every personal read carries a `user_id` that identifies **who** (the cognito account) is calling. The filter tuple for personal memories and conversations is `(datapack_id, user_id, agent_id)` — you only ever see memories your own user_id wrote (or content other team members have explicitly promoted to Shared Knowledge, which is separate — see `tools-agent-id-conventions.md`).
4
4
 
5
- > **Note on `agent_id`.** In the current schema `agent_id` is overloaded — it's used for both isolation/namespacing and agent labeling. Discover it via `db_summarize_database` at session start rather than defaulting to the current tool's name. See the main SKILL.md section "Discover the user's `agent_id` namespace" and issue [#10](https://github.com/amiramm/meko-mcp-server/issues/10). The examples below use `claude_code` for illustration; substitute the discovered value at runtime.
5
+ ## The default behavior: automatic user_id from cognito
6
6
 
7
- ## Why user_id matters
7
+ On Cloud, the server resolves your `user_id` automatically from the cognito account attached to your MCP session (the API key or OAuth token). You do not need to pass `user_id` on every call — omit it and the server fills in the account owner's id.
8
8
 
9
- Without `user_id`, the server defaults to `{agent_id}_user` — a single undifferentiated bucket. This works for a solo developer in a single session, but breaks the moment:
10
-
11
- - A second person uses the same agent
12
- - An autonomous agent serves multiple end-users
13
- - You want to retrieve one person's memories without pulling everyone else's
14
-
15
- **Always pass `user_id` explicitly.** Relying on the default creates data you can't cleanly separate later.
16
-
17
- ## The two usage patterns
18
-
19
- ### Interactive coding agent (e.g., Claude Code)
20
-
21
- You are a coding assistant working with one human at a time. You know who they are — use their identity as `user_id`.
9
+ This is the common case for Claude Code / Claude Desktop / Cursor:
22
10
 
23
11
  ```
24
- memory_add(scope="write", agent_id="claude_code", user_id="alice",
25
- text="Prefers concise responses, no emojis.")
26
-
27
- memory_search(scope="read", query="response preferences", agent_id="claude_code", user_id="alice")
28
-
29
- conversation_create(scope="write", agent_id="claude_code", user_id="alice",
30
- title="Debugging auth middleware")
12
+ memory_add(scope="write",
13
+ text="User prefers concise responses, no emojis.",
14
+ agent_id="claude_code:meko-mcp-server",
15
+ conversation_id="<session conversation_id>")
16
+ # → stored with user_id = your cognito account id (auto)
31
17
  ```
32
18
 
33
- **How to determine user_id:**
34
- - Use the person's first name, lowercase: `"alice"`, `"bob"`, `"kyle"`
35
- - If the name is ambiguous within the org, append a qualifier: `"alice_eng"`, `"alice_sales"`
36
- - Use the same value consistently across all calls in a session — and across sessions
37
- - If you don't know the user's name yet, ask. Don't fall back to the default.
19
+ A second person signing into the same Cloud datapack with their own account gets their own `user_id` automatically — they can never read your un-promoted memories via MCP, and you can never read theirs.
38
20
 
39
- ### Autonomous multi-user agent (e.g., support bot, team assistant)
21
+ ## When to pass user_id explicitly
40
22
 
41
- The agent serves many users. Each user's data must be isolated so memories, conversations, and preferences don't leak across users.
23
+ Explicit `user_id` is an **optional sub-scope** for agents that serve multiple end-users behind a single Meko account — think a support bot or team assistant where one cognito account is the "operator" but the agent is acting on behalf of different people. Each end-user's memories live in a separate partition under the same cognito account.
42
24
 
43
25
  ```
44
26
  # Serving Alice
45
- memory_add(scope="write", agent_id="support_bot", user_id="alice_123",
46
- text="Alice prefers email communication. Has Pro plan.")
47
-
48
- # Serving Bob — completely separate memory space
49
- memory_add(scope="write", agent_id="support_bot", user_id="bob_456",
50
- text="Bob prefers Slack. On Enterprise plan.")
51
-
52
- # Searching only returns Alice's memories
53
- memory_search(scope="read", query="communication preference",
54
- agent_id="support_bot", user_id="alice_123")
27
+ memory_add(scope="write",
28
+ text="Alice prefers email communication. Has Pro plan.",
29
+ agent_id="support-bot",
30
+ user_id="alice_123",
31
+ conversation_id="<conv>")
32
+
33
+ # Serving Bob — completely separate partition, same cognito account
34
+ memory_add(scope="write",
35
+ text="Bob prefers Slack. On Enterprise plan.",
36
+ agent_id="support-bot",
37
+ user_id="bob_456",
38
+ conversation_id="<conv>")
39
+
40
+ # Search only returns Alice's memories
41
+ memory_search(scope="read",
42
+ query="communication preference",
43
+ agent_id="support-bot",
44
+ user_id="alice_123",
45
+ conversation_id="<conv>")
55
46
  ```
56
47
 
57
- **How to determine user_id:**
58
- - Use a stable identifier from your auth/identity system: username, email prefix, or external ID
59
- - Must be consistent across sessions — the same person must always map to the same `user_id`
60
- - Keep it human-readable when possible (debugging is easier with `"alice"` than `"usr_8f3a2b"`)
48
+ This is rarely relevant for Claude Code / Desktop / Cursor — those are single-human clients, and the automatic cognito user_id is the right behavior. It matters for autonomous multi-user agents built on top of the Meko API.
49
+
50
+ ## Gotcha — mixing scoped and unscoped calls
61
51
 
62
- ## Scoping behavior
52
+ If you pass `user_id="alice"` on writes but omit it on searches, the search falls back to the auto cognito user_id and **will not find** Alice's memories. They're in a different partition. Pick a convention for a given agent and stick to it:
63
53
 
64
- `user_id` filters data at the Mem0/Langfuse layer:
54
+ - Claude Code / Desktop / Cursor: omit `user_id`, rely on the auto cognito value.
55
+ - Multi-user autonomous agents: pass `user_id` consistently on every write and read.
65
56
 
66
- | Call | What it returns |
67
- |------|----------------|
68
- | `memory_search(agent_id="bot", user_id="alice")` | Only Alice's memories for this agent |
69
- | `memory_search(agent_id="bot")` | Memories stored without a `user_id`, or with the default `bot_user` |
70
- | `memory_get_all(agent_id="bot", user_id="alice")` | All of Alice's memories |
71
- | `conversation_list(agent_id="bot", user_id="alice")` | Only Alice's conversations |
57
+ Mixing creates invisible data.
72
58
 
73
- **Mixing scoped and unscoped calls creates invisible data.** If you store with `user_id="alice"` but search without `user_id`, the search won't find those memories — they live in a different partition. Pick a convention and stick to it.
59
+ ## How user_id relates to agent_id and the Shared Knowledge split
74
60
 
75
- ## Rules summary
61
+ | Layer | Filter | Role of user_id |
62
+ |---|---|---|
63
+ | Personal memories (`memory_add`, `memory_search`, `memory_get_all`) | `(datapack_id, user_id, agent_id)` | Enforced on every read. One user cannot see another's un-promoted memories. |
64
+ | Personal conversations (`conversation_list`, `conversation_get`) | Same tuple | Enforced. Cross-user reads require promotion. |
65
+ | Team Shared Knowledge (`knowledgebase_search`) | `(datapack_id)` only | **Not used** — `user_id` is stripped when a memory is promoted to Knowledge. All team members on the datapack see the same Shared Knowledge. |
76
66
 
77
- 1. **Always pass `user_id` explicitly** — never rely on the server default
78
- 2. **Use the human's identity** — first name lowercase, consistent across sessions
79
- 3. **Same `user_id` for store and retrieve** — mismatched scoping creates invisible data
80
- 4. **Ask if you don't know** — it's better to ask the user's name once than to store data you can't find later
81
- 5. **One human = one `user_id`** — don't create separate IDs for the same person across contexts
67
+ The "Promote to Knowledge" action in the Cloud UI is the one way personal content crosses the user_id boundary. There is no MCP tool for promotion (user-initiated via UI only).
82
68
 
83
- ## Relationship to other parameters
69
+ ## Parameter summary
84
70
 
85
- | Parameter | Identifies | Example |
86
- |-----------|-----------|---------|
87
- | `agent_id` | The AI agent | `"claude_code"`, `"support_bot"` |
88
- | `user_id` | The human being served | `"amiram"`, `"alice_123"` |
89
- | `app_id` | The application/product (optional) | `"second_brain"`, `"helpdesk"` |
90
- | `run_id` | A specific execution run (optional) | `"run_20260415_001"` |
91
- | `datapack_id` | The target database/datapack (optional) | `"dp-uuid-123"` |
71
+ | Parameter | Identifies | Typical value | Required? |
72
+ |---|---|---|---|
73
+ | `datapack_id` | Which datapack's DB/Langfuse project | `"c038ba7b-..."` (UUID) | Optional on most tools (default datapack if omitted). **Required** on `knowledgebase_search`. |
74
+ | `user_id` | The cognito account or end-user (for multi-user agents) | auto from cognito for single-human clients; explicit string for multi-user agents | Optional (server auto-fills from cognito context) |
75
+ | `agent_id` | Which agent wrote / is reading | `"claude_code:<repo>"`, `"claude_desktop"`, `"cursor:<repo>"`, `"meko_agent"` (common bucket) | Optional (empty/missing → `meko_agent` server-side); pass the per-session value verbatim for project-scoped reads/writes. Ignored on `knowledgebase_search`. |
76
+ | `app_id` | Optional application/product sub-scope | `"second_brain"`, `"helpdesk"` | Optional |
77
+ | `run_id` | Optional specific execution run | `"run_20260507_001"` | Optional |
92
78
 
93
- Together, `agent_id` + `user_id` form the primary namespace for memory and conversation isolation. `app_id` and `run_id` provide optional finer-grained grouping within that namespace.
79
+ See `tools-agent-id-conventions.md` for the full `agent_id` model.
@@ -7,11 +7,12 @@ 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 } from '../lib/logger.mjs';
10
+ import { banner, error, dim, info } from '../lib/logger.mjs';
11
11
  import { isPackagedInstall, selfInvocation } from '../lib/clients/common.mjs';
12
12
  import { getClientAdapters } from '../lib/clients/index.mjs';
13
13
  import { parseClientList } from '../lib/clients/aliases.mjs';
14
14
  import { startMigration, runChildMigration, readStatus, stopMigration } from '../lib/migrate.mjs';
15
+ import { checkLatest } from '../lib/version-check.mjs';
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Flag definitions
@@ -27,7 +28,9 @@ const options = {
27
28
  'cursor-hook-mode': { type: 'string', default: 'hybrid' },
28
29
  'skip-skills': { type: 'boolean', default: false },
29
30
  'skip-validation': { type: 'boolean', default: false },
31
+ 'no-canary': { type: 'boolean', default: false },
30
32
  'dry-run': { type: 'boolean', default: false },
33
+ 'check-updates': { type: 'boolean', default: false },
31
34
  uninstall: { type: 'boolean', default: false },
32
35
  verbose: { type: 'boolean', default: false },
33
36
  yes: { type: 'boolean', default: false },
@@ -43,6 +46,7 @@ const options = {
43
46
  rate: { type: 'string' },
44
47
  'no-extract': { type: 'boolean', default: false },
45
48
  'run-id': { type: 'string' },
49
+ datapack: { type: 'string' },
46
50
  '__migrate-child': { type: 'boolean', default: false },
47
51
  };
48
52
 
@@ -59,7 +63,7 @@ function readVersion() {
59
63
  return pkg.version;
60
64
  }
61
65
 
62
- const ALL_CLIENTS = ['claude-code', 'claude-desktop', 'cursor'];
66
+ const ALL_CLIENTS = ['claude-code', 'claude-desktop', 'cursor', 'codex'];
63
67
 
64
68
  function printHelp() {
65
69
  const text = `
@@ -86,22 +90,32 @@ Examples:
86
90
  # Install on every supported client:
87
91
  create-meko-setup --client all --local --yes
88
92
 
93
+ # Install for OpenAI Codex (CLI + Desktop App share the same config):
94
+ create-meko-setup --client codex --yes
95
+
89
96
  # Local Meko (docker-compose stack, Claude Code):
90
97
  create-meko-setup --local --yes
91
98
 
92
99
  Options:
93
- --client <list> Target clients: one or more of claude-code, claude-desktop, cursor.
100
+ --client <list> Target clients: one or more of claude-code, claude-desktop, cursor, codex.
94
101
  "claude" is accepted as an alias for claude-code.
95
102
  Comma-separated, or "all" to target every supported client (default: claude-code)
103
+ codex covers BOTH the OpenAI Codex CLI and the Codex Desktop App
104
+ (they share ~/.codex/config.toml).
96
105
  --url <url> MCP server URL (from app.mekodev.com > Datapacks > Connect)
97
106
  --api-key <key> API key for Cloud Meko authentication
98
107
  --local Use local Meko at http://localhost:8000/mcp
99
108
  --scope <scope> Config scope: user or project (default: user)
109
+ codex always installs at user scope (no project-level
110
+ config); --scope=project degrades with a warning.
100
111
  --name <name> MCP server name in config (default: meko)
101
112
  --cursor-hook-mode Cursor hook mode: native, claude-compat, hybrid (default: hybrid)
102
113
  --skip-skills Skip plugin and skills installation
103
114
  --skip-validation Skip post-install validation
115
+ --no-canary Skip the canary datapack_list call. The
116
+ connectivity + tools/list checks still run.
104
117
  --dry-run Show what would be done without making changes
118
+ --check-updates Compare installed version against npm registry and exit
105
119
  --uninstall Remove Meko MCP server, skills, and env vars
106
120
  --verbose Show detailed output
107
121
  --yes Accept all defaults (non-interactive)
@@ -121,6 +135,10 @@ Migrate existing conversations and memories into Meko:
121
135
  --rate <n> Max concurrent MCP calls during migration (default 4).
122
136
  --force Ignore a live lockfile or re-ingest processed items.
123
137
  --run-id <id> Target a specific run-id for --migrate-status.
138
+ --datapack <name|id> Migration destination datapack. Accepts a datapack
139
+ name or UUID. When omitted, the user's default
140
+ datapack is used (resolved at run time and printed
141
+ before the run starts).
124
142
  `.trimStart();
125
143
 
126
144
  console.log(text);
@@ -130,6 +148,31 @@ function isCloudUrl(url) {
130
148
  return url && !url.includes('localhost') && !url.includes('127.0.0.1');
131
149
  }
132
150
 
151
+ /**
152
+ * Sanitize a raw API key from any source (CLI flag, env var, askSecret prompt).
153
+ *
154
+ * Strips surrounding whitespace and a single matching pair of surrounding
155
+ * quotes. The contaminated value would otherwise be concatenated into
156
+ * `Authorization: Bearer <key>` verbatim and 401 against any server. Common
157
+ * sources of contamination: a trailing newline from
158
+ * `export MEKO_API_KEY=$(cat token.txt)`, a literal `"..."` copied out of a
159
+ * `.env` file, or a stray space at the end of a paste.
160
+ *
161
+ * Returns `null` for null/undefined/empty so the existing `!apiKey` checks
162
+ * still funnel into the "no key supplied" path.
163
+ */
164
+ function normalizeApiKey(value) {
165
+ if (value === null || value === undefined) return null;
166
+ let s = String(value).trim();
167
+ if (
168
+ (s.startsWith('"') && s.endsWith('"') && s.length >= 2) ||
169
+ (s.startsWith("'") && s.endsWith("'") && s.length >= 2)
170
+ ) {
171
+ s = s.slice(1, -1).trim();
172
+ }
173
+ return s || null;
174
+ }
175
+
133
176
  /**
134
177
  * Parse --client into a deduplicated list of client IDs.
135
178
  * Accepts a single value, a comma-separated list, or the keyword "all".
@@ -208,9 +251,41 @@ async function main() {
208
251
  // --version
209
252
  if (flags.version) {
210
253
  console.log(readVersion());
254
+ console.log('(run with --check-updates to compare against npm registry)');
211
255
  process.exit(0);
212
256
  }
213
257
 
258
+ // --check-updates — synchronous probe of the npm registry. Never fails:
259
+ // network errors produce "status: network error (<message>)" and exit 0.
260
+ if (flags['check-updates']) {
261
+ const current = readVersion();
262
+ const result = await checkLatest({ currentVersion: current });
263
+ console.log(`current: ${current}`);
264
+ if (result.latest) {
265
+ console.log(`latest: ${result.latest}`);
266
+ if (result.isOutdated) {
267
+ console.log('status: outdated — run `npx @yugabytedb/meko-mcp@latest` to upgrade');
268
+ } else {
269
+ console.log('status: up to date');
270
+ }
271
+ } else {
272
+ console.log('latest: unknown');
273
+ console.log(`status: network error (${result.error ?? 'unknown'})`);
274
+ }
275
+ process.exit(0);
276
+ }
277
+
278
+ // Kick off a background version check on normal runs. We intentionally don't
279
+ // await — if the registry is slow or unreachable we don't want to delay
280
+ // (or fail) the install. `background: true` unrefs the underlying socket so
281
+ // an in-flight probe never keeps Node alive after the install exits; the
282
+ // result is still consulted (via Promise.race with an already-resolved
283
+ // sentinel) in time for the post-install banner when it arrives early.
284
+ const versionCheckPromise = checkLatest({ currentVersion: readVersion(), background: true });
285
+ versionCheckPromise.catch(() => {
286
+ /* checkLatest never rejects, but belt-and-suspenders. */
287
+ });
288
+
214
289
  // --- Migrate subcommand dispatch ---
215
290
 
216
291
  // Detached child branch: re-entry inside the spawned process.
@@ -230,11 +305,12 @@ async function main() {
230
305
  runId: flags['run-id'],
231
306
  scope: flags.scope,
232
307
  url: flags.url,
233
- apiKey: process.env.MEKO_API_KEY ?? null,
308
+ apiKey: normalizeApiKey(process.env.MEKO_API_KEY),
234
309
  projectRoot,
235
310
  noExtract: flags['no-extract'],
236
311
  force: flags.force,
237
312
  rate: childRate,
313
+ datapack: flags.datapack ?? null,
238
314
  });
239
315
  process.exit(code);
240
316
  }
@@ -257,7 +333,7 @@ async function main() {
257
333
  process.exit(1);
258
334
  }
259
335
  let url = flags.url ?? process.env.MEKO_MCP_URL ?? null;
260
- let apiKey = flags['api-key'] ?? process.env.MEKO_API_KEY ?? null;
336
+ let apiKey = normalizeApiKey(flags['api-key'] ?? process.env.MEKO_API_KEY);
261
337
  if (flags.local) { url = 'http://localhost:8000/mcp'; apiKey = null; }
262
338
 
263
339
  let parentRate;
@@ -274,6 +350,8 @@ async function main() {
274
350
  noExtract: flags['no-extract'],
275
351
  force: flags.force,
276
352
  rate: parentRate,
353
+ nonInteractive: flags.yes,
354
+ datapack: flags.datapack ?? null,
277
355
  });
278
356
  console.log(out.message);
279
357
  process.exit(out.ok ? 0 : 1);
@@ -289,6 +367,15 @@ async function main() {
289
367
  process.exit(1);
290
368
  }
291
369
 
370
+ // Validate --scope BEFORE any branch that interpolates it into a shell
371
+ // command. The --uninstall path now forwards --scope to
372
+ // `claude plugin uninstall --scope <X>`, so an invalid value must be
373
+ // caught here, not after it's already been handed to the shell.
374
+ if (!isValidScope(flags.scope)) {
375
+ error(`Unknown scope "${flags.scope}". Valid values: user, project.`);
376
+ process.exit(1);
377
+ }
378
+
292
379
  // --uninstall
293
380
  if (flags.uninstall) {
294
381
  for (const client of clients) {
@@ -303,19 +390,20 @@ async function main() {
303
390
  }
304
391
  process.exit(0);
305
392
  }
306
-
307
- if (!isValidScope(flags.scope)) {
308
- error(`Unknown scope "${flags.scope}". Valid values: user, project.`);
309
- process.exit(1);
310
- }
311
393
  if (!isValidCursorHookMode(flags['cursor-hook-mode'])) {
312
394
  error(`Unknown --cursor-hook-mode "${flags['cursor-hook-mode']}". Valid values: native, claude-compat, hybrid.`);
313
395
  process.exit(1);
314
396
  }
315
397
 
316
398
  let url = flags.url ?? null;
317
- let apiKey = flags['api-key'] ?? null;
399
+ let apiKey = normalizeApiKey(flags['api-key']);
318
400
  let skipSkills = flags['skip-skills'];
401
+ // Tracks whether the full interactive flow already prompted for the API
402
+ // key. The Cloud-URL fallback below should only run for the
403
+ // semi-interactive `--url ... --client ...` paste path — otherwise
404
+ // submitting an empty value at the full-flow prompt would re-ask the
405
+ // same question.
406
+ let promptedForApiKey = false;
319
407
 
320
408
  // --local shortcut
321
409
  if (flags.local) {
@@ -336,6 +424,7 @@ async function main() {
336
424
  'claude-code': 'Claude Code',
337
425
  'claude-desktop': 'Claude Desktop',
338
426
  cursor: 'Cursor',
427
+ codex: 'OpenAI Codex (CLI + App)',
339
428
  }[id];
340
429
  return {
341
430
  label: id,
@@ -391,8 +480,11 @@ async function main() {
391
480
  // Step 2 — MCP URL
392
481
  url = await askText('MCP server URL (from app.mekodev.com > Datapacks > Connect):');
393
482
 
394
- // Step 3 — API key
395
- apiKey = await askSecret('API key:');
483
+ // Step 3 — API key. Normalize: terminal paste can drag in trailing
484
+ // whitespace/CR or accidental surrounding quotes, all of which would
485
+ // sail straight into `Authorization: Bearer ...` and 401.
486
+ apiKey = normalizeApiKey(await askSecret('API key:'));
487
+ promptedForApiKey = true;
396
488
  } else {
397
489
  url = 'http://localhost:8000/mcp';
398
490
  apiKey = null;
@@ -414,9 +506,22 @@ async function main() {
414
506
  process.exit(1);
415
507
  }
416
508
 
417
- // Validation — Cloud URL requires an API key
509
+ // Validation — Cloud URL requires an API key.
510
+ // Resolution order: --api-key (already in `apiKey`) → $MEKO_API_KEY →
511
+ // interactive prompt (only on a TTY, not under --yes, and only if the
512
+ // full interactive flow above didn't already ask). The prompt fallback
513
+ // exists so a user who pastes the cloud one-liner from the UI (which
514
+ // only shows --url and --client) isn't dead-ended on a hard error when
515
+ // they forgot to supply the key. Skipping it after a full-flow prompt
516
+ // avoids re-asking the same question when the user submitted empty.
418
517
  if (isCloudUrl(url) && !apiKey) {
419
- apiKey = process.env.MEKO_API_KEY ?? null;
518
+ apiKey = normalizeApiKey(process.env.MEKO_API_KEY);
519
+ if (apiKey) {
520
+ dim('Using API key from MEKO_API_KEY environment variable.');
521
+ } else if (!flags.yes && !promptedForApiKey && process.stdin.isTTY) {
522
+ dim('No --api-key or MEKO_API_KEY env var found; prompting for API key.');
523
+ apiKey = normalizeApiKey(await askSecret('API key:'));
524
+ }
420
525
  if (!apiKey) {
421
526
  error(
422
527
  'API key is required for Cloud Meko. ' +
@@ -424,7 +529,6 @@ async function main() {
424
529
  );
425
530
  process.exit(1);
426
531
  }
427
- dim('Using API key from MEKO_API_KEY environment variable.');
428
532
  }
429
533
 
430
534
  // Install — loop through each selected client sequentially so failures
@@ -439,6 +543,7 @@ async function main() {
439
543
  name: flags.name,
440
544
  skipSkills,
441
545
  skipValidation: flags['skip-validation'],
546
+ skipCanary: flags['no-canary'],
442
547
  dryRun: flags['dry-run'],
443
548
  verbose: flags.verbose,
444
549
  yes: flags.yes,
@@ -472,6 +577,7 @@ async function main() {
472
577
  apiKey,
473
578
  scope: flags.scope,
474
579
  projectRoot,
580
+ datapack: flags.datapack ?? null,
475
581
  });
476
582
  } else if (flags.yes) {
477
583
  console.log('');
@@ -482,16 +588,35 @@ async function main() {
482
588
  apiKey,
483
589
  scope: flags.scope,
484
590
  projectRoot,
591
+ datapack: flags.datapack ?? null,
485
592
  });
486
593
  }
487
594
  }
595
+
596
+ // Upgrade hint — only print if the background probe finished in time.
597
+ // We race the check against a pre-resolved sentinel so an in-flight
598
+ // probe (slow network) is silently dropped instead of delaying exit.
599
+ try {
600
+ const raced = await Promise.race([
601
+ versionCheckPromise,
602
+ Promise.resolve({ timedOut: true }),
603
+ ]);
604
+ if (raced && raced.isOutdated === true && raced.latest && raced.current) {
605
+ info(
606
+ `A newer version is available (${raced.latest}, you have ${raced.current}). ` +
607
+ 'Re-run with `npx @yugabytedb/meko-mcp@latest`.',
608
+ );
609
+ }
610
+ } catch {
611
+ /* never reached — checkLatest doesn't reject — but don't block exit. */
612
+ }
488
613
  }
489
614
 
490
615
  /**
491
616
  * Unattended migration — used by --yes --with-migrate.
492
617
  * No prompts; errors are surfaced but do not fail the install.
493
618
  */
494
- async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
619
+ async function runUnattendedMigration({ url, apiKey, scope, projectRoot, datapack }) {
495
620
  try {
496
621
  const out = await startMigration({
497
622
  url,
@@ -499,6 +624,8 @@ async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
499
624
  scope,
500
625
  projectRoot,
501
626
  scriptPath: __filename,
627
+ nonInteractive: true,
628
+ datapack: datapack ?? null,
502
629
  });
503
630
  console.log('');
504
631
  console.log(out.message);
@@ -516,7 +643,7 @@ async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
516
643
  * whether to kick off the detached migration. Any error here is non-fatal —
517
644
  * install has already succeeded.
518
645
  */
519
- async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
646
+ async function maybeOfferMigration({ url, apiKey, scope, projectRoot, datapack }) {
520
647
  try {
521
648
  const dry = await startMigration({
522
649
  url,
@@ -525,6 +652,7 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
525
652
  projectRoot,
526
653
  scriptPath: __filename,
527
654
  dryRun: true,
655
+ datapack: datapack ?? null,
528
656
  });
529
657
  const s = dry?.message?.match(/Claude Code sessions: *(\d+)/);
530
658
  const c = dry?.message?.match(/Cursor sessions: *(\d+)/);
@@ -559,6 +687,7 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
559
687
  scope,
560
688
  projectRoot,
561
689
  scriptPath: __filename,
690
+ datapack: datapack ?? null,
562
691
  });
563
692
  console.log(out.message);
564
693
  } catch (err) {