// Parse the JSON a CLI printed on stdout, scanning lines LAST-TO-FIRST. // // Lifted from checkAvailability.ts (where it read yt-dlp --dump-json) because // the digest registry needs exactly the same tolerance for `claude -p // --output-format json`: both tools may print warnings, deprecation notices, or // progress to stdout BEFORE the payload, so the JSON is the last parseable line, // not the first. Reading forward finds the warning; reading backward finds the // answer. export function parseStdoutJson(stdout: string): unknown | null { const lines = stdout .split("\n") .map((l) => l.trim()) .filter(Boolean); for (let i = lines.length - 1; i >= 0; i--) { try { return JSON.parse(lines[i]); } catch { continue; } } // Fall back to the whole buffer: `--output-format json` pretty-prints across // several lines, so no single line parses on its own. try { return JSON.parse(stdout); } catch { return null; } } // Pull a JSON object out of model prose. A CLI lane has no constrained decoding, // so the body may arrive fenced (```json … ```) or with a sentence in front of // it. Tries the whole string, then the fenced block, then the outermost braces. export function extractJsonObject(text: string): unknown | null { const trimmed = text.trim(); try { return JSON.parse(trimmed); } catch { // fall through } const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/); if (fence) { try { return JSON.parse(fence[1].trim()); } catch { // fall through } } const first = trimmed.indexOf("{"); const last = trimmed.lastIndexOf("}"); if (first >= 0 && last > first) { try { return JSON.parse(trimmed.slice(first, last + 1)); } catch { return null; } } return null; }