@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.
- package/README.md +233 -40
- package/assets/hooks-handlers/codex-session-end.sh +4 -69
- package/assets/hooks-handlers/cursor-session-start.sh +19 -1
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
- package/assets/hooks-handlers/lib/capture-notices.js +438 -0
- package/assets/hooks-handlers/lib/capture-state.js +2173 -0
- package/assets/hooks-handlers/lib/capture-status-cli.js +86 -0
- package/assets/hooks-handlers/lib/capture.js +2782 -403
- package/assets/hooks-handlers/lib/checkpoint-timer.js +485 -81
- package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
- package/assets/hooks-handlers/lib/kiro-handler.js +760 -0
- package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
- package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
- package/assets/hooks-handlers/lib/terminal-handoff.js +427 -0
- package/assets/hooks-handlers/session-end.sh +3 -34
- package/assets/opencode-plugin/meko.js +213 -0
- package/assets/scripts/meko-statusline.sh +123 -16
- package/assets/skills/meko-mcp-tools/SKILL.md +116 -126
- package/assets/skills/meko-mcp-tools/references/_sections.md +3 -3
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +25 -18
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +99 -24
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +53 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +25 -79
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +37 -26
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +66 -1
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +7 -40
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +56 -20
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +15 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +99 -36
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +18 -14
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +39 -28
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
- package/bin/create-meko-setup.mjs +113 -12
- package/lib/capture-cleanup.mjs +63 -0
- package/lib/capture-status.mjs +67 -0
- package/lib/clients/aliases.mjs +5 -0
- package/lib/clients/claude-code.mjs +1 -0
- package/lib/clients/claude-desktop.mjs +120 -5
- package/lib/clients/codex.mjs +85 -37
- package/lib/clients/common.mjs +7 -0
- package/lib/clients/index.mjs +4 -0
- package/lib/clients/kiro.mjs +705 -0
- package/lib/clients/opencode.mjs +532 -0
- package/lib/config.mjs +158 -1
- package/lib/installer.mjs +18 -1
- package/lib/migrate/adapters/claude-code.mjs +11 -2
- package/lib/migrate/adapters/codex.mjs +497 -0
- package/lib/migrate/adapters/memory-md.mjs +27 -8
- package/lib/migrate/adapters/opencode.mjs +148 -0
- package/lib/migrate/claude-parser.mjs +153 -9
- package/lib/migrate/id.mjs +217 -10
- package/lib/migrate/mcp-client.mjs +25 -1
- package/lib/migrate/opencode-normalizer.mjs +132 -0
- package/lib/migrate/orchestrator.mjs +299 -56
- package/lib/migrate/progress.mjs +239 -5
- package/lib/migrate/registry.mjs +72 -0
- package/lib/migrate.mjs +100 -10
- package/lib/prompt.mjs +16 -1
- package/package.json +4 -4
- 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
|
+
};
|