@yugabytedb/meko-mcp 1.0.4 → 1.0.6
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 +32 -1
- package/bin/create-meko-setup.mjs +96 -11
- package/lib/clients/claude-code.mjs +9 -3
- package/lib/clients/claude-desktop.mjs +11 -1
- package/lib/clients/common.mjs +26 -0
- package/lib/clients/cursor.mjs +11 -0
- package/lib/installer.mjs +8 -13
- package/lib/logger.mjs +6 -6
- package/lib/migrate/orchestrator.mjs +45 -3
- package/lib/migrate.mjs +17 -8
- package/lib/prompt.mjs +74 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ The installer performs up to 5 steps (some are skippable via flags):
|
|
|
79
79
|
|------|--------|------------------------|
|
|
80
80
|
| 1 | Register MCP server | `claude mcp add meko <url> --transport http --scope user` (with `-H "API_KEY: <key>"` for cloud) |
|
|
81
81
|
| 2 | Set hook environment vars | Writes `MEKO_MCP_URL` and `MEKO_API_KEY` into `~/.claude/settings.json` under the `env` key |
|
|
82
|
-
| 3 | Register plugin marketplace | `claude plugin marketplace add yugabyte/meko-mcp-server --sparse skills --scope user` |
|
|
82
|
+
| 3 | Register plugin marketplace | `claude plugin marketplace add yugabyte/meko-mcp-server --sparse .claude-plugin,skills --scope user` |
|
|
83
83
|
| 4 | Install skills plugin | `claude plugin install meko-agent-skills --scope user` |
|
|
84
84
|
| 5 | Validate setup | Config check via `claude mcp list`, MCP `initialize` handshake, `tools/list` verification |
|
|
85
85
|
|
|
@@ -215,6 +215,37 @@ create-meko-setup --migrate-stop
|
|
|
215
215
|
create-meko-setup --migrate --scope user --no-extract
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
### Reading `--migrate-status` output
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
run-id: 20260429-000912-74dbe6
|
|
222
|
+
status: running (pending → running → done | failed | stopped)
|
|
223
|
+
scope: user
|
|
224
|
+
started: 2026-04-29T00:09:12.673Z
|
|
225
|
+
finished: (in progress)
|
|
226
|
+
current: claude-code:-Users-.../meko-test / a2f6a1d6-... (<adapter>:<source-session> / <message-id>)
|
|
227
|
+
sessions: 76 / 111 (processed / discovered)
|
|
228
|
+
messages: 674
|
|
229
|
+
memory pairs: 674 (memory_add from Mem0 extractor)
|
|
230
|
+
memory (MD): 0 (memory_add from CLAUDE.md / memory files)
|
|
231
|
+
warnings: 0 (non-fatal; run continues)
|
|
232
|
+
memory errs: 0 (failed memory_add calls; re-run with --force to retry)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
| Field | Meaning |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `run-id` | Unique ID; also the directory name under `~/.meko/migrate/<run-id>/`. |
|
|
238
|
+
| `status` | Terminal states are `done`, `failed`, `stopped`. A `running` row that hasn't advanced in >30 s may indicate the daemon died — check `~/.meko/migrate/<run-id>/stdout.log`. |
|
|
239
|
+
| `scope` | Matches the `--scope` flag used at start (`user` or `project`). |
|
|
240
|
+
| `started` / `finished` | UTC timestamps. `finished` shows `(in progress)` until a terminal state is reached. |
|
|
241
|
+
| `current` | Which source item is being replayed — `<adapter>:<source-session>` (adapter = `claude-code` / `cursor` / `memory-md`). Becomes stale on `done`/`failed`/`stopped`. |
|
|
242
|
+
| `sessions` | Conversation sessions processed / discovered across all adapters. Climbs during the run. |
|
|
243
|
+
| `messages` | Total messages replayed so far via `conversation_add_message`. |
|
|
244
|
+
| `memory pairs` | `memory_add` calls made via the Mem0 extractor path (messages-style input). Usually close to `messages` when the extractor is enabled. |
|
|
245
|
+
| `memory (MD)` | `memory_add` calls from `CLAUDE.md` / memory-file sources (plain-text input). |
|
|
246
|
+
| `warnings` | Non-fatal issues (skipped item, partial parse). Run keeps going. |
|
|
247
|
+
| `memory errs` | `memory_add` calls that failed. Non-zero means the memory extractor couldn't reach the server or rejected the payload — check the per-run log. Re-run with `--force` to retry only the failed items. |
|
|
248
|
+
|
|
218
249
|
### Behavior around install
|
|
219
250
|
|
|
220
251
|
- **Interactive install** (no `--yes`): after a successful install, the installer dry-runs the enumerator, prints how much local data would be migrated, and asks `Y/n`. Declining prints a reminder to run `--migrate` later.
|
|
@@ -6,8 +6,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
|
|
8
8
|
import { install, uninstall } from '../lib/installer.mjs';
|
|
9
|
-
import { askChoice, askText, askSecret, askYesNo, close as closePrompts } from '../lib/prompt.mjs';
|
|
9
|
+
import { askChoice, askMultiChoice, askText, askSecret, askYesNo, close as closePrompts } from '../lib/prompt.mjs';
|
|
10
10
|
import { banner, error, dim } from '../lib/logger.mjs';
|
|
11
|
+
import { isPackagedInstall, selfInvocation } from '../lib/clients/common.mjs';
|
|
12
|
+
import { getClientAdapters } from '../lib/clients/index.mjs';
|
|
11
13
|
import { startMigration, runChildMigration, readStatus, stopMigration } from '../lib/migrate.mjs';
|
|
12
14
|
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
@@ -146,6 +148,35 @@ function isValidScope(scope) {
|
|
|
146
148
|
return ['user', 'project'].includes(scope);
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Did the user pass --client on the CLI? parseArgs always materializes the
|
|
153
|
+
* default ("claude-code"), so we can't tell from flags alone — have to sniff
|
|
154
|
+
* argv. Used to decide whether to show the interactive client picker.
|
|
155
|
+
*/
|
|
156
|
+
function userPassedClientFlag() {
|
|
157
|
+
return process.argv.some(
|
|
158
|
+
(a) => a === '--client' || a.startsWith('--client='),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Which clients are actually installed on this machine? Uses each adapter's
|
|
164
|
+
* isInstalled() probe (CLI binary for Claude Code, config dir for Claude
|
|
165
|
+
* Desktop and Cursor). NOT detectPrerequisites — that checks deps like npx,
|
|
166
|
+
* which are present on basically any Node.js machine and would default the
|
|
167
|
+
* picker to "install everything". Returns a Set of client IDs.
|
|
168
|
+
*/
|
|
169
|
+
function detectInstalledClients() {
|
|
170
|
+
const adapters = getClientAdapters();
|
|
171
|
+
const detected = new Set();
|
|
172
|
+
for (const [id, adapter] of Object.entries(adapters)) {
|
|
173
|
+
if (typeof adapter.isInstalled === "function" && adapter.isInstalled()) {
|
|
174
|
+
detected.add(id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return detected;
|
|
178
|
+
}
|
|
179
|
+
|
|
149
180
|
function isValidCursorHookMode(mode) {
|
|
150
181
|
return ['native', 'claude-compat', 'hybrid'].includes(mode);
|
|
151
182
|
}
|
|
@@ -251,7 +282,7 @@ async function main() {
|
|
|
251
282
|
process.exit(out.ok ? 0 : 1);
|
|
252
283
|
}
|
|
253
284
|
|
|
254
|
-
|
|
285
|
+
let { clients, invalid } = parseClients(flags.client);
|
|
255
286
|
if (invalid.length > 0) {
|
|
256
287
|
error(`Unknown client(s): ${invalid.join(', ')}. Valid values: ${ALL_CLIENTS.join(', ')}, all.`);
|
|
257
288
|
process.exit(1);
|
|
@@ -295,15 +326,69 @@ async function main() {
|
|
|
295
326
|
apiKey = null;
|
|
296
327
|
}
|
|
297
328
|
|
|
329
|
+
// Client picker — runs for any interactive invocation (including
|
|
330
|
+
// semi-interactive `--url ...` without `--client`). Historically the
|
|
331
|
+
// installer silently defaulted to --client=claude-code, so a user with
|
|
332
|
+
// Cursor or Claude Desktop also installed would never know those
|
|
333
|
+
// clients weren't configured. Skip when the user explicitly passed
|
|
334
|
+
// --client, or --yes (non-interactive).
|
|
335
|
+
if (!userPassedClientFlag() && !flags.yes) {
|
|
336
|
+
const detected = detectInstalledClients();
|
|
337
|
+
const pickerOptions = ALL_CLIENTS.map((id) => {
|
|
338
|
+
const displayName = {
|
|
339
|
+
'claude-code': 'Claude Code',
|
|
340
|
+
'claude-desktop': 'Claude Desktop',
|
|
341
|
+
cursor: 'Cursor',
|
|
342
|
+
}[id];
|
|
343
|
+
return {
|
|
344
|
+
label: id,
|
|
345
|
+
description: detected.has(id) ? `${displayName} (detected)` : `${displayName} (not detected)`,
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
if (detected.size === 1) {
|
|
349
|
+
clients = [[...detected][0]];
|
|
350
|
+
dim(`Only ${pickerOptions.find((o) => o.label === clients[0]).description.replace(' (detected)', '')} detected; installing for it.`);
|
|
351
|
+
} else if (detected.size === 0) {
|
|
352
|
+
// No client detected — fall through to the existing default.
|
|
353
|
+
// Validation later in the install will raise a clearer error than a
|
|
354
|
+
// blank picker would.
|
|
355
|
+
dim('No supported clients auto-detected. Defaulting to Claude Code; pass --client to override.');
|
|
356
|
+
} else {
|
|
357
|
+
const picked = await askMultiChoice(
|
|
358
|
+
'Install Meko for which client(s)?',
|
|
359
|
+
pickerOptions,
|
|
360
|
+
{ defaults: [...detected] },
|
|
361
|
+
);
|
|
362
|
+
if (picked.length === 0) {
|
|
363
|
+
error('No clients selected. Aborting.');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
clients = picked;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
298
370
|
// Interactive mode: URL not provided and not auto-accepting defaults
|
|
299
371
|
if (!url && !flags.yes) {
|
|
300
|
-
banner();
|
|
301
|
-
|
|
302
|
-
// Step 1 — connection type
|
|
303
|
-
|
|
372
|
+
banner('Meko Setup');
|
|
373
|
+
|
|
374
|
+
// Step 1 — connection type. Local Meko only makes sense for users who
|
|
375
|
+
// have the server repo cloned locally (docker-compose stack on :8000);
|
|
376
|
+
// npm/npx users have no way to run it, so hide the option for them.
|
|
377
|
+
// When only Cloud is available, skip the prompt entirely — asking to
|
|
378
|
+
// pick between one option is pointless UX.
|
|
379
|
+
const connectionOptions = [
|
|
304
380
|
{ label: 'Cloud Meko', description: 'Hosted at mekodev.com (requires URL + API key)' },
|
|
305
|
-
|
|
306
|
-
|
|
381
|
+
];
|
|
382
|
+
if (!isPackagedInstall()) {
|
|
383
|
+
connectionOptions.push({
|
|
384
|
+
label: 'Local Meko',
|
|
385
|
+
description: 'Local stack at localhost:8000',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const connectionType =
|
|
389
|
+
connectionOptions.length === 1
|
|
390
|
+
? connectionOptions[0].label
|
|
391
|
+
: await askChoice('Connection type:', connectionOptions);
|
|
307
392
|
|
|
308
393
|
if (connectionType === 'Cloud Meko') {
|
|
309
394
|
// Step 2 — MCP URL
|
|
@@ -393,7 +478,7 @@ async function main() {
|
|
|
393
478
|
});
|
|
394
479
|
} else if (flags.yes) {
|
|
395
480
|
console.log('');
|
|
396
|
-
dim(
|
|
481
|
+
dim(`Tip: run \`${selfInvocation()} --migrate\` to import existing conversations and memories.`);
|
|
397
482
|
} else if (process.stdin.isTTY) {
|
|
398
483
|
await maybeOfferMigration({
|
|
399
484
|
url,
|
|
@@ -422,7 +507,7 @@ async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
|
|
|
422
507
|
console.log(out.message);
|
|
423
508
|
if (!out.ok) {
|
|
424
509
|
// Don't fail the install; the user can retry with --migrate.
|
|
425
|
-
dim(
|
|
510
|
+
dim(`Migration did not start cleanly. Install succeeded; try \`${selfInvocation()} --migrate\` later.`);
|
|
426
511
|
}
|
|
427
512
|
} catch (err) {
|
|
428
513
|
dim(`Migration could not be started: ${err.message ?? err}`);
|
|
@@ -468,7 +553,7 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
|
|
|
468
553
|
true,
|
|
469
554
|
);
|
|
470
555
|
if (!proceed) {
|
|
471
|
-
dim(
|
|
556
|
+
dim(`Skipped. Run \`${selfInvocation()} --migrate\` later when you're ready.`);
|
|
472
557
|
return;
|
|
473
558
|
}
|
|
474
559
|
const out = await startMigration({
|
|
@@ -64,6 +64,10 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
64
64
|
log.dim(` Claude CLI: ${claude.path} (${claude.version})`);
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
+
/** Is Claude Code actually installed on this machine? */
|
|
68
|
+
isInstalled() {
|
|
69
|
+
return detectClaude().found;
|
|
70
|
+
},
|
|
67
71
|
detectExisting(name) {
|
|
68
72
|
return detectExistingMeko(name);
|
|
69
73
|
},
|
|
@@ -142,7 +146,7 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
142
146
|
},
|
|
143
147
|
installSkills({ scope, dryRun, verbose, log }) {
|
|
144
148
|
const marketplaceCmd =
|
|
145
|
-
`claude plugin marketplace add yugabyte/meko-mcp-server --sparse skills --scope ${scope}`;
|
|
149
|
+
`claude plugin marketplace add yugabyte/meko-mcp-server --sparse .claude-plugin,skills --scope ${scope}`;
|
|
146
150
|
const pluginCmd = `claude plugin install meko-agent-skills --scope ${scope}`;
|
|
147
151
|
|
|
148
152
|
if (dryRun) {
|
|
@@ -161,7 +165,8 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
161
165
|
marketplaceStatus = "already";
|
|
162
166
|
} else {
|
|
163
167
|
marketplaceStatus = "error";
|
|
164
|
-
log.warn(
|
|
168
|
+
log.warn("Marketplace install skipped (non-critical).");
|
|
169
|
+
log.dim(msg.trim());
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
|
|
@@ -175,7 +180,8 @@ export function createClaudeCodeAdapter(options = {}) {
|
|
|
175
180
|
skillStatus = "already";
|
|
176
181
|
} else {
|
|
177
182
|
skillStatus = "error";
|
|
178
|
-
log.warn(
|
|
183
|
+
log.warn("Skills plugin install skipped (non-critical).");
|
|
184
|
+
log.dim(msg.trim());
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
187
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
getClaudeDesktopConfigPath,
|
|
@@ -61,6 +61,16 @@ export function createClaudeDesktopAdapter(options = {}) {
|
|
|
61
61
|
log.dim(` npx: ${npx.path}${npx.version ? ` (${npx.version})` : ""}`);
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
+
/**
|
|
65
|
+
* Is Claude Desktop actually installed? The app creates its config
|
|
66
|
+
* directory on first launch, so the presence of that directory is a
|
|
67
|
+
* strong signal the app has been run at least once on this machine.
|
|
68
|
+
* (A user who installed the app but never opened it would produce a
|
|
69
|
+
* false negative — acceptable; they'd pass --client explicitly.)
|
|
70
|
+
*/
|
|
71
|
+
isInstalled() {
|
|
72
|
+
return existsSync(dirname(configPathResolver()));
|
|
73
|
+
},
|
|
64
74
|
detectExisting(name) {
|
|
65
75
|
const cfg = readJsonFile(configPathResolver());
|
|
66
76
|
const entry = cfg?.mcpServers?.[name];
|
package/lib/clients/common.mjs
CHANGED
|
@@ -31,6 +31,32 @@ export function getAssetRoot() {
|
|
|
31
31
|
return join(getRepoRoot(), "skills");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* True when the installer is running from an npm tarball (prepack-bundled
|
|
36
|
+
* `installer/assets/` exists), false when running from a source checkout.
|
|
37
|
+
* Use this to branch UX between the two install paths — e.g. hiding options
|
|
38
|
+
* that only make sense for repo cloners.
|
|
39
|
+
*
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isPackagedInstall() {
|
|
43
|
+
const libDir = dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
return existsSync(resolve(libDir, "..", "..", "assets"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The command a user would run to re-invoke this installer from a fresh
|
|
49
|
+
* shell. Differs between npm/npx (no global bin — must re-invoke via npx)
|
|
50
|
+
* and repo clones (bin isn't on PATH either, so use `node ...`).
|
|
51
|
+
*
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function selfInvocation() {
|
|
55
|
+
return isPackagedInstall()
|
|
56
|
+
? "npx @yugabytedb/meko-mcp"
|
|
57
|
+
: "node installer/bin/create-meko-setup.mjs";
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
/**
|
|
35
61
|
* @param {string} name - skill directory name (e.g. "meko-mcp-tools")
|
|
36
62
|
* @returns {string} absolute path to the skill source directory
|
package/lib/clients/cursor.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
writeFileSync,
|
|
6
6
|
mkdirSync,
|
|
7
7
|
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import { dirname, join, resolve } from "node:path";
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -51,6 +52,16 @@ export function createCursorAdapter(customPaths = {}) {
|
|
|
51
52
|
async detectPrerequisites() {
|
|
52
53
|
// No CLI dependency for Cursor config-mode installs.
|
|
53
54
|
},
|
|
55
|
+
/**
|
|
56
|
+
* Is Cursor actually installed? Cursor creates `~/.cursor/` on first
|
|
57
|
+
* launch for MCP / hooks / skills config, so its existence is a strong
|
|
58
|
+
* signal the app has been run at least once on this machine. A user who
|
|
59
|
+
* installed Cursor but never opened it produces a false negative —
|
|
60
|
+
* acceptable; they'd pass --client explicitly.
|
|
61
|
+
*/
|
|
62
|
+
isInstalled() {
|
|
63
|
+
return existsSync(join(homedir(), ".cursor"));
|
|
64
|
+
},
|
|
54
65
|
detectExisting(name, { scope, projectRoot }) {
|
|
55
66
|
const path = paths.getMcpPath(scope, projectRoot);
|
|
56
67
|
const cfg = readJsonFile(path);
|
package/lib/installer.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
dim,
|
|
5
5
|
error,
|
|
6
6
|
info,
|
|
7
|
-
step,
|
|
8
7
|
stepFail,
|
|
9
8
|
stepOk,
|
|
10
9
|
success,
|
|
@@ -107,7 +106,6 @@ export async function install(options) {
|
|
|
107
106
|
let currentStep = 1;
|
|
108
107
|
|
|
109
108
|
if (!skipMcpRegistration) {
|
|
110
|
-
step(currentStep, totalSteps, "Registering MCP server...");
|
|
111
109
|
try {
|
|
112
110
|
adapter.registerServer({
|
|
113
111
|
name,
|
|
@@ -119,18 +117,17 @@ export async function install(options) {
|
|
|
119
117
|
log: LOG,
|
|
120
118
|
projectRoot,
|
|
121
119
|
});
|
|
122
|
-
stepOk(currentStep, totalSteps, dryRun ? "Registering MCP server (dry-run)" : "MCP server registered.");
|
|
120
|
+
stepOk(currentStep, totalSteps, dryRun ? "Registering MCP server (dry-run)." : "MCP server registered.");
|
|
123
121
|
} catch (err) {
|
|
124
122
|
stepFail(currentStep, totalSteps, "Failed to register MCP server.");
|
|
125
123
|
error(err.stderr || err.message);
|
|
126
124
|
throw new Error("MCP server registration failed.");
|
|
127
125
|
}
|
|
128
126
|
} else {
|
|
129
|
-
|
|
127
|
+
stepOk(currentStep, totalSteps, "MCP server registration skipped (existing config kept).");
|
|
130
128
|
}
|
|
131
129
|
currentStep++;
|
|
132
130
|
|
|
133
|
-
step(currentStep, totalSteps, "Configuring hook environment...");
|
|
134
131
|
try {
|
|
135
132
|
adapter.setHookEnvironment({
|
|
136
133
|
url,
|
|
@@ -143,7 +140,7 @@ export async function install(options) {
|
|
|
143
140
|
projectRoot,
|
|
144
141
|
cursorHookMode,
|
|
145
142
|
});
|
|
146
|
-
stepOk(currentStep, totalSteps, dryRun ? "Configuring hook environment (dry-run)" : "Hook environment configured.");
|
|
143
|
+
stepOk(currentStep, totalSteps, dryRun ? "Configuring hook environment (dry-run)." : "Hook environment configured.");
|
|
147
144
|
} catch (err) {
|
|
148
145
|
stepFail(currentStep, totalSteps, "Failed to configure hook environment.");
|
|
149
146
|
error(err.message);
|
|
@@ -152,7 +149,6 @@ export async function install(options) {
|
|
|
152
149
|
currentStep++;
|
|
153
150
|
|
|
154
151
|
if (!skipSkills) {
|
|
155
|
-
step(currentStep, totalSteps, "Installing skills support...");
|
|
156
152
|
try {
|
|
157
153
|
const result = adapter.installSkills({
|
|
158
154
|
scope,
|
|
@@ -164,7 +160,7 @@ export async function install(options) {
|
|
|
164
160
|
if (!dryRun && result?.marketplace === "manual") {
|
|
165
161
|
warn("Skills installation requires manual action for this client.");
|
|
166
162
|
}
|
|
167
|
-
stepOk(currentStep, totalSteps, dryRun ? "Installing skills support (dry-run)" : "Skills support configured.");
|
|
163
|
+
stepOk(currentStep, totalSteps, dryRun ? "Installing skills support (dry-run)." : "Skills support configured.");
|
|
168
164
|
} catch (err) {
|
|
169
165
|
stepFail(currentStep, totalSteps, "Failed to configure skills support.");
|
|
170
166
|
warn(`Non-critical: ${err.message}`);
|
|
@@ -173,10 +169,9 @@ export async function install(options) {
|
|
|
173
169
|
}
|
|
174
170
|
|
|
175
171
|
if (!skipValidation) {
|
|
176
|
-
step(currentStep, totalSteps, "Validating setup...");
|
|
177
172
|
if (dryRun) {
|
|
178
|
-
dim("
|
|
179
|
-
stepOk(currentStep, totalSteps, "Validating setup (dry-run)");
|
|
173
|
+
dim("Would validate config, MCP connectivity, and tool availability.");
|
|
174
|
+
stepOk(currentStep, totalSteps, "Validating setup (dry-run).");
|
|
180
175
|
} else {
|
|
181
176
|
try {
|
|
182
177
|
const results = await runValidation(url, apiKey, name, {
|
|
@@ -190,8 +185,8 @@ export async function install(options) {
|
|
|
190
185
|
{ label: "Tools", ...results.tools },
|
|
191
186
|
];
|
|
192
187
|
for (const check of checks) {
|
|
193
|
-
if (check.ok) success(
|
|
194
|
-
else warn(
|
|
188
|
+
if (check.ok) success(`${check.label}: ${check.message}`);
|
|
189
|
+
else warn(`${check.label}: ${check.message}`);
|
|
195
190
|
}
|
|
196
191
|
|
|
197
192
|
if (results.allPassed) {
|
package/lib/logger.mjs
CHANGED
|
@@ -55,26 +55,26 @@ export function error(msg) {
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Print a numbered step that is in progress.
|
|
58
|
-
* Example:
|
|
58
|
+
* Example: [1/5] Registering MCP server...
|
|
59
59
|
*/
|
|
60
60
|
export function step(n, total, msg) {
|
|
61
|
-
console.log(
|
|
61
|
+
console.log(`${BLUE}[${n}/${total}]${RESET} ${msg}`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Print a numbered step that succeeded.
|
|
66
|
-
* Example:
|
|
66
|
+
* Example: ✓ [1/5] MCP server registered.
|
|
67
67
|
*/
|
|
68
68
|
export function stepOk(n, total, msg) {
|
|
69
|
-
console.log(
|
|
69
|
+
console.log(`${GREEN}${CHECK}${RESET} ${BLUE}[${n}/${total}]${RESET} ${msg}`);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Print a numbered step that failed.
|
|
74
|
-
* Example:
|
|
74
|
+
* Example: ✗ [3/5] Registering plugin marketplace failed: ...
|
|
75
75
|
*/
|
|
76
76
|
export function stepFail(n, total, msg) {
|
|
77
|
-
console.log(
|
|
77
|
+
console.log(`${RED}${CROSS}${RESET} ${BLUE}[${n}/${total}]${RESET} ${msg}`);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
@@ -283,11 +283,17 @@ export async function runMigration(opts) {
|
|
|
283
283
|
const [, memRes] = await Promise.all([addMsgPromise, memPromise]);
|
|
284
284
|
messagesSent++;
|
|
285
285
|
if (!opts.noExtract) {
|
|
286
|
-
if (memRes && memRes.ok) {
|
|
286
|
+
if (memRes && memRes.ok && !isErrorText(memRes.text)) {
|
|
287
287
|
memoryPairsSent++;
|
|
288
288
|
prog.counts.memoryPairsSent++;
|
|
289
289
|
} else if (memRes) {
|
|
290
290
|
prog.counts.memoryErrors++;
|
|
291
|
+
log.warn("memory_add_failed", {
|
|
292
|
+
agentId: ref.agentId,
|
|
293
|
+
sessionUuid: ref.sessionUuid,
|
|
294
|
+
userUuid: ex.user_uuid,
|
|
295
|
+
detail: errorDetail(memRes),
|
|
296
|
+
});
|
|
291
297
|
}
|
|
292
298
|
}
|
|
293
299
|
lastUuid = ex.user_uuid;
|
|
@@ -361,7 +367,7 @@ export async function runMigration(opts) {
|
|
|
361
367
|
metadata: JSON.stringify(item.metadata),
|
|
362
368
|
}, { fireAndForget: true });
|
|
363
369
|
|
|
364
|
-
if (res.ok) {
|
|
370
|
+
if (res.ok && !isErrorText(res.text)) {
|
|
365
371
|
state.markMemory(st, item.agentId, item.hash, {
|
|
366
372
|
memoryId: extractId(res.text) || "",
|
|
367
373
|
name: item.name,
|
|
@@ -370,7 +376,12 @@ export async function runMigration(opts) {
|
|
|
370
376
|
prog.counts.memoriesFromMD++;
|
|
371
377
|
} else {
|
|
372
378
|
prog.counts.memoryErrors++;
|
|
373
|
-
log.warn("memory_md_failed", {
|
|
379
|
+
log.warn("memory_md_failed", {
|
|
380
|
+
agentId: item.agentId,
|
|
381
|
+
name: item.name,
|
|
382
|
+
error: res.error?.message,
|
|
383
|
+
detail: errorDetail(res),
|
|
384
|
+
});
|
|
374
385
|
}
|
|
375
386
|
if (++opsSinceFlush >= FLUSH_EVERY) flushState();
|
|
376
387
|
writeProgress();
|
|
@@ -441,3 +452,34 @@ function extractId(text) {
|
|
|
441
452
|
return null;
|
|
442
453
|
}
|
|
443
454
|
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The MCP transport returns `{ok:true}` whenever the HTTP call itself
|
|
458
|
+
* succeeded, even if the tool's body encoded a failure. memory_add in
|
|
459
|
+
* older server builds returned plain "Error adding memory: ..." strings
|
|
460
|
+
* on exception; newer builds return JSON `{"error":"memory_add_failed",...}`.
|
|
461
|
+
* Either way, the text content is the authoritative success signal.
|
|
462
|
+
*/
|
|
463
|
+
function isErrorText(text) {
|
|
464
|
+
if (!text) return false;
|
|
465
|
+
const trimmed = text.trim();
|
|
466
|
+
if (trimmed.startsWith("Error adding memory")) return true;
|
|
467
|
+
if (trimmed.startsWith("{")) {
|
|
468
|
+
try {
|
|
469
|
+
const obj = JSON.parse(trimmed);
|
|
470
|
+
if (obj && typeof obj === "object" && obj.error) return true;
|
|
471
|
+
} catch { /* not JSON; fall through */ }
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function errorDetail(res) {
|
|
477
|
+
if (!res) return null;
|
|
478
|
+
if (res.error?.message) return res.error.message;
|
|
479
|
+
if (!res.text) return null;
|
|
480
|
+
try {
|
|
481
|
+
const obj = JSON.parse(res.text);
|
|
482
|
+
if (obj?.error) return obj.detail || obj.error;
|
|
483
|
+
} catch { /* not JSON */ }
|
|
484
|
+
return res.text.slice(0, 200);
|
|
485
|
+
}
|
package/lib/migrate.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { createLogger } from "./migrate/logger.mjs";
|
|
|
17
17
|
import { generateRunId, forkDetached, migrateRoot } from "./migrate/daemon.mjs";
|
|
18
18
|
import * as lockfile from "./migrate/lockfile.mjs";
|
|
19
19
|
import * as progress from "./migrate/progress.mjs";
|
|
20
|
+
import { selfInvocation } from "./clients/common.mjs";
|
|
20
21
|
|
|
21
22
|
function home() {
|
|
22
23
|
return process.env.HOME || "";
|
|
@@ -183,7 +184,7 @@ export async function startMigration(opts) {
|
|
|
183
184
|
message:
|
|
184
185
|
`Migration started. run-id=${runId}, pid=${pid}\n` +
|
|
185
186
|
`Logs: ${P.logFile(runId)}\n` +
|
|
186
|
-
`Status:
|
|
187
|
+
`Status: ${selfInvocation()} --migrate-status`,
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
|
|
@@ -268,19 +269,27 @@ export function readStatus(opts = {}) {
|
|
|
268
269
|
if (!prog) {
|
|
269
270
|
return { ok: false, runId, progress: null, message: `No progress file for run-id=${runId}.` };
|
|
270
271
|
}
|
|
272
|
+
// Gray annotations next to each field. Keeps the output self-documenting
|
|
273
|
+
// without forcing the reader to cross-reference README docs for common
|
|
274
|
+
// fields. Respects NO_COLOR.
|
|
275
|
+
const useColor = !process.env.NO_COLOR;
|
|
276
|
+
const GRAY = useColor ? "\x1b[90m" : "";
|
|
277
|
+
const RESET = useColor ? "\x1b[0m" : "";
|
|
278
|
+
const note = (s) => (s ? ` ${GRAY}${s}${RESET}` : "");
|
|
279
|
+
|
|
271
280
|
const lines = [
|
|
272
281
|
`run-id: ${prog.runId}`,
|
|
273
|
-
`status: ${prog.status}`,
|
|
282
|
+
`status: ${prog.status}${note("(pending → running → done | failed | stopped)")}`,
|
|
274
283
|
`scope: ${prog.scope}`,
|
|
275
284
|
`started: ${prog.startedAt}`,
|
|
276
285
|
`finished: ${prog.finishedAt ?? "(in progress)"}`,
|
|
277
|
-
`current: ${prog.currentAgent ?? "-"} / ${prog.currentSession ?? "-"}`,
|
|
278
|
-
`sessions: ${prog.counts.sessionsDone} / ${prog.counts.sessionsPlanned}`,
|
|
286
|
+
`current: ${prog.currentAgent ?? "-"} / ${prog.currentSession ?? "-"}${note("(<adapter>:<source-session> / <message-id>)")}`,
|
|
287
|
+
`sessions: ${prog.counts.sessionsDone} / ${prog.counts.sessionsPlanned}${note("(processed / discovered)")}`,
|
|
279
288
|
`messages: ${prog.counts.messagesSent}`,
|
|
280
|
-
`memory pairs: ${prog.counts.memoryPairsSent}`,
|
|
281
|
-
`memory (MD): ${prog.counts.memoriesFromMD}`,
|
|
282
|
-
`warnings: ${prog.counts.warnings}`,
|
|
283
|
-
`memory errs: ${prog.counts.memoryErrors}`,
|
|
289
|
+
`memory pairs: ${prog.counts.memoryPairsSent}${note("(memory_add from Mem0 extractor)")}`,
|
|
290
|
+
`memory (MD): ${prog.counts.memoriesFromMD}${note("(memory_add from CLAUDE.md / memory files)")}`,
|
|
291
|
+
`warnings: ${prog.counts.warnings}${note("(non-fatal; run continues)")}`,
|
|
292
|
+
`memory errs: ${prog.counts.memoryErrors}${note("(failed memory_add calls; re-run with --force to retry)")}`,
|
|
284
293
|
];
|
|
285
294
|
return { ok: true, runId, progress: prog, message: lines.join("\n") };
|
|
286
295
|
}
|
package/lib/prompt.mjs
CHANGED
|
@@ -71,6 +71,67 @@ export async function askChoice(questionText, options) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// askMultiChoice
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Display a numbered list and let the user pick multiple items.
|
|
80
|
+
*
|
|
81
|
+
* Input formats accepted (all case-insensitive):
|
|
82
|
+
* - `1,3,5` comma-separated indices
|
|
83
|
+
* - `1-3` range, or mixed `1,3-5`
|
|
84
|
+
* - `all` selects every option, `none` selects none
|
|
85
|
+
* - empty input accepts the default set shown in the prompt
|
|
86
|
+
*
|
|
87
|
+
* @param {string} questionText
|
|
88
|
+
* @param {{ label: string, description?: string }[]} options
|
|
89
|
+
* @param {{ defaults?: string[] }} [opts] - Labels to default-on; shown with
|
|
90
|
+
* `[x]` in the list and returned when the user presses Enter.
|
|
91
|
+
* @returns {Promise<string[]>} Array of selected option `label`s.
|
|
92
|
+
*/
|
|
93
|
+
export async function askMultiChoice(questionText, options, opts = {}) {
|
|
94
|
+
const defaults = new Set(opts.defaults ?? []);
|
|
95
|
+
console.log(`\n ${questionText}`);
|
|
96
|
+
for (let i = 0; i < options.length; i++) {
|
|
97
|
+
const o = options[i];
|
|
98
|
+
const mark = defaults.has(o.label) ? "[x]" : "[ ]";
|
|
99
|
+
const desc = o.description ? ` - ${o.description}` : "";
|
|
100
|
+
console.log(` ${mark} ${i + 1}) ${o.label}${desc}`);
|
|
101
|
+
}
|
|
102
|
+
console.log(` (comma list like "1,3" or "1-3" / "all" / "none" / empty = keep defaults)`);
|
|
103
|
+
|
|
104
|
+
while (true) {
|
|
105
|
+
const raw = (await question(` Choice: `)).trim().toLowerCase();
|
|
106
|
+
if (!raw) {
|
|
107
|
+
return options.filter((o) => defaults.has(o.label)).map((o) => o.label);
|
|
108
|
+
}
|
|
109
|
+
if (raw === "all") return options.map((o) => o.label);
|
|
110
|
+
if (raw === "none") return [];
|
|
111
|
+
|
|
112
|
+
const picked = new Set();
|
|
113
|
+
let bad = false;
|
|
114
|
+
for (const token of raw.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
115
|
+
const rangeMatch = token.match(/^(\d+)-(\d+)$/);
|
|
116
|
+
if (rangeMatch) {
|
|
117
|
+
const lo = parseInt(rangeMatch[1], 10);
|
|
118
|
+
const hi = parseInt(rangeMatch[2], 10);
|
|
119
|
+
if (lo < 1 || hi > options.length || lo > hi) { bad = true; break; }
|
|
120
|
+
for (let i = lo; i <= hi; i++) picked.add(i);
|
|
121
|
+
} else {
|
|
122
|
+
const n = parseInt(token, 10);
|
|
123
|
+
if (!Number.isInteger(n) || n < 1 || n > options.length) { bad = true; break; }
|
|
124
|
+
picked.add(n);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (bad || picked.size === 0) {
|
|
128
|
+
console.log(` Please enter numbers between 1 and ${options.length}, "all", or "none".`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
return [...picked].sort((a, b) => a - b).map((i) => options[i - 1].label);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
74
135
|
// ---------------------------------------------------------------------------
|
|
75
136
|
// askText
|
|
76
137
|
// ---------------------------------------------------------------------------
|
|
@@ -103,6 +164,18 @@ export async function askText(questionText, defaultValue) {
|
|
|
103
164
|
* @returns {Promise<string>}
|
|
104
165
|
*/
|
|
105
166
|
export async function askSecret(questionText) {
|
|
167
|
+
// Fully close the singleton readline BEFORE printing the prompt or
|
|
168
|
+
// entering raw mode. pause() alone was insufficient — readline left
|
|
169
|
+
// stdin in a state where the terminal echo would still leak keystrokes
|
|
170
|
+
// before our raw-mode handler wrote its `*` mask. close() forcefully
|
|
171
|
+
// releases stdin back to the OS, which lets setRawMode actually
|
|
172
|
+
// suppress echo. getRL() will recreate the interface on the next
|
|
173
|
+
// askText/askYesNo call, so downstream prompts still work.
|
|
174
|
+
if (rl) {
|
|
175
|
+
rl.close();
|
|
176
|
+
rl = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
106
179
|
// Print the prompt without a trailing newline.
|
|
107
180
|
process.stdout.write(` ${questionText} `);
|
|
108
181
|
|
|
@@ -131,6 +204,7 @@ export async function askSecret(questionText) {
|
|
|
131
204
|
function cleanup() {
|
|
132
205
|
stdin.setRawMode(wasRaw);
|
|
133
206
|
stdin.removeListener("data", onData);
|
|
207
|
+
stdin.pause();
|
|
134
208
|
process.stdout.write("\n");
|
|
135
209
|
}
|
|
136
210
|
|