Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate images via OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream, and Replicate APIs with batch support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/providers/codex-cli.ts
1import path from "node:path";2import { spawn } from "node:child_process";3import { fileURLToPath } from "node:url";4import { tmpdir } from "node:os";5import { mkdir, readFile, rm, writeFile, access } from "node:fs/promises";6import { randomBytes } from "node:crypto";7import type { CliArgs } from "../types";89const PROVIDER_FILE = fileURLToPath(import.meta.url);10const SCRIPTS_DIR = path.resolve(path.dirname(PROVIDER_FILE), "..");11const BUNDLED_WRAPPER = path.join(SCRIPTS_DIR, "codex-imagegen", "main.ts");1213type WrapperOkResult = {14status: "ok";15path: string;16bytes: number;17elapsed_seconds: number;18thread_id: string | null;19attempts: number;20cached: boolean;21};2223type WrapperErrorResult = {24status: "error";25path: string;26bytes: number;27error: string;28error_kind: string;29};3031type WrapperResult = WrapperOkResult | WrapperErrorResult;3233export function getDefaultModel(): string {34return "codex-image-gen";35}3637export function getDefaultOutputExtension(): string {38return ".png";39}4041export function validateArgs(_model: string, args: CliArgs): void {42if (args.n > 1) {43throw new Error(44"codex-cli provider supports only n=1 (Codex image_gen returns a single image per call).",45);46}47if (args.imageApiDialect && args.imageApiDialect !== "openai-native") {48throw new Error(49`Invalid imageApiDialect for codex-cli: ${args.imageApiDialect}. codex-cli does not use OpenAI Images API dialects.`,50);51}52}5354async function exists(filePath: string): Promise<boolean> {55try {56await access(filePath);57return true;58} catch {59return false;60}61}6263async function resolveWrapperPath(): Promise<string> {64const override = process.env.BAOYU_CODEX_IMAGEGEN_BIN;65if (override) {66if (!(await exists(override))) {67throw new Error(68`Invalid BAOYU_CODEX_IMAGEGEN_BIN: ${override} does not exist.`,69);70}71return override;72}73if (await exists(BUNDLED_WRAPPER)) return BUNDLED_WRAPPER;74throw new Error(75`codex-cli wrapper not found at ${BUNDLED_WRAPPER}. ` +76`Reinstall baoyu-image-gen, or set BAOYU_CODEX_IMAGEGEN_BIN to a codex-imagegen main.ts (or .sh) path.`,77);78}7980type SpawnResult = {81stdout: string;82stderr: string;83code: number;84};8586async function spawnWrapper(wrapperPath: string, cliArgs: string[]): Promise<SpawnResult> {87return new Promise((resolve, reject) => {88const isTs = wrapperPath.endsWith(".ts");89const command = isTs ? "bun" : wrapperPath;90const args = isTs ? [wrapperPath, ...cliArgs] : cliArgs;91const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });92let stdout = "";93let stderr = "";94child.stdout.on("data", (chunk: Buffer) => {95stdout += chunk.toString("utf8");96});97child.stderr.on("data", (chunk: Buffer) => {98const text = chunk.toString("utf8");99stderr += text;100process.stderr.write(text);101});102child.on("error", (err) => reject(err));103child.on("close", (code) => resolve({ stdout, stderr, code: code ?? 1 }));104});105}106107function parseWrapperJson(stdout: string): WrapperResult {108const trimmed = stdout.trim();109if (!trimmed) {110throw new Error("Invalid codex-cli response: empty stdout from wrapper.");111}112const lastLine = trimmed.split(/\r?\n/).pop() ?? trimmed;113try {114return JSON.parse(lastLine) as WrapperResult;115} catch (parseErr) {116throw new Error(117`Invalid codex-cli response: could not parse JSON from wrapper stdout (${(parseErr as Error).message}).`,118);119}120}121122function parsePositiveInt(value: string | undefined): number | null {123if (!value) return null;124const parsed = parseInt(value, 10);125return Number.isFinite(parsed) && parsed > 0 ? parsed : null;126}127128function getEnvOverride(name: string): string | null {129const value = process.env[name];130return value && value.length > 0 ? value : null;131}132133export async function generateImage(134prompt: string,135_model: string,136args: CliArgs,137): Promise<Uint8Array> {138const wrapperPath = await resolveWrapperPath();139140const sessionDir = path.join(tmpdir(), "baoyu-image-gen-codex-cli");141await mkdir(sessionDir, { recursive: true });142const token = randomBytes(8).toString("hex");143const tmpOutput = path.join(sessionDir, `out-${token}.png`);144const tmpPrompt = path.join(sessionDir, `prompt-${token}.md`);145await writeFile(tmpPrompt, prompt, "utf8");146147const aspect = args.aspectRatio ?? "1:1";148const cliArgs: string[] = [149"--image",150tmpOutput,151"--prompt-file",152tmpPrompt,153"--aspect",154aspect,155];156157for (const ref of args.referenceImages) {158cliArgs.push("--ref", path.resolve(ref));159}160161const cacheDir = getEnvOverride("BAOYU_CODEX_IMAGEGEN_CACHE_DIR");162if (cacheDir) cliArgs.push("--cache-dir", cacheDir);163164const timeoutMs = parsePositiveInt(process.env.BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS);165if (timeoutMs) cliArgs.push("--timeout", String(timeoutMs));166167const retries = parsePositiveInt(process.env.BAOYU_CODEX_IMAGEGEN_RETRIES);168if (retries !== null) cliArgs.push("--retries", String(retries));169170const logFile = getEnvOverride("BAOYU_CODEX_IMAGEGEN_LOG_FILE");171if (logFile) cliArgs.push("--log-file", logFile);172173try {174const spawnResult = await spawnWrapper(wrapperPath, cliArgs);175const parsed = parseWrapperJson(spawnResult.stdout);176177if (parsed.status === "error") {178throw new Error(179`Invalid codex-cli result (${parsed.error_kind}): ${parsed.error}`,180);181}182183if (spawnResult.code !== 0) {184throw new Error(185`Invalid codex-cli result: wrapper exited with code ${spawnResult.code} despite reporting status=ok.`,186);187}188189const bytes = await readFile(parsed.path ?? tmpOutput);190return new Uint8Array(bytes);191} finally {192await Promise.allSettled([193rm(tmpOutput, { force: true }),194rm(tmpPrompt, { force: true }),195]);196}197}198