@yugabytedb/meko-mcp 2.1.0 → 2.2.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 (45) hide show
  1. package/README.md +152 -27
  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-notices.js +321 -0
  5. package/assets/hooks-handlers/lib/capture-state.js +2079 -0
  6. package/assets/hooks-handlers/lib/capture-status-cli.js +82 -0
  7. package/assets/hooks-handlers/lib/capture.js +1578 -299
  8. package/assets/hooks-handlers/lib/checkpoint-timer.js +444 -81
  9. package/assets/hooks-handlers/lib/kiro-handler.js +738 -0
  10. package/assets/hooks-handlers/lib/terminal-handoff.js +418 -0
  11. package/assets/hooks-handlers/session-end.sh +3 -34
  12. package/assets/scripts/meko-statusline.sh +41 -1
  13. package/assets/skills/meko-mcp-tools/SKILL.md +72 -125
  14. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
  15. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +12 -13
  16. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +15 -12
  17. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +25 -1
  18. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +19 -77
  19. package/assets/skills/meko-mcp-tools/references/tools-overview.md +8 -7
  20. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +22 -1
  21. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +1 -1
  22. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +8 -8
  23. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
  24. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +6 -1
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +6 -5
  27. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +4 -4
  28. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +7 -6
  29. package/assets/skills/meko-select-datapack-desktop/SKILL.md +1 -1
  30. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +3 -3
  31. package/bin/create-meko-setup.mjs +48 -10
  32. package/lib/capture-status.mjs +67 -0
  33. package/lib/clients/aliases.mjs +5 -0
  34. package/lib/clients/claude-desktop.mjs +120 -5
  35. package/lib/clients/codex.mjs +6 -3
  36. package/lib/clients/index.mjs +2 -0
  37. package/lib/clients/kiro.mjs +705 -0
  38. package/lib/config.mjs +90 -0
  39. package/lib/installer.mjs +1 -0
  40. package/lib/migrate/adapters/codex.mjs +490 -0
  41. package/lib/migrate/id.mjs +28 -3
  42. package/lib/migrate/orchestrator.mjs +34 -7
  43. package/lib/migrate.mjs +4 -2
  44. package/lib/prompt.mjs +16 -1
  45. package/package.json +4 -4
