// Helpers specific to Opencode CLI output/argv handling. // Preferred binary name for Opencode CLI invocations. export const OPENCODE_BIN = "opencode"; export const OPENCODE_IDENTITY_PREFIX = "You are Openclawd running on the user's Mac via clawdis. Your scratchpad is /Users/steipete/openclawd; this is your folder and you can add what you like in markdown files and/or images. You don't need to be concise, but WhatsApp replies must stay under ~1500 characters. Media you can send: images ≤6MB, audio/video ≤16MB, documents ≤100MB. The prompt may include a media path and an optional Transcript: section—use them when present. If a prompt is a heartbeat poll and nothing needs attention, reply with exactly HEARTBEAT_OK and nothing else; for any alert, do not include HEARTBEAT_OK."; export type OpencodeJsonParseResult = { text?: string; parsed: unknown[]; valid: boolean; meta?: { durationMs?: number; cost?: number; tokens?: { input?: number; output?: number; }; }; }; export function parseOpencodeJson(raw: string): OpencodeJsonParseResult { const lines = raw.split(/\n+/).filter((s) => s.trim()); const parsed: unknown[] = []; let text = ""; let valid = false; let startTime: number | undefined; let endTime: number | undefined; let cost = 0; let inputTokens = 0; let outputTokens = 0; for (const line of lines) { try { const event = JSON.parse(line); parsed.push(event); if (event && typeof event === "object") { // Opencode emits a stream of events. if (event.type === "step_start") { valid = true; if (typeof event.timestamp === "number") { if (startTime === undefined || event.timestamp < startTime) { startTime = event.timestamp; } } } if (event.type === "text" && event.part?.text) { text += event.part.text; valid = true; } if (event.type === "step_finish") { valid = true; if (typeof event.timestamp === "number") { endTime = event.timestamp; } if (event.part) { if (typeof event.part.cost === "number") { cost += event.part.cost; } if (event.part.tokens) { inputTokens += event.part.tokens.input || 0; outputTokens += event.part.tokens.output || 0; } } } } } catch { // ignore non-JSON lines } } const meta: OpencodeJsonParseResult["meta"] = {}; if (startTime !== undefined && endTime !== undefined) { meta.durationMs = endTime - startTime; } if (cost > 0) meta.cost = cost; if (inputTokens > 0 || outputTokens > 0) { meta.tokens = { input: inputTokens, output: outputTokens }; } return { text: text || undefined, parsed, valid: valid && parsed.length > 0, meta: Object.keys(meta).length > 0 ? meta : undefined, }; } export function summarizeOpencodeMetadata( meta: OpencodeJsonParseResult["meta"], ): string | undefined { if (!meta) return undefined; const parts: string[] = []; if (meta.durationMs !== undefined) parts.push(`duration=${meta.durationMs}ms`); if (meta.cost !== undefined) parts.push(`cost=$${meta.cost.toFixed(4)}`); if (meta.tokens) { parts.push(`tokens=${meta.tokens.input}+${meta.tokens.output}`); } return parts.length ? parts.join(", ") : undefined; }