From 9866bf1276989ce14c7f79aba6c44001c1eeab91 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 1 Oct 2025 10:37:07 -0700 Subject: [PATCH 1/4] SDK changes --- sdk/typescript/src/codex.ts | 2 +- sdk/typescript/src/codexOptions.ts | 1 + sdk/typescript/src/exec.ts | 23 ++++++++- sdk/typescript/src/thread.ts | 2 + sdk/typescript/src/turnOptions.ts | 2 + sdk/typescript/tests/run.test.ts | 78 ++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/codex.ts b/sdk/typescript/src/codex.ts index bb4b713550..331b7e6e3f 100644 --- a/sdk/typescript/src/codex.ts +++ b/sdk/typescript/src/codex.ts @@ -6,7 +6,7 @@ export class Codex { private exec: CodexExec; private options: CodexOptions; - constructor(options: CodexOptions) { + constructor(options: CodexOptions = {}) { this.exec = new CodexExec(options.codexPathOverride); this.options = options; } diff --git a/sdk/typescript/src/codexOptions.ts b/sdk/typescript/src/codexOptions.ts index 2d22bcf227..149e61717a 100644 --- a/sdk/typescript/src/codexOptions.ts +++ b/sdk/typescript/src/codexOptions.ts @@ -2,4 +2,5 @@ export type CodexOptions = { codexPathOverride?: string; baseUrl?: string; apiKey?: string; + workingDirectory?: string; }; diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index 6ae5b441a7..2467844d24 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -12,8 +12,14 @@ export type CodexExecArgs = { baseUrl?: string; apiKey?: string; threadId?: string | null; + // --model model?: string; + // --sandbox sandboxMode?: SandboxMode; + // --cd + workingDirectory?: string; + // --skip-git-repo-check + skipGitRepoCheck?: boolean; }; export class CodexExec { @@ -33,6 +39,14 @@ export class CodexExec { commandArgs.push("--sandbox", args.sandboxMode); } + if (args.workingDirectory) { + commandArgs.push("--cd", args.workingDirectory); + } + + if (args.skipGitRepoCheck) { + commandArgs.push("--skip-git-repo-check"); + } + if (args.threadId) { commandArgs.push("resume", args.threadId, args.input); } else { @@ -61,6 +75,13 @@ export class CodexExec { throw new Error("Child process has no stdout"); } + let stderr: string = ""; + if (child.stderr) { + child.stderr.on("data", (data) => { + stderr += data; + }); + } + const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity, @@ -77,7 +98,7 @@ export class CodexExec { if (code === 0) { resolve(code); } else { - throw new Error(`Codex Exec exited with code ${code}`); + throw new Error(`Codex Exec exited with code ${code}: ${stderr}`); } }); }); diff --git a/sdk/typescript/src/thread.ts b/sdk/typescript/src/thread.ts index 3e380e0aea..de2c2c94dc 100644 --- a/sdk/typescript/src/thread.ts +++ b/sdk/typescript/src/thread.ts @@ -41,6 +41,8 @@ export class Thread { threadId: this.id, model: options?.model, sandboxMode: options?.sandboxMode, + workingDirectory: options?.workingDirectory, + skipGitRepoCheck: options?.skipGitRepoCheck, }); for await (const item of generator) { const parsed = JSON.parse(item) as ThreadEvent; diff --git a/sdk/typescript/src/turnOptions.ts b/sdk/typescript/src/turnOptions.ts index c414334d19..ce56d7b2c6 100644 --- a/sdk/typescript/src/turnOptions.ts +++ b/sdk/typescript/src/turnOptions.ts @@ -5,4 +5,6 @@ export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access" export type TurnOptions = { model?: string; sandboxMode?: SandboxMode; + workingDirectory?: string; + skipGitRepoCheck?: boolean; }; diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 8c7dff7725..1919871a02 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -1,9 +1,12 @@ +import fs from "fs"; +import os from "os"; import path from "path"; import { codexExecSpy } from "./codexExecSpy"; import { describe, expect, it } from "@jest/globals"; import { Codex } from "../src/codex"; +import { CodexExec } from "../src/exec"; import { assistantMessage, @@ -217,6 +220,81 @@ describe("Codex", () => { await close(); } }); + + it("runs in provided working directory", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("Working directory applied", "item_1"), + responseCompleted("response_1"), + ), + ], + }); + + const { args: spawnArgs, restore } = codexExecSpy(); + + try { + const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-working-dir-")); + const client = new Codex({ + codexPathOverride: codexExecPath, + baseUrl: url, + apiKey: "test", + }); + + const thread = client.startThread(); + await thread.run("use custom working directory", { + workingDirectory, + skipGitRepoCheck: true, + }); + + + const commandArgs = spawnArgs[0]; + expectPair(commandArgs, ["--cd", workingDirectory]); + + } finally { + restore(); + await close(); + } + }); + + + it("throws if working directory is not git and no skipGitRepoCheck is provided", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("Working directory applied", "item_1"), + responseCompleted("response_1"), + ), + ], + }); + + + const { restore } = codexExecSpy(); + try { + const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-working-dir-")); + const client = new Codex({ + codexPathOverride: codexExecPath, + baseUrl: url, + apiKey: "test", + }); + + const thread = client.startThread(); + //expect(async () => { + await thread.run("use custom working directory", { + workingDirectory, + }); + // }).toThrow(/Codex Exec exited with code 1: Not inside a trusted directory/); + + + } finally { + restore(); + await close(); + } + }); }); From 42aa69570f82ca94c6e9d2bad5d531362fa82b65 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 1 Oct 2025 10:43:22 -0700 Subject: [PATCH 2/4] exec: fix child process exit handling to reject promise on failure Change exitCode promise to reject instead of throwing error when child process exits with non-zero code. Update run.test.ts to properly test error handling and remove unnecessary restore call. --- sdk/typescript/src/exec.ts | 4 ++-- sdk/typescript/tests/run.test.ts | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index 2467844d24..9d30fe4a12 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -93,12 +93,12 @@ export class CodexExec { yield line as string; } - const exitCode = new Promise((resolve) => { + const exitCode = new Promise((resolve, reject) => { child.once("exit", (code) => { if (code === 0) { resolve(code); } else { - throw new Error(`Codex Exec exited with code ${code}: ${stderr}`); + reject(new Error(`Codex Exec exited with code ${code}: ${stderr}`)); } }); }); diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 1919871a02..4e606e4324 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -272,8 +272,6 @@ describe("Codex", () => { ], }); - - const { restore } = codexExecSpy(); try { const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-working-dir-")); const client = new Codex({ @@ -283,15 +281,13 @@ describe("Codex", () => { }); const thread = client.startThread(); - //expect(async () => { - await thread.run("use custom working directory", { + await expect( + thread.run("use custom working directory", { workingDirectory, - }); - // }).toThrow(/Codex Exec exited with code 1: Not inside a trusted directory/); + }), + ).rejects.toThrow(/Not inside a trusted directory/); - } finally { - restore(); await close(); } }); From 7073a9a10763384792c3f147adb72074f242f7c6 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 1 Oct 2025 10:50:18 -0700 Subject: [PATCH 3/4] typescript: clean up formatting, remove unused imports, and fix whitespace --- sdk/typescript/eslint.config.js | 11 ++++------- sdk/typescript/src/exec.ts | 2 +- sdk/typescript/tests/codexExecSpy.ts | 3 +-- sdk/typescript/tests/run.test.ts | 9 +-------- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/sdk/typescript/eslint.config.js b/sdk/typescript/eslint.config.js index c2fb27d2df..accb022be1 100644 --- a/sdk/typescript/eslint.config.js +++ b/sdk/typescript/eslint.config.js @@ -1,8 +1,5 @@ -import eslint from '@eslint/js'; -import { defineConfig } from 'eslint/config'; -import tseslint from 'typescript-eslint'; +import eslint from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; -export default defineConfig( - eslint.configs.recommended, - tseslint.configs.recommended, -); +export default defineConfig(eslint.configs.recommended, tseslint.configs.recommended); diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index 9d30fe4a12..84b20954e2 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -94,7 +94,7 @@ export class CodexExec { } const exitCode = new Promise((resolve, reject) => { - child.once("exit", (code) => { + child.once("exit", (code) => { if (code === 0) { resolve(code); } else { diff --git a/sdk/typescript/tests/codexExecSpy.ts b/sdk/typescript/tests/codexExecSpy.ts index 9028b6e6e5..daf8123cb7 100644 --- a/sdk/typescript/tests/codexExecSpy.ts +++ b/sdk/typescript/tests/codexExecSpy.ts @@ -9,8 +9,7 @@ const actualChildProcess = jest.requireActual("c const spawnMock = child_process.spawn as jest.MockedFunction; export function codexExecSpy(): { args: string[][]; restore: () => void } { - const previousImplementation = - spawnMock.getMockImplementation() ?? actualChildProcess.spawn; + const previousImplementation = spawnMock.getMockImplementation() ?? actualChildProcess.spawn; const args: string[][] = []; spawnMock.mockImplementation(((...spawnArgs: Parameters) => { diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 4e606e4324..7c243b8e79 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -6,7 +6,6 @@ import { codexExecSpy } from "./codexExecSpy"; import { describe, expect, it } from "@jest/globals"; import { Codex } from "../src/codex"; -import { CodexExec } from "../src/exec"; import { assistantMessage, @@ -214,7 +213,6 @@ describe("Codex", () => { expectPair(commandArgs, ["--sandbox", "workspace-write"]); expectPair(commandArgs, ["--model", "gpt-test-1"]); - } finally { restore(); await close(); @@ -249,17 +247,14 @@ describe("Codex", () => { skipGitRepoCheck: true, }); - const commandArgs = spawnArgs[0]; expectPair(commandArgs, ["--cd", workingDirectory]); - } finally { restore(); await close(); } }); - it("throws if working directory is not git and no skipGitRepoCheck is provided", async () => { const { url, close } = await startResponsesTestProxy({ statusCode: 200, @@ -286,14 +281,12 @@ describe("Codex", () => { workingDirectory, }), ).rejects.toThrow(/Not inside a trusted directory/); - } finally { await close(); } - }); + }); }); - function expectPair(args: string[] | undefined, pair: [string, string]) { if (!args) { throw new Error("Args is undefined"); From 18d54210256f25796c59e6f32a998274e9608c49 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 1 Oct 2025 11:07:25 -0700 Subject: [PATCH 4/4] exec.ts: refactor input and stderr handling in CodexExec - Move user input to stdin for both normal and resume execution - Refine argument construction for threadId resume - Capture stderr as Buffer chunks and improve error reporting - Add error handling for missing stdin --- sdk/typescript/src/exec.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index 84b20954e2..d11c2a1646 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -48,10 +48,8 @@ export class CodexExec { } if (args.threadId) { - commandArgs.push("resume", args.threadId, args.input); - } else { - commandArgs.push(args.input); - } + commandArgs.push("resume", args.threadId); + } const env = { ...process.env, @@ -69,16 +67,23 @@ export class CodexExec { let spawnError: unknown | null = 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: Buffer[] = []; - let stderr: string = ""; if (child.stderr) { child.stderr.on("data", (data) => { - stderr += data; + stderrChunks.push(data); }); } @@ -98,7 +103,8 @@ export class CodexExec { if (code === 0) { resolve(code); } else { - reject(new Error(`Codex Exec exited with code ${code}: ${stderr}`)); + const stderrBuffer = Buffer.concat(stderrChunks); + reject(new Error(`Codex Exec exited with code ${code}: ${stderrBuffer.toString('utf8')}`)); } }); });