@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
@@ -131,6 +131,53 @@ function healthCacheFilename() {
131
131
  return envString("MEKO_HEALTH_CACHE", DEFAULTS.HEALTH_CACHE);
132
132
  }
133
133
 
134
+ /**
135
+ * The retry date `--capture-cleanup` parks a session until. Hooks skip the
136
+ * network while failure_class is set and next_retry_at is in the future, so
137
+ * a session with this date is parked, not retrying.
138
+ */
139
+ const CLEANUP_PARK_UNTIL = "9999-12-31T00:00:00.000Z";
140
+
141
+ /**
142
+ * Automatic repair from the checkpoint timer. MEKO_AUTOFIX (on/off) wins when
143
+ * set; otherwise autofix-config.json in the capture directory, which the
144
+ * installer writes for --autofix / --no-autofix. On by default.
145
+ */
146
+ function autofixEnabled() {
147
+ return autofixSetting().enabled;
148
+ }
149
+
150
+ /**
151
+ * Whether automatic repair is on, and what decided it: "env" (MEKO_AUTOFIX),
152
+ * "config" (autofix-config.json, written by --autofix / --no-autofix), or
153
+ * "default" (on).
154
+ */
155
+ function autofixSetting() {
156
+ const raw = process.env.MEKO_AUTOFIX;
157
+ if (raw != null && raw !== "") {
158
+ return { enabled: !/^(0|off|false|no)$/i.test(String(raw)), source: "env", path: null };
159
+ }
160
+ const file = autofixConfigPath();
161
+ try {
162
+ const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
163
+ if (cfg && typeof cfg.enabled === "boolean") {
164
+ return { enabled: cfg.enabled, source: "config", path: file };
165
+ }
166
+ } catch {
167
+ /* no config: default on */
168
+ }
169
+ return { enabled: true, source: "default", path: null };
170
+ }
171
+
172
+ function autofixConfigPath() {
173
+ return path.join(watermarkDir(), "autofix-config.json");
174
+ }
175
+
176
+ /** Seconds between automatic `--capture-cleanup fix` runs on one machine. */
177
+ function autofixIntervalSeconds() {
178
+ return Math.max(60, envInt("MEKO_AUTOFIX_INTERVAL", 1800));
179
+ }
180
+
134
181
  // ---------------------------------------------------------------------------
135
182
  // Path helpers
136
183
  // ---------------------------------------------------------------------------
@@ -162,6 +209,20 @@ function healthCachePath() {
162
209
  return path.join(watermarkDir(), healthCacheFilename());
163
210
  }
164
211
 
