Files
Moltbot/src/auto-reply/opencode.ts
2025-12-05 17:22:53 +00:00

105 lines
3.4 KiB
TypeScript

// 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;
}