@@ -0,0 +1,321 @@
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 extension (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
+
15
+ "use strict";
16
+
17
+ const {
18
+ computeAggregateHealth,
19
+ readHealthCache,
20
+ scanStates,
21
+ writeHealthCache,
22
+ } = require("./capture-state");
23
+
24
+ const NON_HEALTHY = new Set([
25
+ "catching_up",
26
+ "degraded_retrying",
27
+ "blocked_action_required",
28
+ ]);
29
+
30
+ /**
31
+ * Count sessions whose per-session bucket is not healthy.
32
+ * @param {object} health
33
+ * @returns {number}
34
+ */
35
+ function affectedSessionCount(health) {
36
+ const counts = (health && health.counts) || {};
37
+ return (
38
+ Number(counts.catching_up || 0) +
39
+ Number(counts.degraded_retrying || 0) +
40
+ Number(counts.blocked_action_required || 0)
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Human reason string for notices / CLI.
46
+ * @param {object} health
47
+ * @returns {string}
48
+ */
49
+ function reasonForHealth(health) {
50
+ if (!health || typeof health !== "object") return "unknown";
51
+ const status = health.status || "healthy";
52
+ if (status === "healthy") return "all sessions healthy";
53
+ if (status === "blocked_action_required") {
54
+ const reasons = Array.isArray(health.blocked_reasons)
55
+ ? health.blocked_reasons.filter(Boolean)
56
+ : [];
57
+ if (reasons.length) return reasons.join("; ");
58
+ return "blocked — action required (ownership, datapack, or corrupt state)";
59
+ }
60
+ if (status === "degraded_retrying") {
61
+ const sessions = Array.isArray(health.sessions) ? health.sessions : [];
62
+ const classes = [];
63
+ for (const s of sessions) {
64
+ if (s && s.status === "degraded_retrying" && s.failure_class) {
65
+ if (!classes.includes(s.failure_class)) classes.push(s.failure_class);
66
+ }
67
+ }
68
+ if (classes.length) {
69
+ return `retrying after ${classes.join(", ")} failure(s)`;
70
+ }
71
+ return "delivery retrying with backoff";
72
+ }
73
+ // catching_up
74
+ const queued = Number(health.queued_exchanges || 0);
75
+ if (queued > 0) {
76
+ return `pending delivery backlog (${queued} exchange(s) queued)`;
77
+ }
78
+ return "sessions catching up on pending delivery";
79
+ }
80
+
81
+ /**
82
+ * Short composable Claude Code statusline suffix.
83
+ * Healthy → empty string (no capture segment).
84
+ *
85
+ * @param {object|null} health
86
+ * @returns {string}
87
+ */
88
+ function statuslineSuffixFromHealth(health) {
89
+ if (!health || typeof health !== "object") return "";
90
+ const status = health.status || "healthy";
91
+ if (!NON_HEALTHY.has(status)) return "";
92
+ const queued = Number(health.queued_exchanges || 0);
93
+ const short =
94
+ status === "blocked_action_required"
95
+ ? "blocked"
96
+ : status === "degraded_retrying"
97
+ ? "degraded"
98
+ : "catching_up";
99
+ return `capture:${short}(${queued})`;
100
+ }
101
+
102
+ /**
103
+ * Build a SessionStart-injectable health notice.
104
+ *
105
+ * - When status ≠ healthy: always emit (exact affected-session + queued counts + reason).
106
+ * - When status === healthy and previousStatus was non-healthy: one-shot recovery notice.
107
+ * - When status === healthy and previous was already healthy / null: no notice.
108
+ *
109
+ * @param {{ health: object, previousStatus?: string|null }} opts
110
+ * @returns {{
111
+ * notice: string|null,
112
+ * nextNotifiedStatus: string,
113
+ * shouldPersistNotified: boolean,
114
+ * status: string,
115
+ * affectedSessions: number,
116
+ * queuedExchanges: number,
117
+ * reason: string,
118
+ * }}
119
+ */
120
+ function buildHealthNotice({ health, previousStatus } = {}) {
121
+ const h = health && typeof health === "object" ? health : { status: "healthy" };
122
+ const status = h.status || "healthy";
123
+ const prev =
124
+ previousStatus === undefined || previousStatus === null || previousStatus === ""
125
+ ? null
126
+ : String(previousStatus);
127
+ const affected = affectedSessionCount(h);
128
+ const queued = Number(h.queued_exchanges || 0);
129
+ const reason = reasonForHealth(h);
130
+
131
+ if (status === "healthy") {
132
+ if (prev && NON_HEALTHY.has(prev)) {
133
+ return {
134
+ notice:
135
+ "[Meko capture] Capture recovered — all sessions healthy. " +
136
+ "Queued backlog is clear. Automatic conversation capture is running normally again.",
137
+ nextNotifiedStatus: "healthy",
138
+ shouldPersistNotified: true,
139
+ status,
140
+ affectedSessions: 0,
141
+ queuedExchanges: queued,
142
+ reason,
143
+ };
144
+ }
145
+ return {
146
+ notice: null,
147
+ nextNotifiedStatus: "healthy",
148
+ shouldPersistNotified: prev !== "healthy",
149
+ status,
150
+ affectedSessions: 0,
151
+ queuedExchanges: queued,
152
+ reason,
153
+ };
154
+ }
155
+
156
+ const label =
157
+ status === "blocked_action_required"
158
+ ? "blocked_action_required"
159
+ : status === "degraded_retrying"
160
+ ? "degraded_retrying"
161
+ : "catching_up";
162
+
163
+ const notice =
164
+ `[Meko capture] Status: ${label} — ${affected} session(s) affected, ` +
165
+ `${queued} exchange(s) queued. Reason: ${reason}. ` +
166
+ `Run \`meko-mcp --capture-status\` for details.`;
167
+
168
+ return {
169
+ notice,
170
+ nextNotifiedStatus: status,
171
+ shouldPersistNotified: true,
172
+ status,
173
+ affectedSessions: affected,
174
+ queuedExchanges: queued,
175
+ reason,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Refresh aggregate health from disk, build a notice, and optionally persist
181
+ * `last_notified_status` on the health cache.
182
+ *
183
+ * Integration (SessionStart): after building additionalContext,
184
+ * const { injectHealthNotice } = require("./capture-notices");
185
+ * context = injectHealthNotice(context);
186
+ *
187
+ * @param {string} [context]
188
+ * @param {{ persist?: boolean, refresh?: boolean }} [opts]
189
+ * @returns {string}
190
+ */
191
+ function injectHealthNotice(context, opts) {
192
+ const options = opts && typeof opts === "object" ? opts : {};
193
+ const persist = options.persist !== false;
194
+ const refresh = options.refresh !== false;
195
+ const base = context == null ? "" : String(context);
196
+
197
+ let health = null;
198
+ if (refresh) {
199
+ const scan = scanStates({ includeBlocked: true });
200
+ health = computeAggregateHealth(scan.entries || []);
201
+ const existing = readHealthCache();
202
+ if (existing && existing.last_notified_status !== undefined) {
203
+ health.last_notified_status = existing.last_notified_status;
204
+ }
205
+ writeHealthCache(health);
206
+ } else {
207
+ health = readHealthCache() || {
208
+ schema_version: 1,
209
+ status: "healthy",
210
+ counts: {
211
+ healthy: 0,
212
+ catching_up: 0,
213
+ degraded_retrying: 0,
214
+ blocked_action_required: 0,
215
+ },
216
+ queued_exchanges: 0,
217
+ oldest_pending_age_seconds: null,
218
+ last_success_at: null,
219
+ blocked_reasons: [],
220
+ sessions: [],
221
+ };
222
+ }
223
+
224
+ const previousStatus =
225
+ health.last_notified_status !== undefined
226
+ ? health.last_notified_status
227
+ : null;
228
+ const built = buildHealthNotice({ health, previousStatus });
229
+
230
+ if (persist && built.shouldPersistNotified) {
231
+ writeHealthCache({
232
+ ...health,
233
+ last_notified_status: built.nextNotifiedStatus,
234
+ });
235
+ }
236
+
237
+ if (!built.notice) return base;
238
+ if (!base) return built.notice;
239
+ return `${base}\n\n${built.notice}`;
240
+ }
241
+
242
+ /**
243
+ * Format human-readable --capture-status report.
244
+ * @param {object} health
245
+ * @returns {string}
246
+ */
247
+ function formatCaptureStatusText(health) {
248
+ const h = health && typeof health === "object" ? health : {};
249
+ const status = h.status || "healthy";
250
+ const counts = h.counts || {};
251
+ const lines = [
252
+ `Meko capture status: ${status}`,
253
+ ` sessions: healthy=${Number(counts.healthy || 0)} ` +
254
+ `catching_up=${Number(counts.catching_up || 0)} ` +
255
+ `degraded_retrying=${Number(counts.degraded_retrying || 0)} ` +
256
+ `blocked_action_required=${Number(counts.blocked_action_required || 0)}`,
257
+ ` queued exchanges: ${Number(h.queued_exchanges || 0)}`,
258
+ ` oldest pending age (seconds): ${
259
+ h.oldest_pending_age_seconds == null ? "n/a" : h.oldest_pending_age_seconds
260
+ }`,
261
+ ` last success: ${h.last_success_at || "n/a"}`,
262
+ ` reason: ${reasonForHealth(h)}`,
263
+ ];
264
+ const blocked = Array.isArray(h.blocked_reasons) ? h.blocked_reasons : [];
265
+ if (blocked.length) {
266
+ lines.push(` blocked reasons: ${blocked.join("; ")}`);
267
+ }
268
+ return lines.join("\n");
269
+ }
270
+
271
+ /**
272
+ * Exit code for aggregate health (contracts).
273
+ * @param {string} status
274
+ * @returns {number}
275
+ */
276
+ function exitCodeForStatus(status) {
277
+ if (status === "blocked_action_required") return 3;
278
+ if (status === "degraded_retrying") return 2;
279
+ // healthy | catching_up
280
+ return 0;
281
+ }
282
+
283
+ /**
284
+ * Stable JSON shape for --capture-status --json (aggregate health fields).
285
+ * @param {object} health
286
+ * @returns {object}
287
+ */
288
+ function captureStatusJson(health) {
289
+ const h = health && typeof health === "object" ? health : {};
290
+ return {
291
+ schema_version: 1,
292
+ status: h.status || "healthy",
293
+ updated_at: h.updated_at || null,
294
+ counts: {
295
+ healthy: Number((h.counts && h.counts.healthy) || 0),
296
+ catching_up: Number((h.counts && h.counts.catching_up) || 0),
297
+ degraded_retrying: Number((h.counts && h.counts.degraded_retrying) || 0),
298
+ blocked_action_required: Number(
299
+ (h.counts && h.counts.blocked_action_required) || 0,
300
+ ),
301
+ },
302
+ queued_exchanges: Number(h.queued_exchanges || 0),
303
+ oldest_pending_age_seconds:
304
+ h.oldest_pending_age_seconds == null ? null : h.oldest_pending_age_seconds,
305
+ last_success_at: h.last_success_at || null,
306
+ blocked_reasons: Array.isArray(h.blocked_reasons) ? h.blocked_reasons : [],
307
+ sessions: Array.isArray(h.sessions) ? h.sessions : [],
308
+ };
309
+ }
310
+
311
+ module.exports = {
312
+ affectedSessionCount,
313
+ reasonForHealth,
314
+ statuslineSuffixFromHealth,
315
+ buildHealthNotice,
316
+ injectHealthNotice,
317
+ formatCaptureStatusText,
318
+ exitCodeForStatus,
319
+ captureStatusJson,
320
+ NON_HEALTHY,
321
+ };