212
+ /** Summary of the last `--capture-cleanup fix` run, manual or automatic. */
213
+ function autofixLastPath() {
214
+ return path.join(watermarkDir(), "autofix-last.json");
215
+ }
216
+
217
+ function readAutofixLast() {
218
+ try {
219
+ const value = JSON.parse(fs.readFileSync(autofixLastPath(), "utf8"));
220
+ return value && typeof value === "object" ? value : null;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
165
226
  /**
166
227
  * Convert an agent_id into the filesystem slug used by legacy error files and
167
228
  * datapack pins (matches capture.js datapackPinSlug).
@@ -293,6 +354,21 @@ function blockedOutcome(failureClass, error, extras) {
293
354
  };
294
355
  }
295
356
 
357
+ /**
358
+ * MEKO-635: resumable sessions (OpenCode) must not be finalized as
359
+ * confirmed_dead by timer idle or cross-session recovery. Only OpenCode sets
360
+ * this policy today; other clients continue to finalize as before. Existing
361
+ * pre-MEKO-635 `client: "opencode"` watermarks without an explicit
362
+ * `pending_turn_policy` field are also treated as resumable so upgrades don't
363
+ * finalize a turn that a later snapshot could still complete.
364
+ */
365
+ function isResumableState(state) {
366
+ if (!state || typeof state !== "object") return false;
367
+ if (state.pending_turn_policy === "resumable") return true;
368
+ if (state.pending_turn_policy == null && state.client === "opencode") return true;
369
+ return false;
370
+ }
371
+
296
372
  /**
297
373
  * Validate a candidate v2 state object.
298
374
  * @returns {{ ok:true, state:Object } | { ok:false, delivery:"blocked", failure_class:"state_corrupt", error:string }}
@@ -342,6 +418,16 @@ function validateV2(state) {
342
418
  `invalid failure_class: ${state.failure_class}`,
343
419
  );
344
420
  }
421
+ if (
422
+ state.consecutive_exchange_drops != null &&
423
+ (!Number.isInteger(state.consecutive_exchange_drops) ||
424
+ state.consecutive_exchange_drops < 0)
425
+ ) {
426
+ return blockedOutcome(
427
+ "state_corrupt",
428
+ "consecutive_exchange_drops must be a non-negative integer",
429
+ );
430
+ }
345
431
  return { ok: true, state };
346
432
  }
347
433
 
@@ -461,6 +547,7 @@ function normalizeState(raw, context) {
461
547
  last_success_at:
462
548
  raw.last_success_at == null ? null : String(raw.last_success_at),
463
549
  blocked_reason: null,
550
+ consecutive_exchange_drops: 0,
464
551
  };
465
552
 
466
553
  const validated = validateV2(state);
@@ -938,6 +1025,8 @@ function writeSessionIntent(sessionId, fields) {
938
1025
  last_error: null,
939
1026
  last_success_at: null,
940
1027
  blocked_reason: null,
1028
+ // null = finalize unless client is opencode (isResumableState).
1029
+ pending_turn_policy: null,
941
1030
  };
942
1031
 
943
1032
  const conversationId =
@@ -974,6 +1063,12 @@ function writeSessionIntent(sessionId, fields) {
974
1063
  : "needs_conversation",
975
1064
  last_activity_at: nowIso(),
976
1065
  updated_at: nowIso(),
1066
+ // MEKO-635: preserve or set pending_turn_policy. Only "resumable" is a
1067
+ // meaningful non-default value today; anything else stays null.
1068
+ pending_turn_policy:
1069
+ f.pending_turn_policy !== undefined
1070
+ ? (f.pending_turn_policy === "resumable" ? "resumable" : null)
1071
+ : (base.pending_turn_policy === "resumable" ? "resumable" : null),
977
1072
  };
978
1073
 
979
1074
  if (f.last_line_number != null && existing.missing) {
@@ -1065,6 +1160,10 @@ function rebucket(state, newConversationId) {
1065
1160
  blocked_reason: null,
1066
1161
  next_retry_at: null,
1067
1162
  attempt_count: 0,
1163
+ consecutive_exchange_drops: 0,
1164
+ // A replacement guard belongs only to the conversation that set it.
1165
+ recovered_from_conversation_id: null,
1166
+ missing_conversation_streak: null,
1068
1167
  updated_at: nowIso(),
1069
1168
  last_activity_at: nowIso(),
1070
1169
  };
@@ -1257,6 +1356,8 @@ const PERSISTENT_PATTERNS = [
1257
1356
  ["payment_required", /payment[_\s-]?required/i],
1258
1357
  ["subscription_required", /subscription[_\s-]?(required|inactive|expired)/i],
1259
1358
  ["entitlement_denied", /entitlement.*(denied|required|missing)|(?:denied|required|missing).*entitlement/i],
1359
+ // A viewer of a shared datapack can't record conversations (MEKO-876).
1360
+ ["read_only_access", /read_only_access/i],
1260
1361
  ];
1261
1362
 
1262
1363
  /**
@@ -1384,6 +1485,26 @@ function timerLooksLive(sessionId) {
1384
1485
  }
1385
1486
  }
1386
1487
 
1488
+ /**
1489
+ * A session whose client may still be writing turns: its lifecycle is
1490
+ * active and its transcript changed within recoveryMinIdle() seconds, or its
1491
+ * checkpoint timer is alive. --capture-cleanup uses it to decide drain or
1492
+ * recover, and capture.js move re-checks it under the lease.
1493
+ */
1494
+ function sessionLooksLive(state, nowMs) {
1495
+ if (!state || state.lifecycle !== "active") return false;
1496
+ const now = nowMs != null ? nowMs : Date.now();
1497
+ if (state.transcript_path) {
1498
+ try {
1499
+ const st = fs.statSync(state.transcript_path);
1500
+ if (now - st.mtimeMs < recoveryMinIdle() * 1000) return true;
1501
+ } catch {
1502
+ // A missing transcript says nothing about liveness; check the timer.
1503
+ }
1504
+ }
1505
+ return timerLooksLive(state.session_id);
1506
+ }
1507
+
1387
1508
  function buildEligibility(state, nowMs, options) {
1388
1509
  const opts = options && typeof options === "object" ? options : {};
1389
1510
  const now = nowMs != null ? nowMs : Date.now();
@@ -1467,6 +1588,18 @@ function sessionHealthBucket(entry) {
1467
1588
  ) {
1468
1589
  return "blocked_action_required";
1469
1590
  }
1591
+ // A session that has delivered everything is healthy even if an earlier
1592
+ // attempt failed: failure_class records the last attempt, not outstanding
1593
+ // work. Without this, one transient blip pins a closed, fully drained
1594
+ // session to degraded_retrying forever and the rollup reports degraded with
1595
+ // an empty queue. Blocked classes are checked above and still win, since
1596
+ // those need a human.
1597
+ if (
1598
+ (state.delivery === "complete" || state.delivery === "idle") &&
1599
+ coerceNonNegInt(state.queued_exchanges, 0) === 0
1600
+ ) {
1601
+ return "healthy";
1602
+ }
1470
1603
  if (
1471
1604
  state.delivery === "retry_wait" ||
1472
1605
  fc === "transient" ||
@@ -1554,6 +1687,15 @@ function scanStates(options) {
1554
1687
  return { ok: true, entries, dir };
1555
1688
  }
1556
1689
 
1690
+ function scanCompleteForDropClamp(scan) {
1691
+ return Boolean(
1692
+ scan &&
1693
+ scan.ok &&
1694
+ Array.isArray(scan.entries) &&
1695
+ scan.entries.every((entry) => entry && entry.ok !== false),
1696
+ );
1697
+ }
1698
+
1557
1699
  function sortOldestFirst(entries) {
1558
1700
  return [...(entries || [])].sort((a, b) => {
1559
1701
  const ak =
@@ -1588,9 +1730,11 @@ function computeAggregateHealth(states, options) {
1588
1730
  blocked_action_required: 0,
1589
1731
  };
1590
1732
  let queued = 0;
1733
+ let dropped = 0;
1591
1734
  let oldestPendingAge = null;
1592
1735
  let lastSuccessAt = null;
1593
1736
  const blockedReasons = [];
1737
+ const dropReasons = [];
1594
1738
  const sessions = [];
1595
1739
  let status = "healthy";
1596
1740
 
@@ -1612,6 +1756,20 @@ function computeAggregateHealth(states, options) {
1612
1756
  : 0;
1613
1757
  queued += q;
1614
1758
 
1759
+ // Exchanges the server will never accept (see capture.js
1760
+ // classifyPersistentCaptureFailure, scope "exchange"). They are not
1761
+ // backlog — capture moved past them — but the user is still owed the
1762
+ // fact that some turns are missing, so they aggregate separately.
1763
+ const d =
1764
+ state && typeof state === "object"
1765
+ ? coerceNonNegInt(state.dropped_exchanges, 0)
1766
+ : 0;
1767
+ dropped += d;
1768
+ if (d > 0 && state.last_drop_reason) {
1769
+ const reason = String(state.last_drop_reason);
1770
+ if (!dropReasons.includes(reason)) dropReasons.push(reason);
1771
+ }
1772
+
1615
1773
  if (state && state.last_success_at) {
1616
1774
  if (
1617
1775
  !lastSuccessAt ||
@@ -1659,7 +1817,18 @@ function computeAggregateHealth(states, options) {
1659
1817
  (entry && entry.failure_class) ||
1660
1818
  null,
1661
1819
  queued_exchanges: q,
1820
+ dropped_exchanges: d,
1662
1821
  lifecycle: state ? state.lifecycle : null,
1822
+ // Why --capture-cleanup parked this session ("stop", "drain failed: ...")
1823
+ // while the park is in effect; null otherwise.
1824
+ parked_reason:
1825
+ state &&
1826
+ state.parked &&
1827
+ typeof state.parked === "object" &&
1828
+ state.failure_class &&
1829
+ state.next_retry_at === CLEANUP_PARK_UNTIL
1830
+ ? String(state.parked.reason || "parked")
1831
+ : null,
1663
1832
  });
1664
1833
  }
1665
1834
 
@@ -1669,14 +1838,17 @@ function computeAggregateHealth(states, options) {
1669
1838
  updated_at: nowIso(nowMs),
1670
1839
  counts,
1671
1840
  queued_exchanges: queued,
1841
+ dropped_exchanges: dropped,
1672
1842
  oldest_pending_age_seconds: oldestPendingAge,
1673
1843
  last_success_at: lastSuccessAt,
1674
1844
  blocked_reasons: blockedReasons,
1845
+ drop_reasons: dropReasons,
1675
1846
  sessions,
1676
1847
  };
1677
1848
  }
1678
1849
 
1679
- function writeHealthCache(health) {
1850
+ function writeHealthCache(health, options) {
1851
+ const opts = options && typeof options === "object" ? options : {};
1680
1852
  const existing = readHealthCache();
1681
1853
  const merged = {
1682
1854
  ...(health && typeof health === "object" ? health : {}),
@@ -1690,6 +1862,29 @@ function writeHealthCache(health) {
1690
1862
  ) {
1691
1863
  merged.last_notified_status = existing.last_notified_status;
1692
1864
  }
1865
+ // Drop notices are one-shot per new count. Clamp only after a complete scan
1866
+ // proves the aggregate shrank because watermarks disappeared. A failed scan
1867
+ // or unreadable watermark may recover and must not re-announce old drops.
1868
+ if (
1869
+ merged.last_notified_dropped === undefined &&
1870
+ existing &&
1871
+ existing.last_notified_dropped !== undefined
1872
+ ) {
1873
+ const previous = coerceNonNegInt(existing.last_notified_dropped, 0);
1874
+ merged.last_notified_dropped = opts.clampDropped
1875
+ ? Math.min(previous, coerceNonNegInt(merged.dropped_exchanges, 0))
1876
+ : previous;
1877
+ }
1878
+ // A ready-made status bar text, so meko-statusline.sh needs no JSON parsing
1879
+ // beyond one field. capture-notices requires this module, so load it here.
1880
+ try {
1881
+ const fields = require("./capture-notices").statuslineFields(merged, readAutofixLast());
1882
+ merged.statusline = fields.statusline;
1883
+ merged.statusline_until = fields.statusline_until != null ? fields.statusline_until : null;
1884
+ merged.statusline_after = fields.statusline_after != null ? fields.statusline_after : null;
1885
+ } catch {
1886
+ /* keep the cache write even if the text can't be built */
1887
+ }
1693
1888
  try {
1694
1889
  atomicWriteJson(healthCachePath(), merged);
1695
1890
  return { ok: true, path: healthCachePath(), health: merged };
@@ -2025,6 +2220,13 @@ module.exports = {
2025
2220
  workerMaxAgeSeconds,
2026
2221
  timerMaxAge,
2027
2222
  healthCacheFilename,
2223
+ CLEANUP_PARK_UNTIL,
2224
+ autofixEnabled,
2225
+ autofixSetting,
2226
+ autofixConfigPath,
2227
+ autofixIntervalSeconds,
2228
+ autofixLastPath,
2229
+ readAutofixLast,
2028
2230
 
2029
2231
  // paths
2030
2232
  watermarkDir,
@@ -2032,6 +2234,7 @@ module.exports = {
2032
2234
  leasePath,
2033
2235
  mutationLockPath,
2034
2236
  timerPidPath,
2237
+ sessionLooksLive,
2035
2238
  healthCachePath,
2036
2239
  legacyErrorPath,
2037
2240
  agentSlug,
@@ -2039,6 +2242,7 @@ module.exports = {
2039
2242
  // normalize / validate
2040
2243
  normalizeState,
2041
2244
  validateV2,
2245
+ isResumableState,
2042
2246
 
2043
2247
  // persistence
2044
2248
  readState,
@@ -2063,6 +2267,7 @@ module.exports = {
2063
2267
 
2064
2268
  // scan
2065
2269
  scanStates,
2270
+ scanCompleteForDropClamp,
2066
2271
  sortOldestFirst,
2067
2272
  sessionHealthBucket,
2068
2273
  buildEligibility,
@@ -22,6 +22,7 @@ const {
22
22
  importLegacyCaptureErrors,
23
23
  legacyErrorsSafeToRemove,
24
24
  readHealthCache,
25
+ scanCompleteForDropClamp,
25
26
  scanStates,
26
27
  writeHealthCache,
27
28
  } = require("./capture-state");
@@ -63,12 +64,15 @@ function main() {
63
64
  );
64
65
  }
65
66
 
66
- const health = computeAggregateHealth(scan.entries || []);
67
+ let health = computeAggregateHealth(scan.entries || []);
67
68
  const existing = readHealthCache();
68
69
  if (existing && existing.last_notified_status !== undefined) {
69
70
  health.last_notified_status = existing.last_notified_status;
70
71
  }
71
- writeHealthCache(health);
72
+ const written = writeHealthCache(health, {
73
+ clampDropped: scanCompleteForDropClamp(scan),
74
+ });
75
+ if (written.ok) health = written.health;
72
76
 
73
77
  const payload = captureStatusJson(health);
74
78
  if (json) {