Skip to content

Commit 63ed44e

Browse files
authored
Allow Codex Ask AI outside git repos (#965)
1 parent e138c82 commit 63ed44e

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

packages/ai/ai.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,40 @@ describe("resolveSDKModel", () => {
878878
// Codex SDK event mapping
879879
// ---------------------------------------------------------------------------
880880

881-
import { mapCodexEvent, mapCodexItem } from "./providers/codex-sdk.ts";
881+
import {
882+
mapCodexEvent,
883+
mapCodexItem,
884+
shouldSkipGitRepoCheck,
885+
} from "./providers/codex-sdk.ts";
886+
887+
describe("shouldSkipGitRepoCheck", () => {
888+
test("keeps the Codex git repo check inside a worktree", async () => {
889+
const probe = async (command: string, args: string[], options: { encoding: "utf8" }) => {
890+
expect(command).toBe("git");
891+
expect(args).toEqual(["-C", "/repo", "rev-parse", "--is-inside-work-tree"]);
892+
expect(options).toEqual({ encoding: "utf8" });
893+
return { stdout: "true\n" };
894+
};
895+
896+
expect(await shouldSkipGitRepoCheck("/repo", probe)).toBe(false);
897+
});
898+
899+
test("skips the Codex git repo check for standalone document sessions", async () => {
900+
const probe = async () => {
901+
throw new Error("not a git repo");
902+
};
903+
904+
expect(await shouldSkipGitRepoCheck("/tmp/plain-session", probe)).toBe(true);
905+
});
906+
907+
test("skips the Codex git repo check when the probe cannot run", async () => {
908+
const probe = async () => {
909+
throw new Error("git unavailable");
910+
};
911+
912+
expect(await shouldSkipGitRepoCheck("/tmp/plain-session", probe)).toBe(true);
913+
});
914+
});
882915

883916
describe("mapCodexEvent", () => {
884917
function offsets() {

packages/ai/providers/codex-sdk.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import { buildSystemPrompt, buildEffectivePrompt } from "../context.ts";
1414
import { BaseSession } from "../base-session.ts";
15+
import { execFile } from "node:child_process";
16+
import { promisify } from "node:util";
1517
import type {
1618
AIProvider,
1719
AIProviderCapabilities,
@@ -28,6 +30,30 @@ import type {
2830
const PROVIDER_NAME = "codex-sdk";
2931
const DEFAULT_MODEL = "gpt-5.4";
3032

33+
type GitWorkTreeProbe = (
34+
command: string,
35+
args: string[],
36+
options: { encoding: "utf8" },
37+
) => Promise<{ stdout?: string | Buffer }>;
38+
39+
const execFileAsync = promisify(execFile);
40+
41+
export async function shouldSkipGitRepoCheck(
42+
cwd: string,
43+
probe: GitWorkTreeProbe = execFileAsync,
44+
): Promise<boolean> {
45+
try {
46+
const result = probe(
47+
"git",
48+
["-C", cwd, "rev-parse", "--is-inside-work-tree"],
49+
{ encoding: "utf8" },
50+
);
51+
return String((await result).stdout ?? "").trim() !== "true";
52+
} catch {
53+
return true;
54+
}
55+
}
56+
3157
// ---------------------------------------------------------------------------
3258
// Provider
3359
// ---------------------------------------------------------------------------
@@ -57,10 +83,13 @@ export class CodexSDKProvider implements AIProvider {
5783
}
5884

5985
async createSession(options: CreateSessionOptions): Promise<AISession> {
86+
const cwd = options.cwd ?? this.config.cwd ?? process.cwd();
87+
const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd);
6088
return new CodexSDKSession({
6189
...this.baseConfig(options),
6290
systemPrompt: buildSystemPrompt(options.context),
63-
cwd: options.cwd ?? this.config.cwd ?? process.cwd(),
91+
cwd,
92+
skipGitRepoCheck,
6493
parentSessionId: null,
6594
});
6695
}
@@ -73,10 +102,13 @@ export class CodexSDKProvider implements AIProvider {
73102
}
74103

75104
async resumeSession(sessionId: string): Promise<AISession> {
105+
const cwd = this.config.cwd ?? process.cwd();
106+
const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd);
76107
return new CodexSDKSession({
77108
...this.baseConfig(),
78109
systemPrompt: null,
79-
cwd: this.config.cwd ?? process.cwd(),
110+
cwd,
111+
skipGitRepoCheck,
80112
parentSessionId: null,
81113
resumeThreadId: sessionId,
82114
});
@@ -123,6 +155,7 @@ interface SessionConfig {
123155
maxTurns: number;
124156
sandboxMode: "read-only" | "workspace-write" | "danger-full-access";
125157
cwd: string;
158+
skipGitRepoCheck: boolean;
126159
parentSessionId: string | null;
127160
resumeThreadId?: string;
128161
codexExecutablePath?: string;
@@ -173,13 +206,15 @@ class CodexSDKSession extends BaseSession {
173206
this._thread = this._codexInstance.resumeThread(this.config.resumeThreadId, {
174207
model: this.config.model,
175208
workingDirectory: this.config.cwd,
209+
skipGitRepoCheck: this.config.skipGitRepoCheck,
176210
sandboxMode: this.config.sandboxMode,
177211
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
178212
});
179213
} else {
180214
this._thread = this._codexInstance.startThread({
181215
model: this.config.model,
182216
workingDirectory: this.config.cwd,
217+
skipGitRepoCheck: this.config.skipGitRepoCheck,
183218
sandboxMode: this.config.sandboxMode,
184219
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
185220
});

0 commit comments

Comments
 (0)