UNPKG

@openai/codex-sdk

Version:

TypeScript SDK for Codex APIs.

321 lines (318 loc) 9.01 kB
// src/outputSchemaFile.ts import { promises as fs } from "fs"; import os from "os"; import path from "path"; async function createOutputSchemaFile(schema) { if (schema === void 0) { return { cleanup: async () => { } }; } if (!isJsonObject(schema)) { throw new Error("outputSchema must be a plain JSON object"); } const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-")); const schemaPath = path.join(schemaDir, "schema.json"); const cleanup = async () => { try { await fs.rm(schemaDir, { recursive: true, force: true }); } catch { } }; try { await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8"); return { schemaPath, cleanup }; } catch (error) { await cleanup(); throw error; } } function isJsonObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } // src/thread.ts var Thread = class { _exec; _options; _id; _threadOptions; /** Returns the ID of the thread. Populated after the first turn starts. */ get id() { return this._id; } /* @internal */ constructor(exec, options, threadOptions, id = null) { this._exec = exec; this._options = options; this._id = id; this._threadOptions = threadOptions; } /** Provides the input to the agent and streams events as they are produced during the turn. */ async runStreamed(input, turnOptions = {}) { return { events: this.runStreamedInternal(input, turnOptions) }; } async *runStreamedInternal(input, turnOptions = {}) { const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema); const options = this._threadOptions; const { prompt, images } = normalizeInput(input); const generator = this._exec.run({ input: prompt, baseUrl: this._options.baseUrl, apiKey: this._options.apiKey, threadId: this._id, images, model: options?.model, sandboxMode: options?.sandboxMode, workingDirectory: options?.workingDirectory, skipGitRepoCheck: options?.skipGitRepoCheck, outputSchemaFile: schemaPath, modelReasoningEffort: options?.modelReasoningEffort }); try { for await (const item of generator) { let parsed; try { parsed = JSON.parse(item); } catch (error) { throw new Error(`Failed to parse item: ${item}`, { cause: error }); } if (parsed.type === "thread.started") { this._id = parsed.thread_id; } yield parsed; } } finally { await cleanup(); } } /** Provides the input to the agent and returns the completed turn. */ async run(input, turnOptions = {}) { const generator = this.runStreamedInternal(input, turnOptions); const items = []; let finalResponse = ""; let usage = null; let turnFailure = null; for await (const event of generator) { if (event.type === "item.completed") { if (event.item.type === "agent_message") { finalResponse = event.item.text; } items.push(event.item); } else if (event.type === "turn.completed") { usage = event.usage; } else if (event.type === "turn.failed") { turnFailure = event.error; break; } } if (turnFailure) { throw new Error(turnFailure.message); } return { items, finalResponse, usage }; } }; function normalizeInput(input) { if (typeof input === "string") { return { prompt: input, images: [] }; } const promptParts = []; const images = []; for (const item of input) { if (item.type === "text") { promptParts.push(item.text); } else if (item.type === "local_image") { images.push(item.path); } } return { prompt: promptParts.join("\n\n"), images }; } // src/exec.ts import { spawn } from "child_process"; import path2 from "path"; import readline from "readline"; import { fileURLToPath } from "url"; var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"; var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts"; var CodexExec = class { executablePath; constructor(executablePath = null) { this.executablePath = executablePath || findCodexPath(); } async *run(args) { const commandArgs = ["exec", "--experimental-json"]; if (args.model) { commandArgs.push("--model", args.model); } if (args.sandboxMode) { commandArgs.push("--sandbox", args.sandboxMode); } if (args.workingDirectory) { commandArgs.push("--cd", args.workingDirectory); } if (args.skipGitRepoCheck) { commandArgs.push("--skip-git-repo-check"); } if (args.outputSchemaFile) { commandArgs.push("--output-schema", args.outputSchemaFile); } if (args.modelReasoningEffort) { commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`); } if (args.images?.length) { for (const image of args.images) { commandArgs.push("--image", image); } } if (args.threadId) { commandArgs.push("resume", args.threadId); } const env = { ...process.env }; if (!env[INTERNAL_ORIGINATOR_ENV]) { env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR; } if (args.baseUrl) { env.OPENAI_BASE_URL = args.baseUrl; } if (args.apiKey) { env.CODEX_API_KEY = args.apiKey; } const child = spawn(this.executablePath, commandArgs, { env }); let spawnError = null; child.once("error", (err) => spawnError = err); if (!child.stdin) { child.kill(); throw new Error("Child process has no stdin"); } child.stdin.write(args.input); child.stdin.end(); if (!child.stdout) { child.kill(); throw new Error("Child process has no stdout"); } const stderrChunks = []; if (child.stderr) { child.stderr.on("data", (data) => { stderrChunks.push(data); }); } const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity }); try { for await (const line of rl) { yield line; } const exitCode = new Promise((resolve, reject) => { child.once("exit", (code) => { if (code === 0) { resolve(code); } else { const stderrBuffer = Buffer.concat(stderrChunks); reject( new Error(`Codex Exec exited with code ${code}: ${stderrBuffer.toString("utf8")}`) ); } }); }); if (spawnError) throw spawnError; await exitCode; } finally { rl.close(); child.removeAllListeners(); try { if (!child.killed) child.kill(); } catch { } } } }; var scriptFileName = fileURLToPath(import.meta.url); var scriptDirName = path2.dirname(scriptFileName); function findCodexPath() { const { platform, arch } = process; let targetTriple = null; switch (platform) { case "linux": case "android": switch (arch) { case "x64": targetTriple = "x86_64-unknown-linux-musl"; break; case "arm64": targetTriple = "aarch64-unknown-linux-musl"; break; default: break; } break; case "darwin": switch (arch) { case "x64": targetTriple = "x86_64-apple-darwin"; break; case "arm64": targetTriple = "aarch64-apple-darwin"; break; default: break; } break; case "win32": switch (arch) { case "x64": targetTriple = "x86_64-pc-windows-msvc"; break; case "arm64": targetTriple = "aarch64-pc-windows-msvc"; break; default: break; } break; default: break; } if (!targetTriple) { throw new Error(`Unsupported platform: ${platform} (${arch})`); } const vendorRoot = path2.join(scriptDirName, "..", "vendor"); const archRoot = path2.join(vendorRoot, targetTriple); const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex"; const binaryPath = path2.join(archRoot, "codex", codexBinaryName); return binaryPath; } // src/codex.ts var Codex = class { exec; options; constructor(options = {}) { this.exec = new CodexExec(options.codexPathOverride); this.options = options; } /** * Starts a new conversation with an agent. * @returns A new thread instance. */ startThread(options = {}) { return new Thread(this.exec, this.options, options); } /** * Resumes a conversation with an agent based on the thread id. * Threads are persisted in ~/.codex/sessions. * * @param id The id of the thread to resume. * @returns A new thread instance. */ resumeThread(id, options = {}) { return new Thread(this.exec, this.options, options, id); } }; export { Codex, Thread }; //# sourceMappingURL=index.js.map