@yugabytedb/meko-mcp 2.2.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +168 -61
  2. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +2134 -0
  3. package/assets/hooks-handlers/lib/capture-notices.js +224 -14
  4. package/assets/hooks-handlers/lib/capture-state.js +206 -1
  5. package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
  6. package/assets/hooks-handlers/lib/capture.js +1364 -139
  7. package/assets/hooks-handlers/lib/checkpoint-timer.js +445 -18
  8. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  9. package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
  10. package/assets/hooks-handlers/lib/opencode-handler.js +643 -0
  11. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  12. package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
  13. package/assets/opencode-plugin/meko.js +213 -0
  14. package/assets/scripts/meko-statusline.sh +102 -19
  15. package/assets/skills/meko-mcp-tools/SKILL.md +59 -16
  16. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
  17. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
  18. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +275 -19
  19. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +7 -4
  20. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +39 -11
  21. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +10 -6
  22. package/assets/skills/meko-mcp-tools/references/tools-overview.md +44 -29
  23. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +79 -14
  24. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +47 -1
  25. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
  26. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +57 -21
  27. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
  28. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +8 -6
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +284 -38
  31. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +32 -10
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +16 -12
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +46 -31
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +79 -14
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +69 -1
  36. package/assets/skills/meko-select-datapack-desktop/SKILL.md +93 -14
  37. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +44 -12
  38. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +14 -10
  39. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +17 -5
  40. package/bin/create-meko-setup.mjs +186 -30
  41. package/lib/autofix.mjs +100 -0
  42. package/lib/browser-login.mjs +495 -0
  43. package/lib/capture-cleanup.mjs +63 -0
  44. package/lib/clients/claude-code.mjs +1 -0
  45. package/lib/clients/codex.mjs +80 -35
  46. package/lib/clients/common.mjs +7 -0
  47. package/lib/clients/index.mjs +2 -0
  48. package/lib/clients/kiro.mjs +5 -5
  49. package/lib/clients/opencode.mjs +537 -0
  50. package/lib/config.mjs +68 -1
  51. package/lib/installer.mjs +18 -2
  52. package/lib/migrate/adapters/claude-code.mjs +11 -2
  53. package/lib/migrate/adapters/codex.mjs +9 -2
  54. package/lib/migrate/adapters/memory-md.mjs +27 -8
  55. package/lib/migrate/adapters/opencode.mjs +148 -0
  56. package/lib/migrate/claude-parser.mjs +153 -9
  57. package/lib/migrate/datapack.mjs +254 -16
  58. package/lib/migrate/id.mjs +195 -13
  59. package/lib/migrate/mcp-client.mjs +59 -1
  60. package/lib/migrate/opencode-normalizer.mjs +132 -0
  61. package/lib/migrate/orchestrator.mjs +294 -78
  62. package/lib/migrate/progress.mjs +239 -5
  63. package/lib/migrate/registry.mjs +72 -0
  64. package/lib/migrate.mjs +97 -9
  65. package/lib/validate.mjs +111 -22
  66. package/package.json +5 -5
  67. package/scripts/bundle-assets.mjs +4 -0
@@ -5,22 +5,32 @@
5
5
  * CommonJS, zero npm deps. Safe to require from capture.js SessionStart
6
6
  * without touching the drain loop. See docs/plans/meko-capture-v2-contracts.md.
7
7
  *
8
- * Health cache field extension (documented):
8
+ * Health cache field extensions (documented):
9
9
  * capture-health.json may include `last_notified_status` — the aggregate
10
10
  * status last shown to the user via SessionStart. Used for one-shot
11
11
  * recovery notices when status returns to `healthy`. writeHealthCache()
12
12
  * preserves this field when callers omit it.
13
+ *
14
+ * `last_notified_dropped` records the aggregate dropped-exchange count last
15
+ * shown. A complete refresh may clamp it when session watermarks disappear.
13
16
  */
14
17
 
15
18
  "use strict";
16
19
 
17
20
  const {
21
+ autofixEnabled,
22
+ autofixIntervalSeconds,
18
23
  computeAggregateHealth,
19
24
  readHealthCache,
25
+ scanCompleteForDropClamp,
20
26
  scanStates,
21
27
  writeHealthCache,
22
28
  } = require("./capture-state");
23
29
 
