@yugabytedb/meko-mcp 1.0.6 → 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.
- package/README.md +189 -36
- package/assets/hooks-handlers/lib/capture.js +177 -78
- package/assets/hooks-handlers/lib/package.json +1 -0
- package/assets/hooks-handlers/test_single_conversation.sh +1 -1
- 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/bin/create-meko-setup.mjs +152 -26
- package/lib/clients/aliases.mjs +30 -0
- package/lib/clients/claude-code.mjs +384 -69
- package/lib/clients/claude-desktop.mjs +43 -8
- package/lib/clients/codex.mjs +977 -0
- package/lib/clients/common.mjs +142 -1
- package/lib/clients/cursor.mjs +73 -5
- package/lib/clients/index.mjs +2 -0
- package/lib/config.mjs +30 -0
- package/lib/detect.mjs +52 -44
- package/lib/installer.mjs +9 -2
- package/lib/mcp-transport.mjs +63 -4
- 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/mcp-client.mjs +1 -1
- package/lib/migrate/orchestrator.mjs +68 -10
- package/lib/migrate.mjs +346 -3
- package/lib/prompt.mjs +50 -76
- package/lib/validate.mjs +314 -10
- package/lib/version-check.mjs +178 -0
- package/package.json +2 -1
|
@@ -1,93 +1,79 @@
|
|
|
1
|
-
# user_id
|
|
1
|
+
# user_id — identity scoping on Cloud Meko
|
|
2
2
|
|
|
3
|
-
`user_id` identifies
|
|
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
|
-
|
|
5
|
+
## The default behavior: automatic user_id from cognito
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## When to pass user_id explicitly
|
|
40
22
|
|
|
41
|
-
|
|
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",
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
## How user_id relates to agent_id and the Shared Knowledge split
|
|
74
60
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
69
|
+
## Parameter summary
|
|
84
70
|
|
|
85
|
-
| Parameter | Identifies |
|
|
86
|
-
|
|
87
|
-
| `
|
|
88
|
-
| `user_id` | The human
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
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
|
-
|
|
79
|
+
See `tools-agent-id-conventions.md` for the full `agent_id` model.
|
|
@@ -7,10 +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
|
+
import { parseClientList } from '../lib/clients/aliases.mjs';
|
|
13
14
|
import { startMigration, runChildMigration, readStatus, stopMigration } from '../lib/migrate.mjs';
|
|
15
|
+
import { checkLatest } from '../lib/version-check.mjs';
|
|
14
16
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Flag definitions
|
|
@@ -26,7 +28,9 @@ const options = {
|
|
|
26
28
|
'cursor-hook-mode': { type: 'string', default: 'hybrid' },
|
|
27
29
|
'skip-skills': { type: 'boolean', default: false },
|
|
28
30
|
'skip-validation': { type: 'boolean', default: false },
|
|
31
|
+
'no-canary': { type: 'boolean', default: false },
|
|
29
32
|
'dry-run': { type: 'boolean', default: false },
|
|
33
|
+
'check-updates': { type: 'boolean', default: false },
|
|
30
34
|
uninstall: { type: 'boolean', default: false },
|
|
31
35
|
verbose: { type: 'boolean', default: false },
|
|
32
36
|
yes: { type: 'boolean', default: false },
|
|
@@ -42,6 +46,7 @@ const options = {
|
|
|
42
46
|
rate: { type: 'string' },
|
|
43
47
|
'no-extract': { type: 'boolean', default: false },
|
|
44
48
|
'run-id': { type: 'string' },
|
|
49
|
+
datapack: { type: 'string' },
|
|
45
50
|
'__migrate-child': { type: 'boolean', default: false },
|
|
46
51
|
};
|
|
47
52
|
|
|
@@ -58,7 +63,7 @@ function readVersion() {
|
|
|
58
63
|
return pkg.version;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
const ALL_CLIENTS = ['claude-code', 'claude-desktop', 'cursor'];
|
|
66
|
+
const ALL_CLIENTS = ['claude-code', 'claude-desktop', 'cursor', 'codex'];
|
|
62
67
|
|
|
63
68
|
function printHelp() {
|
|
64
69
|
const text = `
|
|
@@ -85,21 +90,32 @@ Examples:
|
|
|
85
90
|
# Install on every supported client:
|
|
86
91
|
create-meko-setup --client all --local --yes
|
|
87
92
|
|
|
93
|
+
# Install for OpenAI Codex (CLI + Desktop App share the same config):
|
|
94
|
+
create-meko-setup --client codex --yes
|
|
95
|
+
|
|
88
96
|
# Local Meko (docker-compose stack, Claude Code):
|
|
89
97
|
create-meko-setup --local --yes
|
|
90
98
|
|
|
91
99
|
Options:
|
|
92
|
-
--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.
|
|
101
|
+
"claude" is accepted as an alias for claude-code.
|
|
93
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).
|
|
94
105
|
--url <url> MCP server URL (from app.mekodev.com > Datapacks > Connect)
|
|
95
106
|
--api-key <key> API key for Cloud Meko authentication
|
|
96
107
|
--local Use local Meko at http://localhost:8000/mcp
|
|
97
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.
|
|
98
111
|
--name <name> MCP server name in config (default: meko)
|
|
99
112
|
--cursor-hook-mode Cursor hook mode: native, claude-compat, hybrid (default: hybrid)
|
|
100
113
|
--skip-skills Skip plugin and skills installation
|
|
101
114
|
--skip-validation Skip post-install validation
|
|
115
|
+
--no-canary Skip the canary datapack_list call. The
|
|
116
|
+
connectivity + tools/list checks still run.
|
|
102
117
|
--dry-run Show what would be done without making changes
|
|
118
|
+
--check-updates Compare installed version against npm registry and exit
|
|
103
119
|
--uninstall Remove Meko MCP server, skills, and env vars
|
|
104
120
|
--verbose Show detailed output
|
|
105
121
|
--yes Accept all defaults (non-interactive)
|
|
@@ -119,6 +135,10 @@ Migrate existing conversations and memories into Meko:
|
|
|
119
135
|
--rate <n> Max concurrent MCP calls during migration (default 4).
|
|
120
136
|
--force Ignore a live lockfile or re-ingest processed items.
|
|
121
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).
|
|
122
142
|
`.trimStart();
|
|
123
143
|
|
|
124
144
|
console.log(text);
|
|
@@ -128,20 +148,40 @@ function isCloudUrl(url) {
|
|
|
128
148
|
return url && !url.includes('localhost') && !url.includes('127.0.0.1');
|
|
129
149
|
}
|
|
130
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
|
+
|
|
131
176
|
/**
|
|
132
177
|
* Parse --client into a deduplicated list of client IDs.
|
|
133
178
|
* Accepts a single value, a comma-separated list, or the keyword "all".
|
|
179
|
+
* Aliases (see CLIENT_ALIASES in ../lib/clients.mjs) are resolved before the
|
|
180
|
+
* validity check so the cloud UI snippet (`--client claude`) works verbatim.
|
|
134
181
|
* Returns { clients, invalid } where invalid lists any unknown tokens.
|
|
135
182
|
*/
|
|
136
183
|
function parseClients(raw) {
|
|
137
|
-
|
|
138
|
-
if (!trimmed) return { clients: ['claude-code'], invalid: [] };
|
|
139
|
-
if (trimmed.toLowerCase() === 'all') return { clients: [...ALL_CLIENTS], invalid: [] };
|
|
140
|
-
|
|
141
|
-
const tokens = trimmed.split(',').map((t) => t.trim()).filter(Boolean);
|
|
142
|
-
const invalid = tokens.filter((t) => !ALL_CLIENTS.includes(t));
|
|
143
|
-
const clients = [...new Set(tokens.filter((t) => ALL_CLIENTS.includes(t)))];
|
|
144
|
-
return { clients, invalid };
|
|
184
|
+
return parseClientList(raw, ALL_CLIENTS);
|
|
145
185
|
}
|
|
146
186
|
|
|
147
187
|
function isValidScope(scope) {
|
|
@@ -211,9 +251,41 @@ async function main() {
|
|
|
211
251
|
// --version
|
|
212
252
|
if (flags.version) {
|
|
213
253
|
console.log(readVersion());
|
|
254
|
+
console.log('(run with --check-updates to compare against npm registry)');
|
|
214
255
|
process.exit(0);
|
|
215
256
|
}
|
|
216
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
|
+
|
|
217
289
|
// --- Migrate subcommand dispatch ---
|
|
218
290
|
|
|
219
291
|
// Detached child branch: re-entry inside the spawned process.
|
|
@@ -233,11 +305,12 @@ async function main() {
|
|
|
233
305
|
runId: flags['run-id'],
|
|
234
306
|
scope: flags.scope,
|
|
235
307
|
url: flags.url,
|
|
236
|
-
apiKey: process.env.MEKO_API_KEY
|
|
308
|
+
apiKey: normalizeApiKey(process.env.MEKO_API_KEY),
|
|
237
309
|
projectRoot,
|
|
238
310
|
noExtract: flags['no-extract'],
|
|
239
311
|
force: flags.force,
|
|
240
312
|
rate: childRate,
|
|
313
|
+
datapack: flags.datapack ?? null,
|
|
241
314
|
});
|
|
242
315
|
process.exit(code);
|
|
243
316
|
}
|
|
@@ -260,7 +333,7 @@ async function main() {
|
|
|
260
333
|
process.exit(1);
|
|
261
334
|
}
|
|
262
335
|
let url = flags.url ?? process.env.MEKO_MCP_URL ?? null;
|
|
263
|
-
let apiKey = flags['api-key'] ?? process.env.MEKO_API_KEY
|
|
336
|
+
let apiKey = normalizeApiKey(flags['api-key'] ?? process.env.MEKO_API_KEY);
|
|
264
337
|
if (flags.local) { url = 'http://localhost:8000/mcp'; apiKey = null; }
|
|
265
338
|
|
|
266
339
|
let parentRate;
|
|
@@ -277,6 +350,8 @@ async function main() {
|
|
|
277
350
|
noExtract: flags['no-extract'],
|
|
278
351
|
force: flags.force,
|
|
279
352
|
rate: parentRate,
|
|
353
|
+
nonInteractive: flags.yes,
|
|
354
|
+
datapack: flags.datapack ?? null,
|
|
280
355
|
});
|
|
281
356
|
console.log(out.message);
|
|
282
357
|
process.exit(out.ok ? 0 : 1);
|
|
@@ -292,6 +367,15 @@ async function main() {
|
|
|
292
367
|
process.exit(1);
|
|
293
368
|
}
|
|
294
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
|
+
|
|
295
379
|
// --uninstall
|
|
296
380
|
if (flags.uninstall) {
|
|
297
381
|
for (const client of clients) {
|
|
@@ -306,19 +390,20 @@ async function main() {
|
|
|
306
390
|
}
|
|
307
391
|
process.exit(0);
|
|
308
392
|
}
|
|
309
|
-
|
|
310
|
-
if (!isValidScope(flags.scope)) {
|
|
311
|
-
error(`Unknown scope "${flags.scope}". Valid values: user, project.`);
|
|
312
|
-
process.exit(1);
|
|
313
|
-
}
|
|
314
393
|
if (!isValidCursorHookMode(flags['cursor-hook-mode'])) {
|
|
315
394
|
error(`Unknown --cursor-hook-mode "${flags['cursor-hook-mode']}". Valid values: native, claude-compat, hybrid.`);
|
|
316
395
|
process.exit(1);
|
|
317
396
|
}
|
|
318
397
|
|
|
319
398
|
let url = flags.url ?? null;
|
|
320
|
-
let apiKey = flags['api-key']
|
|
399
|
+
let apiKey = normalizeApiKey(flags['api-key']);
|
|
321
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;
|
|
322
407
|
|
|
323
408
|
// --local shortcut
|
|
324
409
|
if (flags.local) {
|
|
@@ -339,6 +424,7 @@ async function main() {
|
|
|
339
424
|
'claude-code': 'Claude Code',
|
|
340
425
|
'claude-desktop': 'Claude Desktop',
|
|
341
426
|
cursor: 'Cursor',
|
|
427
|
+
codex: 'OpenAI Codex (CLI + App)',
|
|
342
428
|
}[id];
|
|
343
429
|
return {
|
|
344
430
|
label: id,
|
|
@@ -394,8 +480,11 @@ async function main() {
|
|
|
394
480
|
// Step 2 — MCP URL
|
|
395
481
|
url = await askText('MCP server URL (from app.mekodev.com > Datapacks > Connect):');
|
|
396
482
|
|
|
397
|
-
// Step 3 — API key
|
|
398
|
-
|
|
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;
|
|
399
488
|
} else {
|
|
400
489
|
url = 'http://localhost:8000/mcp';
|
|
401
490
|
apiKey = null;
|
|
@@ -417,9 +506,22 @@ async function main() {
|
|
|
417
506
|
process.exit(1);
|
|
418
507
|
}
|
|
419
508
|
|
|
420
|
-
// 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.
|
|
421
517
|
if (isCloudUrl(url) && !apiKey) {
|
|
422
|
-
apiKey = process.env.MEKO_API_KEY
|
|
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
|
+
}
|
|
423
525
|
if (!apiKey) {
|
|
424
526
|
error(
|
|
425
527
|
'API key is required for Cloud Meko. ' +
|
|
@@ -427,7 +529,6 @@ async function main() {
|
|
|
427
529
|
);
|
|
428
530
|
process.exit(1);
|
|
429
531
|
}
|
|
430
|
-
dim('Using API key from MEKO_API_KEY environment variable.');
|
|
431
532
|
}
|
|
432
533
|
|
|
433
534
|
// Install — loop through each selected client sequentially so failures
|
|
@@ -442,6 +543,7 @@ async function main() {
|
|
|
442
543
|
name: flags.name,
|
|
443
544
|
skipSkills,
|
|
444
545
|
skipValidation: flags['skip-validation'],
|
|
546
|
+
skipCanary: flags['no-canary'],
|
|
445
547
|
dryRun: flags['dry-run'],
|
|
446
548
|
verbose: flags.verbose,
|
|
447
549
|
yes: flags.yes,
|
|
@@ -475,6 +577,7 @@ async function main() {
|
|
|
475
577
|
apiKey,
|
|
476
578
|
scope: flags.scope,
|
|
477
579
|
projectRoot,
|
|
580
|
+
datapack: flags.datapack ?? null,
|
|
478
581
|
});
|
|
479
582
|
} else if (flags.yes) {
|
|
480
583
|
console.log('');
|
|
@@ -485,16 +588,35 @@ async function main() {
|
|
|
485
588
|
apiKey,
|
|
486
589
|
scope: flags.scope,
|
|
487
590
|
projectRoot,
|
|
591
|
+
datapack: flags.datapack ?? null,
|
|
488
592
|
});
|
|
489
593
|
}
|
|
490
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
|
+
}
|
|
491
613
|
}
|
|
492
614
|
|
|
493
615
|
/**
|
|
494
616
|
* Unattended migration — used by --yes --with-migrate.
|
|
495
617
|
* No prompts; errors are surfaced but do not fail the install.
|
|
496
618
|
*/
|
|
497
|
-
async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
|
|
619
|
+
async function runUnattendedMigration({ url, apiKey, scope, projectRoot, datapack }) {
|
|
498
620
|
try {
|
|
499
621
|
const out = await startMigration({
|
|
500
622
|
url,
|
|
@@ -502,6 +624,8 @@ async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
|
|
|
502
624
|
scope,
|
|
503
625
|
projectRoot,
|
|
504
626
|
scriptPath: __filename,
|
|
627
|
+
nonInteractive: true,
|
|
628
|
+
datapack: datapack ?? null,
|
|
505
629
|
});
|
|
506
630
|
console.log('');
|
|
507
631
|
console.log(out.message);
|
|
@@ -519,7 +643,7 @@ async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
|
|
|
519
643
|
* whether to kick off the detached migration. Any error here is non-fatal —
|
|
520
644
|
* install has already succeeded.
|
|
521
645
|
*/
|
|
522
|
-
async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
|
|
646
|
+
async function maybeOfferMigration({ url, apiKey, scope, projectRoot, datapack }) {
|
|
523
647
|
try {
|
|
524
648
|
const dry = await startMigration({
|
|
525
649
|
url,
|
|
@@ -528,6 +652,7 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
|
|
|
528
652
|
projectRoot,
|
|
529
653
|
scriptPath: __filename,
|
|
530
654
|
dryRun: true,
|
|
655
|
+
datapack: datapack ?? null,
|
|
531
656
|
});
|
|
532
657
|
const s = dry?.message?.match(/Claude Code sessions: *(\d+)/);
|
|
533
658
|
const c = dry?.message?.match(/Cursor sessions: *(\d+)/);
|
|
@@ -562,6 +687,7 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
|
|
|
562
687
|
scope,
|
|
563
688
|
projectRoot,
|
|
564
689
|
scriptPath: __filename,
|
|
690
|
+
datapack: datapack ?? null,
|
|
565
691
|
});
|
|
566
692
|
console.log(out.message);
|
|
567
693
|
} catch (err) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Tokens the cloud UI or older docs pass on `--client` that should map to a
|
|
2
|
+
// canonical adapter ID. Kept in a tiny, side-effect-free module so tests can
|
|
3
|
+
// import it without booting the bin entry point.
|
|
4
|
+
export const CLIENT_ALIASES = Object.freeze({
|
|
5
|
+
claude: "claude-code",
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a `--client` argument into a deduplicated list of client IDs.
|
|
10
|
+
*
|
|
11
|
+
* @param {string|null|undefined} raw - Raw CLI argument value.
|
|
12
|
+
* @param {string[]} allClients - Canonical IDs accepted by the installer.
|
|
13
|
+
* @returns {{clients: string[], invalid: string[]}}
|
|
14
|
+
*/
|
|
15
|
+
export function parseClientList(raw, allClients) {
|
|
16
|
+
const trimmed = (raw ?? "").trim();
|
|
17
|
+
if (!trimmed) return { clients: ["claude-code"], invalid: [] };
|
|
18
|
+
if (trimmed.toLowerCase() === "all") {
|
|
19
|
+
return { clients: [...allClients], invalid: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tokens = trimmed
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((t) => t.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map((t) => CLIENT_ALIASES[t] ?? t);
|
|
27
|
+
const invalid = tokens.filter((t) => !allClients.includes(t));
|
|
28
|
+
const clients = [...new Set(tokens.filter((t) => allClients.includes(t)))];
|
|
29
|
+
return { clients, invalid };
|
|
30
|
+
}
|