@yugabytedb/meko-mcp 2.1.0 → 2.3.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 (70) hide show
  1. package/README.md +233 -40
  2. package/assets/hooks-handlers/codex-session-end.sh +4 -69
  3. package/assets/hooks-handlers/cursor-session-start.sh +19 -1
  4. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
  5. package/assets/hooks-handlers/lib/capture-notices.js +438 -0
  6. package/assets/hooks-handlers/lib/capture-state.js +2173 -0
  7. package/assets/hooks-handlers/lib/capture-status-cli.js +86 -0
  8. package/assets/hooks-handlers/lib/capture.js +2782 -403
  9. package/assets/hooks-handlers/lib/checkpoint-timer.js +485 -81
  10. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  11. package/assets/hooks-handlers/lib/kiro-handler.js +760 -0
  12. package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
  13. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  14. package/assets/hooks-handlers/lib/terminal-handoff.js +427 -0
  15. package/assets/hooks-handlers/session-end.sh +3 -34
  16. package/assets/opencode-plugin/meko.js +213 -0
  17. package/assets/scripts/meko-statusline.sh +123 -16
  18. package/assets/skills/meko-mcp-tools/SKILL.md +116 -126
  19. package/assets/skills/meko-mcp-tools/references/_sections.md +3 -3
  20. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +25 -18
  21. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +99 -24
  22. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
  23. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +53 -6
  24. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +25 -79
  25. package/assets/skills/meko-mcp-tools/references/tools-overview.md +37 -26
  26. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
  27. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +66 -1
  28. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +7 -40
  29. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +56 -20
  30. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
  31. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +15 -8
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +99 -36
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +18 -14
  36. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +39 -28
  37. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
  38. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
  39. package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
  40. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
  41. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
  42. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
  43. package/bin/create-meko-setup.mjs +113 -12
  44. package/lib/capture-cleanup.mjs +63 -0
  45. package/lib/capture-status.mjs +67 -0
  46. package/lib/clients/aliases.mjs +5 -0
  47. package/lib/clients/claude-code.mjs +1 -0
  48. package/lib/clients/claude-desktop.mjs +120 -5
  49. package/lib/clients/codex.mjs +85 -37
  50. package/lib/clients/common.mjs +7 -0
  51. package/lib/clients/index.mjs +4 -0
  52. package/lib/clients/kiro.mjs +705 -0
  53. package/lib/clients/opencode.mjs +532 -0
  54. package/lib/config.mjs +158 -1
  55. package/lib/installer.mjs +18 -1
  56. package/lib/migrate/adapters/claude-code.mjs +11 -2
  57. package/lib/migrate/adapters/codex.mjs +497 -0
  58. package/lib/migrate/adapters/memory-md.mjs +27 -8
  59. package/lib/migrate/adapters/opencode.mjs +148 -0
  60. package/lib/migrate/claude-parser.mjs +153 -9
  61. package/lib/migrate/id.mjs +217 -10
  62. package/lib/migrate/mcp-client.mjs +25 -1
  63. package/lib/migrate/opencode-normalizer.mjs +132 -0
  64. package/lib/migrate/orchestrator.mjs +299 -56
  65. package/lib/migrate/progress.mjs +239 -5
  66. package/lib/migrate/registry.mjs +72 -0
  67. package/lib/migrate.mjs +100 -10
  68. package/lib/prompt.mjs +16 -1
  69. package/package.json +4 -4
  70. package/scripts/bundle-assets.mjs +4 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * capture-notices.js — SessionStart / statusline health notice builders