30
+ // blocked_reason prefix written by `--capture-cleanup stop`
31
+ // (capture-cleanup-cli.js RESUME_HINT).
32
+ const PARKED_REASON_PREFIX = "parked by capture cleanup";
33
+
24
34
  const NON_HEALTHY = new Set([
25
35
  "catching_up",
26
36
  "degraded_retrying",
@@ -99,14 +109,160 @@ function statuslineSuffixFromHealth(health) {
99
109
  return `capture:${short}(${queued})`;
100
110
  }
101
111
 
112
+ const NEEDS_YOU_LABELS = {
113
+ auth: "API key",
114
+ account_limit: "plan limit",
115
+ datapack: "datapack access",
116
+ forbidden: "access",
117
+ };
118
+
119
+ /**
120
+ * Claude Code status bar text for capture, or "" when there is nothing to
121
+ * show. Built when the health cache is written and read by
122
+ * meko-statusline.sh, which the npm installer copies and wires into Claude
123
+ * Code's statusLine; the plugin ships no status-line reader, so a plugin-only
124
+ * install shows nothing. `autofix` is the last `--capture-cleanup fix` summary
125
+ * (autofix-last.json). Uses a clock time for the next automatic fix, so the
126
+ * text stays right between cache writes.
127
+ *
128
+ * @param {object} health
129
+ * @param {object|null} autofix
130
+ * @param {number} [nowMs]
131
+ * @returns {string}
132
+ */
133
+ function statuslineText(health, autofix, nowMs) {
134
+ return statuslineFields(health, autofix, nowMs).statusline;
135
+ }
136
+
137
+ /**
138
+ * statuslineText plus when it stops being true: `statusline_until` (epoch
139
+ * seconds) and the text to show after that, `statusline_after`. The
140
+ * status bar compares them with the clock, so "auto-fixed N" disappears after
141
+ * an hour and "auto-fix at 14:30" turns into "auto-fix soon" at 14:30, even
142
+ * when nothing rewrites the cache.
143
+ */
144
+ function statuslineFields(health, autofix, nowMs) {
145
+ const until = (ms, after) => ({ statusline_until: Math.floor(ms / 1000), statusline_after: after });
146
+ const h = health && typeof health === "object" ? health : {};
147
+ const status = h.status || "healthy";
148
+ const now = nowMs != null ? nowMs : Date.now();
149
+ const a = autofix && typeof autofix === "object" ? autofix : null;
150
+ const sessions = Array.isArray(h.sessions) ? h.sessions : [];
151
+ const queued = Number(h.queued_exchanges || 0);
152
+ const stopped = sessions.filter((x) => x && x.parked_reason === "stop");
153
+ const parkedByFix = sessions.filter((x) => x && x.parked_reason && x.parked_reason !== "stop");
154
+ const stuck = sessions.filter((x) => x && x.status !== "healthy" && !x.parked_reason);
155
+
156
+ // A run that never finished (its timer was killed) leaves a placeholder;
157
+ // once it is older than any run can take, its needs_you list is stale.
158
+ const ranAtMs = a ? Date.parse(a.ran_at) : NaN;
159
+ const abandoned = Boolean(a && a.running && (!Number.isFinite(ranAtMs) || now - ranAtMs > 30 * 60 * 1000));
160
+ const needs = a && !abandoned && Array.isArray(a.needs_you) ? a.needs_you : [];
161
+ if (needs.length && (parkedByFix.length || stuck.length)) {
162
+ const labels = [...new Set(needs.map((n) => NEEDS_YOU_LABELS[n.cause] || n.cause))];
163
+ return { statusline: `capture: needs you (${labels.join(", ")})` };
164
+ }
165
+
166
+ if (status === "healthy") {
167
+ const fixed = a && a.counts ? Number(a.counts.fixed || 0) : 0;
168
+ const ranAt = a ? Date.parse(a.ran_at) : NaN;
169
+ if (a && a.mode === "auto" && fixed > 0 && Number.isFinite(ranAt) && now - ranAt < 3600 * 1000) {
170
+ return { statusline: `capture: auto-fixed ${fixed}`, ...until(ranAt + 3600 * 1000, "") };
171
+ }
172
+ return { statusline: "" };
173
+ }
174
+
175
+ const q = `${queued} queued`;
176
+ if (!stuck.length && !parkedByFix.length && stopped.length) {
177
+ return { statusline: `capture: paused (${q})` };
178
+ }
179
+
180
+ let next = "";
181
+ let due = null;
182
+ if (autofixEnabled()) {
183
+ const ranAt = a ? Date.parse(a.ran_at) : NaN;
184
+ due = (Number.isFinite(ranAt) ? ranAt : now) + autofixIntervalSeconds() * 1000;
185
+ next =
186
+ due <= now
187
+ ? ", auto-fix soon"
188
+ : `, auto-fix at ${new Date(due).toTimeString().slice(0, 5)}`;
189
+ }
190
+ const label =
191
+ status === "degraded_retrying" ? "retrying" : status === "blocked_action_required" ? "blocked" : null;
192
+ if (!label) return { statusline: `capture: sending ${queued}` };
193
+ const text = `capture: ${label} (${q})${next}`;
194
+ if (due != null && due > now) {
195
+ return { statusline: text, ...until(due, `capture: ${label} (${q}), auto-fix soon`) };
196
+ }
197
+ return { statusline: text };
198
+ }
199
+
200
+ /**
201
+ * Sentence describing exchanges that were skipped because the server will
202
+ * never accept them, or "" when there are none new to report.
203
+ *
204
+ * Drops are permanent, so they are not a health status — capture is running
205
+ * fine — but the user is owed the fact that specific turns are missing.
206
+ */
207
+ function dropSentence(dropped, previouslyNotified, health) {
208
+ const newDrops = dropped - previouslyNotified;
209
+ if (newDrops <= 0) return "";
210
+ const reasons = Array.isArray(health && health.drop_reasons)
211
+ ? health.drop_reasons.filter(Boolean)
212
+ : [];
213
+ // drop_reasons is the union of every session's last_drop_reason, including
214
+ // drops already announced. Attribute a reason only when there is one shared
215
+ // cause; otherwise point at --capture-status instead of mis-blaming.
216
+ const because =
217
+ reasons.length === 1
218
+ ? ` Reason: ${reasons[0]}.`
219
+ : reasons.length > 1
220
+ ? " Run `meko-mcp --capture-status` for drop reasons."
221
+ : "";
222
+ return (
223
+ `[Meko capture] ${newDrops} exchange(s) could not be stored and were ` +
224
+ `skipped; the rest of the session is captured normally.${because} ` +
225
+ `Tell the user those turns are missing from Meko.`
226
+ );
227
+ }
228
+
229
+ /**
230
+ * True when some session is still retrying. A session parked by
231
+ * `--capture-cleanup stop` is blocked on purpose and never retries, so it
232
+ * doesn't count; its blocked reason already says how to resume it.
233
+ */
234
+ function retriesRunning(health) {
235
+ // A degraded session with nothing left to send (leftover failure fields on
236
+ // a delivered session) isn't retrying; `repair` clears it, not `stop`.
237
+ const sessions = Array.isArray(health && health.sessions) ? health.sessions : [];
238
+ const retrying = sessions.some(
239
+ (s) =>
240
+ s &&
241
+ s.status === "degraded_retrying" &&
242
+ (Number(s.queued_exchanges || 0) > 0 ||
243
+ ["needs_conversation", "pending", "draining", "retry_wait"].includes(s.delivery)),
244
+ );
245
+ if (retrying) return true;
246
+ if (health.status !== "blocked_action_required") return false;
247
+ const reasons = Array.isArray(health.blocked_reasons)
248
+ ? health.blocked_reasons.filter(Boolean)
249
+ : [];
250
+ return (
251
+ reasons.length === 0 ||
252
+ reasons.some((r) => !String(r).startsWith(PARKED_REASON_PREFIX))
253
+ );
254
+ }
255
+
102
256
  /**
103
257
  * Build a SessionStart-injectable health notice.
104
258
  *
105
259
  * - When status ≠ healthy: always emit (exact affected-session + queued counts + reason).
106
260
  * - When status === healthy and previousStatus was non-healthy: one-shot recovery notice.
107
261
  * - When status === healthy and previous was already healthy / null: no notice.
262
+ * - Any new dropped exchanges are appended in every case, and are enough on
263
+ * their own to produce a notice while status is healthy.
108
264
  *
109
- * @param {{ health: object, previousStatus?: string|null }} opts
265
+ * @param {{ health: object, previousStatus?: string|null, previousDropped?: number }} opts
110
266
  * @returns {{
111
267
  * notice: string|null,
112
268
  * nextNotifiedStatus: string,
@@ -114,10 +270,11 @@ function statuslineSuffixFromHealth(health) {
114
270
  * status: string,
115
271
  * affectedSessions: number,
116
272
  * queuedExchanges: number,
273
+ * droppedExchanges: number,
117
274
  * reason: string,
118
275
  * }}
119
276
  */
120
- function buildHealthNotice({ health, previousStatus } = {}) {
277
+ function buildHealthNotice({ health, previousStatus, previousDropped } = {}) {
121
278
  const h = health && typeof health === "object" ? health : { status: "healthy" };
122
279
  const status = h.status || "healthy";
123
280
  const prev =
@@ -126,29 +283,37 @@ function buildHealthNotice({ health, previousStatus } = {}) {
126
283
  : String(previousStatus);
127
284
  const affected = affectedSessionCount(h);
128
285
  const queued = Number(h.queued_exchanges || 0);
286
+ const dropped = Number(h.dropped_exchanges || 0);
287
+ const notifiedDropped = Number(previousDropped || 0);
288
+ const drops = dropSentence(dropped, notifiedDropped, h);
129
289
  const reason = reasonForHealth(h);
290
+ const join = (a, b) => (a && b ? `${a}\n\n${b}` : a || b || null);
130
291
 
131
292
  if (status === "healthy") {
132
293
  if (prev && NON_HEALTHY.has(prev)) {
133
294
  return {
134
- notice:
295
+ notice: join(
135
296
  "[Meko capture] Capture recovered — all sessions healthy. " +
136
- "Queued backlog is clear. Automatic conversation capture is running normally again.",
297
+ "Queued backlog is clear. Automatic conversation capture is running normally again.",
298
+ drops,
299
+ ),
137
300
  nextNotifiedStatus: "healthy",
138
301
  shouldPersistNotified: true,
139
302
  status,
140
303
  affectedSessions: 0,
141
304
  queuedExchanges: queued,
305
+ droppedExchanges: dropped,
142
306
  reason,
143
307
  };
144
308
  }
145
309
  return {
146
- notice: null,
310
+ notice: drops || null,
147
311
  nextNotifiedStatus: "healthy",
148
- shouldPersistNotified: prev !== "healthy",
312
+ shouldPersistNotified: prev !== "healthy" || Boolean(drops),
149
313
  status,
150
314
  affectedSessions: 0,
151
315
  queuedExchanges: queued,
316
+ droppedExchanges: dropped,
152
317
  reason,
153
318
  };
154
319
  }
@@ -160,10 +325,18 @@ function buildHealthNotice({ health, previousStatus } = {}) {
160
325
  ? "degraded_retrying"
161
326
  : "catching_up";
162
327
 
163
- const notice =
328
+ const cleanupHint = retriesRunning(h)
329
+ ? " To repair it, offer `npx @yugabytedb/meko-mcp@latest " +
330
+ "--capture-cleanup fix`. To only stop the retries, offer " +
331
+ "`npx @yugabytedb/meko-mcp@latest --capture-cleanup stop`; it parks " +
332
+ "the backlog without deleting it."
333
+ : "";
334
+ const notice = join(
164
335
  `[Meko capture] Status: ${label} — ${affected} session(s) affected, ` +
165
- `${queued} exchange(s) queued. Reason: ${reason}. ` +
166
- `Run \`meko-mcp --capture-status\` for details.`;
336
+ `${queued} exchange(s) queued. Reason: ${reason}. ` +
337
+ `Run \`meko-mcp --capture-status\` for details.${cleanupHint}`,
338
+ drops,
339
+ );
167
340
 
168
341
  return {
169
342
  notice,
@@ -172,6 +345,7 @@ function buildHealthNotice({ health, previousStatus } = {}) {
172
345
  status,
173
346
  affectedSessions: affected,
174
347
  queuedExchanges: queued,
348
+ droppedExchanges: dropped,
175
349
  reason,
176
350
  };
177
351
  }
@@ -195,14 +369,23 @@ function injectHealthNotice(context, opts) {
195
369
  const base = context == null ? "" : String(context);
196
370
 
197
371
  let health = null;
372
+ let scanComplete = false;
198
373
  if (refresh) {
199
374
  const scan = scanStates({ includeBlocked: true });
375
+ scanComplete = scanCompleteForDropClamp(scan);
200
376
  health = computeAggregateHealth(scan.entries || []);
201
377
  const existing = readHealthCache();
202
378
  if (existing && existing.last_notified_status !== undefined) {
203
379
  health.last_notified_status = existing.last_notified_status;
204
380
  }
205
- writeHealthCache(health);
381
+ if (existing && existing.last_notified_dropped !== undefined) {
382
+ const previous = Number(existing.last_notified_dropped || 0);
383
+ health.last_notified_dropped = scanComplete
384
+ ? Math.min(previous, Number(health.dropped_exchanges || 0))
385
+ : previous;
386
+ }
387
+ const written = writeHealthCache(health, { clampDropped: scanComplete });
388
+ if (written.ok) health = written.health;
206
389
  } else {
207
390
  health = readHealthCache() || {
208
391
  schema_version: 1,
@@ -214,9 +397,11 @@ function injectHealthNotice(context, opts) {
214
397
  blocked_action_required: 0,
215
398
  },
216
399
  queued_exchanges: 0,
400
+ dropped_exchanges: 0,
217
401
  oldest_pending_age_seconds: null,
218
402
  last_success_at: null,
219
403
  blocked_reasons: [],
404
+ drop_reasons: [],
220
405
  sessions: [],
221
406
  };
222
407
  }
@@ -225,13 +410,23 @@ function injectHealthNotice(context, opts) {
225
410
  health.last_notified_status !== undefined
226
411
  ? health.last_notified_status
227
412
  : null;
228
- const built = buildHealthNotice({ health, previousStatus });
413
+ const previousDropped = Number(health.last_notified_dropped || 0);
414
+ const built = buildHealthNotice({ health, previousStatus, previousDropped });
229
415
 
230
416
  if (persist && built.shouldPersistNotified) {
231
- writeHealthCache({
417
+ const nextHealth = {
232
418
  ...health,
233
419
  last_notified_status: built.nextNotifiedStatus,
234
- });
420
+ };
421
+ if (scanComplete || !refresh) {
422
+ nextHealth.last_notified_dropped = built.droppedExchanges;
423
+ } else {
424
+ nextHealth.last_notified_dropped = Math.max(
425
+ previousDropped,
426
+ built.droppedExchanges,
427
+ );
428
+ }
429
+ writeHealthCache(nextHealth, { clampDropped: scanComplete });
235
430
  }
236
431
 
237
432
  if (!built.notice) return base;
@@ -255,6 +450,7 @@ function formatCaptureStatusText(health) {
255
450
  `degraded_retrying=${Number(counts.degraded_retrying || 0)} ` +
256
451
  `blocked_action_required=${Number(counts.blocked_action_required || 0)}`,
257
452
  ` queued exchanges: ${Number(h.queued_exchanges || 0)}`,
453
+ ` dropped exchanges: ${Number(h.dropped_exchanges || 0)}`,
258
454
  ` oldest pending age (seconds): ${
259
455
  h.oldest_pending_age_seconds == null ? "n/a" : h.oldest_pending_age_seconds
260
456
  }`,
@@ -265,6 +461,15 @@ function formatCaptureStatusText(health) {
265
461
  if (blocked.length) {
266
462
  lines.push(` blocked reasons: ${blocked.join("; ")}`);
267
463
  }
464
+ const dropReasons = Array.isArray(h.drop_reasons) ? h.drop_reasons : [];
465
+ if (dropReasons.length) {
466
+ lines.push(` drop reasons: ${dropReasons.join("; ")}`);
467
+ }
468
+ if (status === "degraded_retrying" || status === "blocked_action_required") {
469
+ lines.push(
470
+ " per-session detail and cleanup: meko-mcp --capture-cleanup status",
471
+ );
472
+ }
268
473
  return lines.join("\n");
269
474
  }
270
475
 
@@ -300,18 +505,23 @@ function captureStatusJson(health) {
300
505
  ),
301
506
  },
302
507
  queued_exchanges: Number(h.queued_exchanges || 0),
508
+ dropped_exchanges: Number(h.dropped_exchanges || 0),
303
509
  oldest_pending_age_seconds:
304
510
  h.oldest_pending_age_seconds == null ? null : h.oldest_pending_age_seconds,
305
511
  last_success_at: h.last_success_at || null,
306
512
  blocked_reasons: Array.isArray(h.blocked_reasons) ? h.blocked_reasons : [],
513
+ drop_reasons: Array.isArray(h.drop_reasons) ? h.drop_reasons : [],
307
514
  sessions: Array.isArray(h.sessions) ? h.sessions : [],
308
515
  };
309
516
  }
310
517
 
311
518
  module.exports = {
519
+ PARKED_REASON_PREFIX,
312
520
  affectedSessionCount,
313
521
  reasonForHealth,
314
522
  statuslineSuffixFromHealth,
523
+ statuslineText,
524
+ statuslineFields,
315
525
  buildHealthNotice,
316
526
  injectHealthNotice,
317
527
  formatCaptureStatusText,