@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,213 @@
1
+ // Managed by meko installer; reinstall rather than editing.
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { randomBytes } from "node:crypto";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url));
9
+ const MEKO_ROOT = path.join(PLUGIN_DIR, "..", "meko");
10
+ const ENV_PATH = path.join(MEKO_ROOT, "opencode-hook-env.json");
11
+ const HANDLER_PATH = path.join(MEKO_ROOT, "hooks-handlers", "lib", "opencode-handler.js");
12
+ const MANAGED_MARKER = "Managed by meko installer";
13
+ function readEnv() {
14
+ try {
15
+ return JSON.parse(fs.readFileSync(ENV_PATH, "utf8"));
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+ // MEKO-635: staged snapshots live under the durable Capture v2 watermark root
21
+ // (matches skills/hooks-handlers/lib/capture-state.js). This keeps them
22
+ // outside installer-owned runtime and lets uninstall preserve them.
23
+ function resolveStagingDir(env) {
24
+ const watermarkDir =
25
+ (typeof env.MEKO_WATERMARK_DIR === "string" && env.MEKO_WATERMARK_DIR) ||
26
+ path.join(os.homedir(), ".claude", "meko-capture");
27
+ return path.join(watermarkDir, "opencode-snapshots");
28
+ }
29
+ function safeSegment(value) {
30
+ return String(value || "unknown").replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 120);
31
+ }
32
+ function sanitizeSnapshot(session, messages) {
33
+ return {
34
+ info: {
35
+ id: session?.id,
36
+ title: session?.title,
37
+ directory: session?.directory,
38
+ parentID: session?.parentID,
39
+ time: session?.time,
40
+ },
41
+ messages: (Array.isArray(messages) ? messages : []).map((message) => ({
42
+ info: {
43
+ id: message?.info?.id,
44
+ role: message?.info?.role,
45
+ type: message?.info?.type,
46
+ order: message?.info?.order,
47
+ synthetic: message?.info?.synthetic,
48
+ compaction: message?.info?.compaction,
49
+ subtask: message?.info?.subtask,
50
+ continuation: message?.info?.continuation,
51
+ time: message?.info?.time,
52
+ createdAt: message?.info?.createdAt,
53
+ timestamp: message?.info?.timestamp,
54
+ },
55
+ parts: (Array.isArray(message?.parts) ? message.parts : [])
56
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
57
+ .map((part) => ({
58
+ type: "text",
59
+ text: part.text,
60
+ synthetic: Boolean(part.synthetic),
61
+ ignored: Boolean(part.ignored),
62
+ })),
63
+ })),
64
+ };
65
+ }
66
+ function stageSnapshot(sessionId, snapshot, terminalError, stagingDir) {
67
+ fs.mkdirSync(stagingDir, { recursive: true, mode: 0o700 });
68
+ // MEKO-635: two events in the same PID and millisecond can race. The 8-byte
69
+ // random suffix makes both the target and the temp path collision-proof so a
70
+ // session.error and a session.idle firing back-to-back cannot overwrite each
71
+ // other (nor let a late pending snapshot clobber one the reconciler is
72
+ // about to read).
73
+ const unique = randomBytes(8).toString("hex");
74
+ const target = path.join(
75
+ stagingDir,
76
+ `${safeSegment(sessionId)}.${process.pid}.${Date.now()}.${unique}.json`,
77
+ );
78
+ const tmp = `${target}.tmp`;
79
+ fs.writeFileSync(
80
+ tmp,
81
+ JSON.stringify({
82
+ schema_version: 1,
83
+ terminalError: Boolean(terminalError),
84
+ export: snapshot,
85
+ }),
86
+ { encoding: "utf8", mode: 0o600 },
87
+ );
88
+ fs.renameSync(tmp, target);
89
+ try { fs.chmodSync(target, 0o600); } catch { /* best effort */ }
90
+ return target;
91
+ }
92
+ function launchHandler(stagedPath, nodeExecutable, spawnProcess) {
93
+ if (!nodeExecutable) {
94
+ throw new Error("MEKO_NODE_EXECUTABLE is missing; reinstall Meko for OpenCode");
95
+ }
96
+ const child = spawnProcess(nodeExecutable, [HANDLER_PATH], {
97
+ detached: false,
98
+ stdio: ["pipe", "ignore", "ignore"],
99
+ env: {
100
+ ...process.env,
101
+ MEKO_OPENCODE_ENV_FILE: ENV_PATH,
102
+ MEKO_HOOK_CLIENT: "opencode",
103
+ },
104
+ });
105
+ child.once?.("error", (err) => {
106
+ console.error(`[meko-opencode] handler launch failed: ${err.message}`);
107
+ });
108
+ child.stdin?.on?.("error", () => {});
109
+ child.stdin?.end?.(JSON.stringify({ snapshotPath: stagedPath }));
110
+ return new Promise((resolve) => {
111
+ if (!child || typeof child.once !== "function") {
112
+ resolve();
113
+ return;
114
+ }
115
+ const done = () => resolve();
116
+ child.once("exit", done);
117
+ child.once("close", done);
118
+ });
119
+ }
120
+ async function captureSession(
121
+ client,
122
+ sessionID,
123
+ terminalError,
124
+ nodeExecutable,
125
+ spawnProcess,
126
+ stagingDir,
127
+ ) {
128
+ if (!sessionID) return;
129
+ let staged = "";
130
+ try {
131
+ const [sessionResponse, messagesResponse] = await Promise.all([
132
+ client.session.get({ path: { id: sessionID } }),
133
+ client.session.messages({ path: { id: sessionID } }),
134
+ ]);
135
+ const session = sessionResponse?.data;
136
+ const messages = messagesResponse?.data;
137
+ if (!session || !Array.isArray(messages)) {
138
+ throw new Error("OpenCode SDK returned an incomplete session snapshot");
139
+ }
140
+ staged = stageSnapshot(
141
+ session.id || sessionID,
142
+ sanitizeSnapshot(session, messages),
143
+ terminalError,
144
+ stagingDir,
145
+ );
146
+ await launchHandler(staged, nodeExecutable, spawnProcess);
147
+ } catch (err) {
148
+ console.error(`[meko-opencode] capture failed: ${err.message}`);
149
+ }
150
+ }
151
+ export default async function MekoPlugin({ client }, options = {}) {
152
+ const runtimeEnv = readEnv();
153
+ const nodeExecutable = runtimeEnv.MEKO_NODE_EXECUTABLE;
154
+ const stagingDir = resolveStagingDir(runtimeEnv);
155
+ const spawnProcess =
156
+ typeof options.__mekoSpawn === "function" ? options.__mekoSpawn : spawn;
157
+ const sessionTails = new Map();
158
+ function enqueue(sessionID, task) {
159
+ const key = sessionID || "_";
160
+ const next = (sessionTails.get(key) || Promise.resolve()).then(task, task);
161
+ sessionTails.set(key, next);
162
+ return next;
163
+ }
164
+ if (process.env.MEKO_OPENCODE_DISABLE_RECOVERY !== "1") {
165
+ try {
166
+ if (!nodeExecutable) {
167
+ throw new Error("MEKO_NODE_EXECUTABLE is missing; reinstall Meko for OpenCode");
168
+ }
169
+ spawnProcess(nodeExecutable, [HANDLER_PATH, "--recovery-scan"], {
170
+ detached: true,
171
+ windowsHide: true,
172
+ stdio: "ignore",
173
+ env: {
174
+ ...process.env,
175
+ MEKO_OPENCODE_ENV_FILE: ENV_PATH,
176
+ MEKO_HOOK_CLIENT: "opencode",
177
+ },
178
+ }).unref();
179
+ } catch (err) {
180
+ console.error(`[meko-opencode] recovery scan failed: ${err.message}`);
181
+ }
182
+ }
183
+
184
+ return {
185
+ async config(config) {
186
+ const env = readEnv();
187
+ const serverName = env.MEKO_MCP_SERVER_NAME || "meko";
188
+ if (!env.MEKO_MCP_URL) return;
189
+ const headers = {
190
+ "User-Agent": env.MEKO_MCP_USER_AGENT || "meko-mcp-installer/1.0",
191
+ };
192
+ if (env.MEKO_API_KEY) headers.Authorization = `Bearer ${env.MEKO_API_KEY}`;
193
+ config.mcp ??= {};
194
+ config.mcp[serverName] = {
195
+ type: "remote",
196
+ url: env.MEKO_MCP_URL,
197
+ enabled: true,
198
+ oauth: false,
199
+ headers,
200
+ };
201
+ },
202
+ async event({ event }) {
203
+ const sessionID = event.properties?.sessionID;
204
+ if (event?.type === "session.idle") {
205
+ await enqueue(sessionID, () =>
206
+ captureSession(client, sessionID, false, nodeExecutable, spawnProcess, stagingDir));
207
+ } else if (event?.type === "session.error") {
208
+ await enqueue(sessionID, () =>
209
+ captureSession(client, sessionID, true, nodeExecutable, spawnProcess, stagingDir));
210
+ }
211
+ },
212
+ };
213
+ }
@@ -46,12 +46,18 @@ fi
46
46
 
47
47
  # --- Derive agent_id from cwd ---
48
48
  #
49
- # Mirrors deriveAgentId() in skills/hooks-handlers/lib/capture.js:51-65:
50
- # - basename of cwd, lowercased
49
+ # Mirrors deriveAgentId() + resolveRepoRoot() in
50
+ # skills/hooks-handlers/lib/capture.js:
51
+ # - walk parents of cwd for a '.git' entry (dir OR file, for worktrees and
52
+ # submodules); the walk stops at $HOME from below, so a git-tracked home
53
+ # does not claim every directory beneath it
54
+ # - basename of that repo root, lowercased
51
55
  # - non-[a-z0-9-]+ → '-'
52
56
  # - trim leading/trailing '-'
53
57
  # - max 64 chars
54
58
  # - prefix with 'claude_code:'
59
+ # - no repo root found → bare 'claude_code' (NOT 'claude_code:<leaf>'), so
60
+ # this segment reads the same pin file the hook writes
55
61
  #
56
62
  # Then mirrors datapackPinSlug() in capture.js:341-348:
57
63
  # - non-[A-Za-z0-9._-]+ → '_'
@@ -74,19 +80,72 @@ if [[ -n "$STDIN_PAYLOAD" ]]; then
74
80
  fi
75
81
  fi
76
82
 
83
+ if [[ -n "${HOME:-}" ]]; then
84
+ USER_HOME="$(cd -L -- "$HOME" 2>/dev/null && pwd -L)"
85
+ else
86
+ USER_HOME="$(cd -L -- ~ 2>/dev/null && pwd -L)"
87
+ fi
88
+
77
89
  # MEKO_AGENT_ID overrides the cwd-derived id, matching resolveSessionAgentId() in
78
90
  # capture.js (the env override wins verbatim when non-empty).
79
91
  if [[ -n "${MEKO_AGENT_ID:-}" ]]; then
80
92
  AGENT_ID="$MEKO_AGENT_ID"
93
+ LEGACY_AGENT_ID="$MEKO_AGENT_ID"
81
94
  else
82
- BASENAME="$(basename "$CWD")"
83
- PROJECT_SLUG="$(printf '%s' "$BASENAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+|-+$//g')"
84
- PROJECT_SLUG="${PROJECT_SLUG:0:64}"
95
+ # Walk up from $CWD for a '.git' entry. $HOME bounds the walk from below.
96
+ REPO_ROOT=""
97
+ # Normalize relative paths and '..' before the textual $HOME comparison,
98
+ # matching path.resolve() for live directories. Unlike JavaScript's lexical
99
+ # resolution, cd/pwd requires the directory to exist; statusline cwd does.
100
+ START_DIR="$(cd -L -- "$CWD" 2>/dev/null && pwd -L)"
101
+ if [[ -z "$START_DIR" ]]; then
102
+ START_DIR="${CWD%/}"
103
+ [[ -z "$START_DIR" ]] && START_DIR="$CWD"
104
+ fi
105
+ WALK_DIR="$START_DIR"
106
+ HOME_BOUND="$USER_HOME"
107
+ while [[ -n "$WALK_DIR" ]]; do
108
+ # A directory below a git-tracked home must not inherit home's .git.
109
+ if [[ -n "$HOME_BOUND" && "$WALK_DIR" == "$HOME_BOUND" && "$WALK_DIR" != "$START_DIR" ]]; then
110
+ break
111
+ fi
112
+ if [[ -e "$WALK_DIR/.git" ]]; then
113
+ REPO_ROOT="$WALK_DIR"
114
+ break
115
+ fi
116
+ # Check home itself for .git, then stop. A non-git home must not inherit
117
+ # repository state from /Users, /home, or /.
118
+ if [[ -n "$HOME_BOUND" && "$WALK_DIR" == "$HOME_BOUND" ]]; then
119
+ break
120
+ fi
121
+ PARENT="$(dirname "$WALK_DIR")"
122
+ [[ "$PARENT" == "$WALK_DIR" ]] && break
123
+ WALK_DIR="$PARENT"
124
+ done
85
125
 
86
- if [[ -z "$PROJECT_SLUG" || "$PROJECT_SLUG" == "-" ]]; then
126
+ if [[ -z "$REPO_ROOT" ]]; then
127
+ # Outside any repo: bare client name, matching deriveAgentId().
87
128
  AGENT_ID="claude_code"
88
129
  else
89
- AGENT_ID="claude_code:$PROJECT_SLUG"
130
+ BASENAME="$(basename "$REPO_ROOT")"
131
+ PROJECT_SLUG="$(printf '%s' "$BASENAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+|-+$//g')"
132
+ PROJECT_SLUG="${PROJECT_SLUG:0:64}"
133
+
134
+ if [[ -z "$PROJECT_SLUG" || "$PROJECT_SLUG" == "-" ]]; then
135
+ AGENT_ID="claude_code"
136
+ else
137
+ AGENT_ID="claude_code:$PROJECT_SLUG"
138
+ fi
139
+ fi
140
+
141
+ # Read-only compatibility alias for pins written before MEKO-590.
142
+ LEGACY_BASENAME="$(basename "$CWD")"
143
+ LEGACY_SLUG="$(printf '%s' "$LEGACY_BASENAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+|-+$//g')"
144
+ LEGACY_SLUG="${LEGACY_SLUG:0:64}"
145
+ if [[ -n "$LEGACY_SLUG" && "$LEGACY_SLUG" != "-" ]]; then
146
+ LEGACY_AGENT_ID="claude_code:$LEGACY_SLUG"
147
+ else
148
+ LEGACY_AGENT_ID="claude_code"
90
149
  fi
91
150
  fi
92
151
 
@@ -96,22 +155,70 @@ PIN_SLUG="$(printf '%s' "$AGENT_ID" | sed -E 's/[^A-Za-z0-9._-]+/_/g; s/^_+|_+$/
96
155
  if [[ -z "$PIN_SLUG" ]]; then
97
156
  PIN_SLUG="meko_agent"
98
157
  fi
99
- PIN_FILE="${MEKO_WATERMARK_DIR:-$HOME/.claude/meko-capture}/pin-${PIN_SLUG}.json"
158
+ PIN_DIR="${MEKO_WATERMARK_DIR:-$USER_HOME/.claude/meko-capture}"
159
+ PIN_FILE="${PIN_DIR}/pin-${PIN_SLUG}.json"
160
+ LEGACY_PIN_SLUG="$(printf '%s' "$LEGACY_AGENT_ID" | sed -E 's/[^A-Za-z0-9._-]+/_/g; s/^_+|_+$//g')"
161
+ [[ -z "$LEGACY_PIN_SLUG" ]] && LEGACY_PIN_SLUG="meko_agent"
162
+ LEGACY_PIN_FILE="${PIN_DIR}/pin-${LEGACY_PIN_SLUG}.json"
100
163
 
101
164
  # --- Read the pin file and append the suffix if a valid pin is present ---
102
165
 
166
+ HAVE_SEGMENT=0
167
+ if [[ -n "${INNER_OUT:-}" ]]; then
168
+ HAVE_SEGMENT=1
169
+ fi
170
+
171
+ PIN_NAME=""
103
172
  if [[ -f "$PIN_FILE" ]]; then
104
173
  # grep -oE isolates the exact "datapack_name":"…" pair before extracting, so a
105
174
  # greedy `.*` can't grab a different key that merely ends in datapack_name.
106
175
  PIN_NAME="$(grep -oE '"datapack_name"[[:space:]]*:[[:space:]]*"[^"]+"' "$PIN_FILE" | sed -E 's/.*"([^"]+)"$/\1/' | head -n 1)"
