Skip to content

Commit de636ed

Browse files
committed
Allow Codex Ask AI outside git repos
1 parent e138c82 commit de636ed

2 files changed

Lines changed: 64 additions & 3 deletions

File tree

packages/ai/ai.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,38 @@ 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", () => {
889+
const probe = (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 { status: 0, stdout: "true\n" };
894+
};
895+
896+
expect(shouldSkipGitRepoCheck("/repo", probe)).toBe(false);
897+
});
898+
899+
test("skips the Codex git repo check for standalone document sessions", () => {
900+
const probe = () => ({ status: 128, stdout: "" });
901+
902+
expect(shouldSkipGitRepoCheck("/tmp/plain-session", probe)).toBe(true);
903+
});
904+
905+
test("skips the Codex git repo check when the probe cannot run", () => {
906+
const probe = () => {
907+
throw new Error("git unavailable");
908+
};
909+
910+
expect(shouldSkipGitRepoCheck("/tmp/plain-session", probe)).toBe(true);
911+
});
912+
});
882913

883914
describe("mapCodexEvent", () => {
884915
function offsets() {

packages/ai/providers/codex-sdk.ts

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

1313
import { buildSystemPrompt, buildEffectivePrompt } from "../context.ts";
1414
import { BaseSession } from "../base-session.ts";
15+
import { spawnSync } from "node:child_process";
1516
import type {
1617
AIProvider,
1718
AIProviderCapabilities,
@@ -28,6 +29,28 @@ import type {
2829
const PROVIDER_NAME = "codex-sdk";
2930
const DEFAULT_MODEL = "gpt-5.4";
3031

32+
type GitWorkTreeProbe = (
33+
command: string,
34+
args: string[],
35+
options: { encoding: "utf8" },
36+
) => { status: number | null; stdout?: string | Buffer };
37+
38+
export function shouldSkipGitRepoCheck(
39+
cwd: string,
40+
probe: GitWorkTreeProbe = spawnSync,
41+
): boolean {
42+
try {
43+
const result = probe(
44+
"git",
45+
["-C", cwd, "rev-parse", "--is-inside-work-tree"],
46+
{ encoding: "utf8" },
47+
);
48+
return result.status !== 0 || String(result.stdout ?? "").trim() !== "true";
49+
} catch {
50+
return true;
51+
}
52+
}
53+
3154
// ---------------------------------------------------------------------------
3255
// Provider
3356
// ---------------------------------------------------------------------------
@@ -57,10 +80,12 @@ export class CodexSDKProvider implements AIProvider {
5780
}
5881

5982
async createSession(options: CreateSessionOptions): Promise<AISession> {
83+
const cwd = options.cwd ?? this.config.cwd ?? process.cwd();
6084
return new CodexSDKSession({
6185
...this.baseConfig(options),
6286
systemPrompt: buildSystemPrompt(options.context),
63-
cwd: options.cwd ?? this.config.cwd ?? process.cwd(),
87+
cwd,
88+
skipGitRepoCheck: shouldSkipGitRepoCheck(cwd),
6489
parentSessionId: null,
6590
});
6691
}
@@ -73,10 +98,12 @@ export class CodexSDKProvider implements AIProvider {
7398
}
7499

75100
async resumeSession(sessionId: string): Promise<AISession> {
101+
const cwd = this.config.cwd ?? process.cwd();
76102
return new CodexSDKSession({
77103
...this.baseConfig(),
78104
systemPrompt: null,
79-
cwd: this.config.cwd ?? process.cwd(),
105+
cwd,
106+
skipGitRepoCheck: shouldSkipGitRepoCheck(cwd),
80107
parentSessionId: null,
81108
resumeThreadId: sessionId,
82109
});
@@ -123,6 +150,7 @@ interface SessionConfig {
123150
maxTurns: number;
124151
sandboxMode: "read-only" | "workspace-write" | "danger-full-access";
125152
cwd: string;
153+
skipGitRepoCheck: boolean;
126154
parentSessionId: string | null;
127155
resumeThreadId?: string;
128156
codexExecutablePath?: string;
@@ -173,13 +201,15 @@ class CodexSDKSession extends BaseSession {
173201
this._thread = this._codexInstance.resumeThread(this.config.resumeThreadId, {
174202
model: this.config.model,
175203
workingDirectory: this.config.cwd,
204+
skipGitRepoCheck: this.config.skipGitRepoCheck,
176205
sandboxMode: this.config.sandboxMode,
177206
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
178207
});
179208
} else {
180209
this._thread = this._codexInstance.startThread({
181210
model: this.config.model,
182211
workingDirectory: this.config.cwd,
212+
skipGitRepoCheck: this.config.skipGitRepoCheck,
183213
sandboxMode: this.config.sandboxMode,
184214
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
185215
});

0 commit comments

Comments
 (0)