3
+ * for Meko Capture V2 (Workstream D).
4
+ *
5
+ * CommonJS, zero npm deps. Safe to require from capture.js SessionStart
6
+ * without touching the drain loop. See docs/plans/meko-capture-v2-contracts.md.
7
+ *
8
+ * Health cache field extensions (documented):
9
+ * capture-health.json may include `last_notified_status` — the aggregate
10
+ * status last shown to the user via SessionStart. Used for one-shot
11
+ * recovery notices when status returns to `healthy`. writeHealthCache()
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.
16
+ */
17
+
18
+ "use strict";
19
+
20
+ const {
21
+ computeAggregateHealth,
22
+ readHealthCache,
23
+ scanCompleteForDropClamp,
24
+ scanStates,
25
+ writeHealthCache,
26
+ } = require("./capture-state");
27
+
28
+ // blocked_reason prefix written by `--capture-cleanup stop`
29
+ // (capture-cleanup-cli.js RESUME_HINT).
30
+ const PARKED_REASON_PREFIX = "parked by capture cleanup";
31
+
32
+ const NON_HEALTHY = new Set([
33
+ "catching_up",
34
+ "degraded_retrying",
35
+ "blocked_action_required",
36
+ ]);
37
+
38
+ /**
39
+ * Count sessions whose per-session bucket is not healthy.
40
+ * @param {object} health
41
+ * @returns {number}
42
+ */
43
+ function affectedSessionCount(health) {
44
+ const counts = (health && health.counts) || {};
45
+ return (
46
+ Number(counts.catching_up || 0) +
47
+ Number(counts.degraded_retrying || 0) +
48
+ Number(counts.blocked_action_required || 0)
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Human reason string for notices / CLI.
54
+ * @param {object} health
55
+ * @returns {string}
56
+ */
57
+ function reasonForHealth(health) {
58
+ if (!health || typeof health !== "object") return "unknown";
59
+ const status = health.status || "healthy";
60
+ if (status === "healthy") return "all sessions healthy";
61
+ if (status === "blocked_action_required") {
62
+ const reasons = Array.isArray(health.blocked_reasons)
63
+ ? health.blocked_reasons.filter(Boolean)
64
+ : [];
65
+ if (reasons.length) return reasons.join("; ");
66
+ return "blocked — action required (ownership, datapack, or corrupt state)";
67
+ }
68
+ if (status === "degraded_retrying") {
69
+ const sessions = Array.isArray(health.sessions) ? health.sessions : [];
70
+ const classes = [];
71
+ for (const s of sessions) {
72
+ if (s && s.status === "degraded_retrying" && s.failure_class) {
73
+ if (!classes.includes(s.failure_class)) classes.push(s.failure_class);
74
+ }
75
+ }
76
+ if (classes.length) {
77
+ return `retrying after ${classes.join(", ")} failure(s)`;
78
+ }
79
+ return "delivery retrying with backoff";
80
+ }
81
+ // catching_up
82
+ const queued = Number(health.queued_exchanges || 0);
83
+ if (queued > 0) {
84
+ return `pending delivery backlog (${queued} exchange(s) queued)`;
85
+ }
86
+ return "sessions catching up on pending delivery";
87
+ }
88
+
89
+ /**
90
+ * Short composable Claude Code statusline suffix.
91
+ * Healthy → empty string (no capture segment).
92
+ *
93
+ * @param {object|null} health
94
+ * @returns {string}
95
+ */
96
+ function statuslineSuffixFromHealth(health) {
97
+ if (!health || typeof health !== "object") return "";
98
+ const status = health.status || "healthy";
99
+ if (!NON_HEALTHY.has(status)) return "";
100
+ const queued = Number(health.queued_exchanges || 0);
101
+ const short =
102
+ status === "blocked_action_required"
103
+ ? "blocked"
104
+ : status === "degraded_retrying"
105
+ ? "degraded"
106
+ : "catching_up";
107
+ return `capture:${short}(${queued})`;
108
+ }
109
+
110
+ /**
111
+ * Sentence describing exchanges that were skipped because the server will
112
+ * never accept them, or "" when there are none new to report.
113
+ *
114
+ * Drops are permanent, so they are not a health status — capture is running
115
+ * fine — but the user is owed the fact that specific turns are missing.
116
+ */
117
+ function dropSentence(dropped, previouslyNotified, health) {
118
+ const newDrops = dropped - previouslyNotified;
119
+ if (newDrops <= 0) return "";
120
+ const reasons = Array.isArray(health && health.drop_reasons)
121
+ ? health.drop_reasons.filter(Boolean)
122
+ : [];
123
+ // drop_reasons is the union of every session's last_drop_reason, including
124
+ // drops already announced. Attribute a reason only when there is one shared
125
+ // cause; otherwise point at --capture-status instead of mis-blaming.
126
+ const because =
127
+ reasons.length === 1
128
+ ? ` Reason: ${reasons[0]}.`
129
+ : reasons.length > 1
130
+ ? " Run `meko-mcp --capture-status` for drop reasons."
131
+ : "";
132
+ return (
133
+ `[Meko capture] ${newDrops} exchange(s) could not be stored and were ` +
134
+ `skipped; the rest of the session is captured normally.${because} ` +
135
+ `Tell the user those turns are missing from Meko.`
136
+ );
137
+ }
138
+
139
+ /**
140
+ * True when some session is still retrying. A session parked by
141
+ * `--capture-cleanup stop` is blocked on purpose and never retries, so it
142
+ * doesn't count; its blocked reason already says how to resume it.
143
+ */
144
+ function retriesRunning(health) {
145
+ // A degraded session with nothing left to send (leftover failure fields on
146
+ // a delivered session) isn't retrying; `repair` clears it, not `stop`.
147
+ const sessions = Array.isArray(health && health.sessions) ? health.sessions : [];
148
+ const retrying = sessions.some(
149
+ (s) =>
150
+ s &&
151
+ s.status === "degraded_retrying" &&
152
+ (Number(s.queued_exchanges || 0) > 0 ||
153
+ ["needs_conversation", "pending", "draining", "retry_wait"].includes(s.delivery)),
154
+ );
155
+ if (retrying) return true;
156
+ if (health.status !== "blocked_action_required") return false;
157
+ const reasons = Array.isArray(health.blocked_reasons)
158
+ ? health.blocked_reasons.filter(Boolean)
159
+ : [];
160
+ return (
161
+ reasons.length === 0 ||
162
+ reasons.some((r) => !String(r).startsWith(PARKED_REASON_PREFIX))
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Build a SessionStart-injectable health notice.
168
+ *
169
+ * - When status ≠ healthy: always emit (exact affected-session + queued counts + reason).
170
+ * - When status === healthy and previousStatus was non-healthy: one-shot recovery notice.
171
+ * - When status === healthy and previous was already healthy / null: no notice.
172
+ * - Any new dropped exchanges are appended in every case, and are enough on
173
+ * their own to produce a notice while status is healthy.
174
+ *
175
+ * @param {{ health: object, previousStatus?: string|null, previousDropped?: number }} opts
176
+ * @returns {{
177
+ * notice: string|null,
178
+ * nextNotifiedStatus: string,
179
+ * shouldPersistNotified: boolean,
180
+ * status: string,
181
+ * affectedSessions: number,
182
+ * queuedExchanges: number,
183
+ * droppedExchanges: number,
184
+ * reason: string,
185
+ * }}
186
+ */
187
+ function buildHealthNotice({ health, previousStatus, previousDropped } = {}) {
188
+ const h = health && typeof health === "object" ? health : { status: "healthy" };
189
+ const status = h.status || "healthy";
190
+ const prev =
191
+ previousStatus === undefined || previousStatus === null || previousStatus === ""
192
+ ? null
193
+ : String(previousStatus);
194
+ const affected = affectedSessionCount(h);
195
+ const queued = Number(h.queued_exchanges || 0);
196
+ const dropped = Number(h.dropped_exchanges || 0);
197
+ const notifiedDropped = Number(previousDropped || 0);
198
+ const drops = dropSentence(dropped, notifiedDropped, h);
199
+ const reason = reasonForHealth(h);
200
+ const join = (a, b) => (a && b ? `${a}\n\n${b}` : a || b || null);
201
+
202
+ if (status === "healthy") {
203
+ if (prev && NON_HEALTHY.has(prev)) {
204
+ return {
205
+ notice: join(
206
+ "[Meko capture] Capture recovered — all sessions healthy. " +
207
+ "Queued backlog is clear. Automatic conversation capture is running normally again.",
208
+ drops,
209
+ ),
210
+ nextNotifiedStatus: "healthy",
211
+ shouldPersistNotified: true,
212
+ status,
213
+ affectedSessions: 0,
214
+ queuedExchanges: queued,
215
+ droppedExchanges: dropped,
216
+ reason,
217
+ };
218
+ }
219
+ return {
220
+ notice: drops || null,
221
+ nextNotifiedStatus: "healthy",
222
+ shouldPersistNotified: prev !== "healthy" || Boolean(drops),
223
+ status,
224
+ affectedSessions: 0,
225
+ queuedExchanges: queued,
226
+ droppedExchanges: dropped,
227
+ reason,
228
+ };
229
+ }
230
+
231
+ const label =
232
+ status === "blocked_action_required"
233
+ ? "blocked_action_required"
234
+ : status === "degraded_retrying"
235
+ ? "degraded_retrying"
236
+ : "catching_up";
237
+
238
+ const cleanupHint = retriesRunning(h)
239
+ ? " If the user wants the retries to stop, offer " +
240
+ "`npx @yugabytedb/meko-mcp@latest --capture-cleanup stop`; it parks " +
241
+ "the backlog without deleting it."
242
+ : "";
243
+ const notice = join(
244
+ `[Meko capture] Status: ${label} — ${affected} session(s) affected, ` +
245
+ `${queued} exchange(s) queued. Reason: ${reason}. ` +
246
+ `Run \`meko-mcp --capture-status\` for details.${cleanupHint}`,
247
+ drops,
248
+ );
249
+
250
+ return {
251
+ notice,
252
+ nextNotifiedStatus: status,
253
+ shouldPersistNotified: true,
254
+ status,
255
+ affectedSessions: affected,
256
+ queuedExchanges: queued,
257
+ droppedExchanges: dropped,
258
+ reason,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Refresh aggregate health from disk, build a notice, and optionally persist
264
+ * `last_notified_status` on the health cache.
265
+ *
266
+ * Integration (SessionStart): after building additionalContext,
267
+ * const { injectHealthNotice } = require("./capture-notices");
268
+ * context = injectHealthNotice(context);
269
+ *
270
+ * @param {string} [context]
271
+ * @param {{ persist?: boolean, refresh?: boolean }} [opts]
272
+ * @returns {string}
273
+ */
274
+ function injectHealthNotice(context, opts) {
275
+ const options = opts && typeof opts === "object" ? opts : {};
276
+ const persist = options.persist !== false;
277
+ const refresh = options.refresh !== false;
278
+ const base = context == null ? "" : String(context);
279
+
280
+ let health = null;
281
+ let scanComplete = false;
282
+ if (refresh) {
283
+ const scan = scanStates({ includeBlocked: true });
284
+ scanComplete = scanCompleteForDropClamp(scan);
285
+ health = computeAggregateHealth(scan.entries || []);
286
+ const existing = readHealthCache();
287
+ if (existing && existing.last_notified_status !== undefined) {
288
+ health.last_notified_status = existing.last_notified_status;
289
+ }
290
+ if (existing && existing.last_notified_dropped !== undefined) {
291
+ const previous = Number(existing.last_notified_dropped || 0);
292
+ health.last_notified_dropped = scanComplete
293
+ ? Math.min(previous, Number(health.dropped_exchanges || 0))
294
+ : previous;
295
+ }
296
+ const written = writeHealthCache(health, { clampDropped: scanComplete });
297
+ if (written.ok) health = written.health;
298
+ } else {
299
+ health = readHealthCache() || {
300
+ schema_version: 1,
301
+ status: "healthy",
302
+ counts: {
303
+ healthy: 0,
304
+ catching_up: 0,
305
+ degraded_retrying: 0,
306
+ blocked_action_required: 0,
307
+ },
308
+ queued_exchanges: 0,
309
+ dropped_exchanges: 0,
310
+ oldest_pending_age_seconds: null,
311
+ last_success_at: null,
312
+ blocked_reasons: [],
313
+ drop_reasons: [],
314
+ sessions: [],
315
+ };
316
+ }
317
+
318
+ const previousStatus =
319
+ health.last_notified_status !== undefined
320
+ ? health.last_notified_status
321
+ : null;
322
+ const previousDropped = Number(health.last_notified_dropped || 0);
323
+ const built = buildHealthNotice({ health, previousStatus, previousDropped });
324
+
325
+ if (persist && built.shouldPersistNotified) {
326
+ const nextHealth = {
327
+ ...health,
328
+ last_notified_status: built.nextNotifiedStatus,
329
+ };
330
+ if (scanComplete || !refresh) {
331
+ nextHealth.last_notified_dropped = built.droppedExchanges;
332
+ } else {
333
+ nextHealth.last_notified_dropped = Math.max(
334
+ previousDropped,
335
+ built.droppedExchanges,
336
+ );
337
+ }
338
+ writeHealthCache(nextHealth, { clampDropped: scanComplete });
339
+ }
340
+
341
+ if (!built.notice) return base;
342
+ if (!base) return built.notice;
343
+ return `${base}\n\n${built.notice}`;
344
+ }
345
+
346
+ /**
347
+ * Format human-readable --capture-status report.
348
+ * @param {object} health
349
+ * @returns {string}
350
+ */
351
+ function formatCaptureStatusText(health) {
352
+ const h = health && typeof health === "object" ? health : {};
353
+ const status = h.status || "healthy";
354
+ const counts = h.counts || {};
355
+ const lines = [
356
+ `Meko capture status: ${status}`,
357
+ ` sessions: healthy=${Number(counts.healthy || 0)} ` +
358
+ `catching_up=${Number(counts.catching_up || 0)} ` +
359
+ `degraded_retrying=${Number(counts.degraded_retrying || 0)} ` +
360
+ `blocked_action_required=${Number(counts.blocked_action_required || 0)}`,
361
+ ` queued exchanges: ${Number(h.queued_exchanges || 0)}`,
362
+ ` dropped exchanges: ${Number(h.dropped_exchanges || 0)}`,
363
+ ` oldest pending age (seconds): ${
364
+ h.oldest_pending_age_seconds == null ? "n/a" : h.oldest_pending_age_seconds
365
+ }`,
366
+ ` last success: ${h.last_success_at || "n/a"}`,
367
+ ` reason: ${reasonForHealth(h)}`,
368
+ ];
369
+ const blocked = Array.isArray(h.blocked_reasons) ? h.blocked_reasons : [];
370
+ if (blocked.length) {
371
+ lines.push(` blocked reasons: ${blocked.join("; ")}`);
372
+ }
373
+ const dropReasons = Array.isArray(h.drop_reasons) ? h.drop_reasons : [];
374
+ if (dropReasons.length) {
375
+ lines.push(` drop reasons: ${dropReasons.join("; ")}`);
376
+ }
377
+ if (status === "degraded_retrying" || status === "blocked_action_required") {
378
+ lines.push(
379
+ " per-session detail and cleanup: meko-mcp --capture-cleanup status",
380
+ );
381
+ }
382
+ return lines.join("\n");
383
+ }
384
+
385
+ /**
386
+ * Exit code for aggregate health (contracts).
387
+ * @param {string} status
388
+ * @returns {number}
389
+ */
390
+ function exitCodeForStatus(status) {
391
+ if (status === "blocked_action_required") return 3;
392
+ if (status === "degraded_retrying") return 2;
393
+ // healthy | catching_up
394
+ return 0;
395
+ }
396
+
397
+ /**
398
+ * Stable JSON shape for --capture-status --json (aggregate health fields).
399
+ * @param {object} health
400
+ * @returns {object}
401
+ */
402
+ function captureStatusJson(health) {
403
+ const h = health && typeof health === "object" ? health : {};
404
+ return {
405
+ schema_version: 1,
406
+ status: h.status || "healthy",
407
+ updated_at: h.updated_at || null,
408
+ counts: {
409
+ healthy: Number((h.counts && h.counts.healthy) || 0),
410
+ catching_up: Number((h.counts && h.counts.catching_up) || 0),
411
+ degraded_retrying: Number((h.counts && h.counts.degraded_retrying) || 0),
412
+ blocked_action_required: Number(
413
+ (h.counts && h.counts.blocked_action_required) || 0,
414
+ ),
415
+ },
416
+ queued_exchanges: Number(h.queued_exchanges || 0),
417
+ dropped_exchanges: Number(h.dropped_exchanges || 0),
418
+ oldest_pending_age_seconds:
419
+ h.oldest_pending_age_seconds == null ? null : h.oldest_pending_age_seconds,
420
+ last_success_at: h.last_success_at || null,
421
+ blocked_reasons: Array.isArray(h.blocked_reasons) ? h.blocked_reasons : [],
422
+ drop_reasons: Array.isArray(h.drop_reasons) ? h.drop_reasons : [],
423
+ sessions: Array.isArray(h.sessions) ? h.sessions : [],
424
+ };
425
+ }
426
+
427
+ module.exports = {
428
+ PARKED_REASON_PREFIX,
429
+ affectedSessionCount,
430
+ reasonForHealth,
431
+ statuslineSuffixFromHealth,
432
+ buildHealthNotice,
433
+ injectHealthNotice,
434
+ formatCaptureStatusText,
435
+ exitCodeForStatus,
436
+ captureStatusJson,
437
+ NON_HEALTHY,
438
+ };