@yugabytedb/meko-mcp 1.0.4 → 1.0.5

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 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
- const { clients, invalid } = parseClients(flags.client);
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,64 @@ 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();
372
+ banner('Meko Setup');
301
373
 
302
- // Step 1 — connection type
303
- const connectionType = await askChoice('Connection type:', [
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
+ const connectionOptions = [
304
378
  { label: 'Cloud Meko', description: 'Hosted at mekodev.com (requires URL + API key)' },
305
- { label: 'Local Meko', description: 'Local docker stack at localhost:8000' },
306
- ]);
379
+ ];
380
+ if (!isPackagedInstall()) {
381
+ connectionOptions.push({
382
+ label: 'Local Meko',
383
+ description: 'Local stack at localhost:8000',
384
+ });
385
+ }
386
+ const connectionType = await askChoice('Connection type:', connectionOptions);
307
387
 
308
388
  if (connectionType === 'Cloud Meko') {
309
389
  // Step 2 — MCP URL
@@ -393,7 +473,7 @@ async function main() {
393
473
  });
394
474
  } else if (flags.yes) {
395
475
  console.log('');
396
- dim('Tip: run `create-meko-setup --migrate` to import existing conversations and memories.');
476
+ dim(`Tip: run \`${selfInvocation()} --migrate\` to import existing conversations and memories.`);
397
477
  } else if (process.stdin.isTTY) {
398
478
  await maybeOfferMigration({
399
479
  url,
@@ -422,7 +502,7 @@ async function runUnattendedMigration({ url, apiKey, scope, projectRoot }) {
422
502
  console.log(out.message);
423
503
  if (!out.ok) {
424
504
  // Don't fail the install; the user can retry with --migrate.
425
- dim('Migration did not start cleanly. Install succeeded; try `create-meko-setup --migrate` later.');
505
+ dim(`Migration did not start cleanly. Install succeeded; try \`${selfInvocation()} --migrate\` later.`);
426
506
  }
427
507
  } catch (err) {
428
508
  dim(`Migration could not be started: ${err.message ?? err}`);
@@ -468,7 +548,7 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot }) {
468
548
  true,
469
549
  );
470
550
  if (!proceed) {
471
- dim('Skipped. Run `create-meko-setup --migrate` later when you\'re ready.');
551
+ dim(`Skipped. Run \`${selfInvocation()} --migrate\` later when you're ready.`);
472
552
  return;
473
553
  }
474
554
  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(`Non-critical: ${msg}`);
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(`Non-critical: ${msg}`);
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];
@@ -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
@@ -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
- step(currentStep, totalSteps, "Registering MCP server... skipped (existing config kept).");
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(" Would validate config, MCP connectivity, and tool availability.");
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(` ${check.label}: ${check.message}`);
194
- else warn(` ${check.label}: ${check.message}`);
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: [1/5] Registering MCP server...
58
+ * Example: [1/5] Registering MCP server...
59
59
  */
60
60
  export function step(n, total, msg) {
61
- console.log(` ${BLUE}[${n}/${total}]${RESET} ${msg}`);
61
+ console.log(`${BLUE}[${n}/${total}]${RESET} ${msg}`);
62
62
  }
63
63
 
64
64
  /**
65
65
  * Print a numbered step that succeeded.
66
- * Example: [1/5] Registering MCP server... ✓
66
+ * Example: ✓ [1/5] MCP server registered.
67
67
  */
68
68
  export function stepOk(n, total, msg) {
69
- console.log(` ${GREEN}${CHECK}${RESET} ${BLUE}[${n}/${total}]${RESET} ${msg}`);
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: [3/5] Registering plugin marketplace... ✗ Failed: ...
74
+ * Example: ✗ [3/5] Registering plugin marketplace failed: ...
75
75
  */
76
76
  export function stepFail(n, total, msg) {
77
- console.log(` ${RED}${CROSS}${RESET} ${BLUE}[${n}/${total}]${RESET} ${msg}`);
77
+ console.log(`${RED}${CROSS}${RESET} ${BLUE}[${n}/${total}]${RESET} ${msg}`);
78
78
  }
79
79
 
80
80
  /**
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: create-meko-setup --migrate-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
  // ---------------------------------------------------------------------------
@@ -106,6 +167,13 @@ export async function askSecret(questionText) {
106
167
  // Print the prompt without a trailing newline.
107
168
  process.stdout.write(` ${questionText} `);
108
169
 
170
+ // Pause the singleton readline so its data listener doesn't echo each
171
+ // keystroke in parallel with our manual "*" write. Without this, users
172
+ // see `d*j*f*k*...` (raw char + mask interleaved) and backspace fights
173
+ // with readline over cursor position.
174
+ const pausedRL = rl;
175
+ if (pausedRL) pausedRL.pause();
176
+
109
177
  return new Promise((resolve, reject) => {
110
178
  let secret = "";
111
179
  const stdin = process.stdin;
@@ -117,6 +185,7 @@ export async function askSecret(questionText) {
117
185
  const fallbackRL = createInterface({ input: stdin, output: process.stdout });
118
186
  fallbackRL.question("", (answer) => {
119
187
  fallbackRL.close();
188
+ if (pausedRL) pausedRL.resume();
120
189
  resolve(answer.trim());
121
190
  });
122
191
  return;
@@ -132,6 +201,7 @@ export async function askSecret(questionText) {
132
201
  stdin.setRawMode(wasRaw);
133
202
  stdin.removeListener("data", onData);
134
203
  process.stdout.write("\n");
204
+ if (pausedRL) pausedRL.resume();
135
205
  }
136
206
 
137
207
  function onData(ch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yugabytedb/meko-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Single-line installer for Meko MCP Server across Claude and Cursor clients",
5
5
  "type": "module",
6
6
  "bin": {