107
- if [[ -n "$PIN_NAME" ]]; then
108
- SUFFIX="Meko: $PIN_NAME"
109
- # Add a separator only if the inner command produced output.
110
- if [[ -n "${INNER_OUT:-}" ]]; then
111
- printf ' \xe2\x80\xa2 %s' "$SUFFIX"
112
- else
113
- printf '%s' "$SUFFIX"
114
- fi
176
+ fi
177
+ if [[ -z "$PIN_NAME" && "$LEGACY_PIN_FILE" != "$PIN_FILE" && -f "$LEGACY_PIN_FILE" ]]; then
178
+ PIN_NAME="$(grep -oE '"datapack_name"[[:space:]]*:[[:space:]]*"[^"]+"' "$LEGACY_PIN_FILE" | sed -E 's/.*"([^"]+)"$/\1/' | head -n 1)"
179
+ fi
180
+ if [[ -n "$PIN_NAME" ]]; then
181
+ SUFFIX="Meko: $PIN_NAME"
182
+ # Add a separator only if the inner command produced output.
183
+ if [[ "$HAVE_SEGMENT" -eq 1 ]]; then
184
+ printf ' \xe2\x80\xa2 %s' "$SUFFIX"
185
+ else
186
+ printf '%s' "$SUFFIX"
187
+ fi
188
+ HAVE_SEGMENT=1
189
+ fi
190
+
191
+ # --- Capture health suffix (Capture V2) ---
192
+ #
193
+ # Composable: appends only for catching_up / degraded / blocked. Healthy adds
194
+ # nothing. Reads rebuildable capture-health.json (no full watermark scan).
195
+ # Format mirrors statuslineSuffixFromHealth() in capture-notices.js.
196
+
197
+ WATERMARK_DIR="$PIN_DIR"
198
+ HEALTH_FILE="${WATERMARK_DIR}/capture-health.json"
199
+ CAPTURE_SUFFIX=""
200
+ if [[ -f "$HEALTH_FILE" ]]; then
201
+ HEALTH_STATUS="$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]+"' "$HEALTH_FILE" | sed -E 's/.*"([^"]+)"$/\1/' | head -n 1)"
202
+ QUEUED="$(grep -oE '"queued_exchanges"[[:space:]]*:[[:space:]]*[0-9]+' "$HEALTH_FILE" | sed -E 's/.*:[[:space:]]*([0-9]+)$/\1/' | head -n 1)"
203
+ QUEUED="${QUEUED:-0}"
204
+ case "$HEALTH_STATUS" in
205
+ catching_up)
206
+ CAPTURE_SUFFIX="capture:catching_up(${QUEUED})"
207
+ ;;
208
+ degraded_retrying)
209
+ CAPTURE_SUFFIX="capture:degraded(${QUEUED})"
210
+ ;;
211
+ blocked_action_required)
212
+ CAPTURE_SUFFIX="capture:blocked(${QUEUED})"
213
+ ;;
214
+ esac
215
+ fi
216
+
217
+ if [[ -n "$CAPTURE_SUFFIX" ]]; then
218
+ if [[ "$HAVE_SEGMENT" -eq 1 ]]; then
219
+ printf ' \xe2\x80\xa2 %s' "$CAPTURE_SUFFIX"
220
+ else
221
+ printf '%s' "$CAPTURE_SUFFIX"
115
222
  fi
116
223
  fi
117
224