95 lines
3.0 KiB
TypeScript
95 lines
3.0 KiB
TypeScript
import { field, isObject } from "#common";
|
|
import { AgentAuthError } from "#profiles";
|
|
|
|
export function openCodeArgs(model: string, prompt: string): string[] {
|
|
return ["run", "--format", "json", "--auto", "--model", model, prompt];
|
|
}
|
|
|
|
type ChunkWriter = (chunk: Uint8Array) => void | Promise<void>;
|
|
|
|
interface OpenCodeOutput {
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
export async function collectOpenCodeOutput(
|
|
stdout: AsyncIterable<Uint8Array>,
|
|
stderr: AsyncIterable<Uint8Array>,
|
|
writeStdout: ChunkWriter,
|
|
writeStderr: ChunkWriter,
|
|
): Promise<OpenCodeOutput> {
|
|
const collect = async (
|
|
stream: AsyncIterable<Uint8Array>,
|
|
write: ChunkWriter,
|
|
): Promise<string> => {
|
|
const chunks: Uint8Array[] = [];
|
|
for await (const chunk of stream) {
|
|
chunks.push(chunk);
|
|
await write(chunk);
|
|
}
|
|
const length = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
const joined = new Uint8Array(length);
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
joined.set(chunk, offset);
|
|
offset += chunk.length;
|
|
}
|
|
return new TextDecoder().decode(joined);
|
|
};
|
|
const [output, error] = await Promise.all([
|
|
collect(stdout, writeStdout),
|
|
collect(stderr, writeStderr),
|
|
]);
|
|
return { stdout: output, stderr: error };
|
|
}
|
|
|
|
// OpenCode reports provider failures, including a rejected OpenRouter key
|
|
// (401 "User not found."), as a JSON error event on stdout and exits 1 with an
|
|
// empty stderr.
|
|
export function openCodeError(output: string): Error | null {
|
|
let failure: Error | null = null;
|
|
for (const line of output.split("\n")) {
|
|
if (line.trim() === "") continue;
|
|
let event: unknown;
|
|
try {
|
|
event = JSON.parse(line);
|
|
} catch {
|
|
continue;
|
|
}
|
|
const error = field(event, "error");
|
|
if (field(event, "type") !== "error" || !isObject(error)) {
|
|
continue;
|
|
}
|
|
const name = field(error, "name");
|
|
const data = field(error, "data");
|
|
const message = field(data, "message");
|
|
const statusCode = field(data, "statusCode");
|
|
const detail = `opencode ${typeof name === "string" ? name : "error"}${
|
|
typeof statusCode === "number" ? ` (API error ${statusCode})` : ""
|
|
}: ${typeof message === "string" ? message : "unknown error"}`;
|
|
failure =
|
|
statusCode === 401 || statusCode === 403
|
|
? new AgentAuthError("opencode sign-in failed", {
|
|
cause: new Error(detail),
|
|
})
|
|
: new Error(detail);
|
|
}
|
|
return failure;
|
|
}
|
|
|
|
export function parseOpenCodeEvents(output: string): string {
|
|
let result: string | null = null;
|
|
for (const line of output.split("\n")) {
|
|
if (line.trim() === "") continue;
|
|
const event: unknown = JSON.parse(line);
|
|
const part = field(event, "part");
|
|
if (field(event, "type") !== "text" || !isObject(part)) continue;
|
|
const text = field(part, "text");
|
|
if (typeof text === "string" && text.trim() !== "") result = text;
|
|
}
|
|
if (result === null) {
|
|
throw openCodeError(output) ?? new Error("OpenCode produced no text output");
|
|
}
|
|
return result;
|
|
}
|