SDK: support working directory and skipGitRepoCheck options (#4563)
Make options not required, add support for working directory and skipGitRepoCheck options on the turn
This commit is contained in:
@@ -1,8 +1,5 @@
|
|||||||
import eslint from '@eslint/js';
|
import eslint from "@eslint/js";
|
||||||
import { defineConfig } from 'eslint/config';
|
import { defineConfig } from "eslint/config";
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(eslint.configs.recommended, tseslint.configs.recommended);
|
||||||
eslint.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class Codex {
|
|||||||
private exec: CodexExec;
|
private exec: CodexExec;
|
||||||
private options: CodexOptions;
|
private options: CodexOptions;
|
||||||
|
|
||||||
constructor(options: CodexOptions) {
|
constructor(options: CodexOptions = {}) {
|
||||||
this.exec = new CodexExec(options.codexPathOverride);
|
this.exec = new CodexExec(options.codexPathOverride);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export type CodexOptions = {
|
|||||||
codexPathOverride?: string;
|
codexPathOverride?: string;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
workingDirectory?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ export type CodexExecArgs = {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
threadId?: string | null;
|
threadId?: string | null;
|
||||||
|
// --model
|
||||||
model?: string;
|
model?: string;
|
||||||
|
// --sandbox
|
||||||
sandboxMode?: SandboxMode;
|
sandboxMode?: SandboxMode;
|
||||||
|
// --cd
|
||||||
|
workingDirectory?: string;
|
||||||
|
// --skip-git-repo-check
|
||||||
|
skipGitRepoCheck?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CodexExec {
|
export class CodexExec {
|
||||||
@@ -33,10 +39,16 @@ export class CodexExec {
|
|||||||
commandArgs.push("--sandbox", 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.threadId) {
|
if (args.threadId) {
|
||||||
commandArgs.push("resume", args.threadId, args.input);
|
commandArgs.push("resume", args.threadId);
|
||||||
} else {
|
|
||||||
commandArgs.push(args.input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
@@ -56,10 +68,24 @@ export class CodexExec {
|
|||||||
let spawnError: unknown | null = null;
|
let spawnError: unknown | null = null;
|
||||||
child.once("error", (err) => (spawnError = err));
|
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) {
|
if (!child.stdout) {
|
||||||
child.kill();
|
child.kill();
|
||||||
throw new Error("Child process has no stdout");
|
throw new Error("Child process has no stdout");
|
||||||
}
|
}
|
||||||
|
const stderrChunks: Buffer[] = [];
|
||||||
|
|
||||||
|
if (child.stderr) {
|
||||||
|
child.stderr.on("data", (data) => {
|
||||||
|
stderrChunks.push(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: child.stdout,
|
input: child.stdout,
|
||||||
@@ -72,12 +98,13 @@ export class CodexExec {
|
|||||||
yield line as string;
|
yield line as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const exitCode = new Promise((resolve) => {
|
const exitCode = new Promise((resolve, reject) => {
|
||||||
child.once("exit", (code) => {
|
child.once("exit", (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve(code);
|
resolve(code);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Codex Exec exited with code ${code}`);
|
const stderrBuffer = Buffer.concat(stderrChunks);
|
||||||
|
reject(new Error(`Codex Exec exited with code ${code}: ${stderrBuffer.toString('utf8')}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export class Thread {
|
|||||||
threadId: this.id,
|
threadId: this.id,
|
||||||
model: options?.model,
|
model: options?.model,
|
||||||
sandboxMode: options?.sandboxMode,
|
sandboxMode: options?.sandboxMode,
|
||||||
|
workingDirectory: options?.workingDirectory,
|
||||||
|
skipGitRepoCheck: options?.skipGitRepoCheck,
|
||||||
});
|
});
|
||||||
for await (const item of generator) {
|
for await (const item of generator) {
|
||||||
const parsed = JSON.parse(item) as ThreadEvent;
|
const parsed = JSON.parse(item) as ThreadEvent;
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"
|
|||||||
export type TurnOptions = {
|
export type TurnOptions = {
|
||||||
model?: string;
|
model?: string;
|
||||||
sandboxMode?: SandboxMode;
|
sandboxMode?: SandboxMode;
|
||||||
|
workingDirectory?: string;
|
||||||
|
skipGitRepoCheck?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const actualChildProcess = jest.requireActual<typeof import("child_process")>("c
|
|||||||
const spawnMock = child_process.spawn as jest.MockedFunction<typeof actualChildProcess.spawn>;
|
const spawnMock = child_process.spawn as jest.MockedFunction<typeof actualChildProcess.spawn>;
|
||||||
|
|
||||||
export function codexExecSpy(): { args: string[][]; restore: () => void } {
|
export function codexExecSpy(): { args: string[][]; restore: () => void } {
|
||||||
const previousImplementation =
|
const previousImplementation = spawnMock.getMockImplementation() ?? actualChildProcess.spawn;
|
||||||
spawnMock.getMockImplementation() ?? actualChildProcess.spawn;
|
|
||||||
const args: string[][] = [];
|
const args: string[][] = [];
|
||||||
|
|
||||||
spawnMock.mockImplementation(((...spawnArgs: Parameters<typeof child_process.spawn>) => {
|
spawnMock.mockImplementation(((...spawnArgs: Parameters<typeof child_process.spawn>) => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { codexExecSpy } from "./codexExecSpy";
|
import { codexExecSpy } from "./codexExecSpy";
|
||||||
@@ -211,14 +213,79 @@ describe("Codex", () => {
|
|||||||
|
|
||||||
expectPair(commandArgs, ["--sandbox", "workspace-write"]);
|
expectPair(commandArgs, ["--sandbox", "workspace-write"]);
|
||||||
expectPair(commandArgs, ["--model", "gpt-test-1"]);
|
expectPair(commandArgs, ["--model", "gpt-test-1"]);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
restore();
|
restore();
|
||||||
await close();
|
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"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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 expect(
|
||||||
|
thread.run("use custom working directory", {
|
||||||
|
workingDirectory,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/Not inside a trusted directory/);
|
||||||
|
} finally {
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function expectPair(args: string[] | undefined, pair: [string, string]) {
|
function expectPair(args: string[] | undefined, pair: [string, string]) {
|
||||||
if (!args) {
|
if (!args) {
|
||||||
|
|||||||
Reference in New Issue
Block a user