Skip to content

Commit 762ab01

Browse files
committed
feat(api): add CodexClient wrapper with defaults; tests and README usage
1 parent bcc708e commit 762ab01

4 files changed

Lines changed: 120 additions & 1 deletion

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ Options:
3030
- Full auto: `run_exec("scaffold a cli", full_auto=True)`
3131
- Run in another dir: `run_exec("...", cd="/path/to/project")`
3232

33+
Using a client with defaults:
34+
35+
```
36+
from codex import CodexClient
37+
38+
client = CodexClient(model="gpt-4.1", full_auto=True)
39+
print(client.run("explain this repo"))
40+
```
41+
3342
### Install uv
3443

3544
- macOS (Homebrew): `brew install uv`

codex/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
output = run_exec("explain this codebase to me")
88
"""
99

10-
from .api import CodexError, CodexNotFoundError, CodexProcessError, find_binary, run_exec
10+
from .api import (
11+
CodexClient,
12+
CodexError,
13+
CodexNotFoundError,
14+
CodexProcessError,
15+
find_binary,
16+
run_exec,
17+
)
1118

1219
__all__ = [
1320
"__version__",
1421
"CodexError",
1522
"CodexNotFoundError",
1623
"CodexProcessError",
24+
"CodexClient",
1725
"find_binary",
1826
"run_exec",
1927
]

codex/api.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,67 @@ def run_exec(
9999
)
100100
return stdout
101101

102+
103+
@dataclass(slots=True)
104+
class CodexClient:
105+
"""Lightweight, synchronous client for the Codex CLI.
106+
107+
Provides defaults for repeated invocations and convenience helpers.
108+
"""
109+
110+
executable: str = "codex"
111+
model: Optional[str] = None
112+
full_auto: bool = False
113+
cd: Optional[str] = None
114+
env: Optional[Mapping[str, str]] = None
115+
extra_args: Optional[Sequence[str]] = None
116+
117+
def ensure_available(self) -> str:
118+
"""Return the resolved binary path or raise CodexNotFoundError."""
119+
return find_binary(self.executable)
120+
121+
def run(
122+
self,
123+
prompt: str,
124+
*,
125+
model: Optional[str] = None,
126+
full_auto: Optional[bool] = None,
127+
cd: Optional[str] = None,
128+
timeout: Optional[float] = None,
129+
env: Optional[Mapping[str, str]] = None,
130+
extra_args: Optional[Iterable[str]] = None,
131+
) -> str:
132+
"""Execute `codex exec` and return stdout.
133+
134+
Explicit arguments override the client's defaults.
135+
"""
136+
eff_model = model if model is not None else self.model
137+
eff_full_auto = full_auto if full_auto is not None else self.full_auto
138+
eff_cd = cd if cd is not None else self.cd
139+
140+
# Merge environment overlays; run_exec will merge with os.environ
141+
merged_env: Optional[Mapping[str, str]]
142+
if self.env and env:
143+
tmp = dict(self.env)
144+
tmp.update(env)
145+
merged_env = tmp
146+
else:
147+
merged_env = env or self.env
148+
149+
# Compose extra args
150+
eff_extra: list[str] = []
151+
if self.extra_args:
152+
eff_extra.extend(self.extra_args)
153+
if extra_args:
154+
eff_extra.extend(list(extra_args))
155+
156+
return run_exec(
157+
prompt,
158+
model=eff_model,
159+
full_auto=eff_full_auto,
160+
cd=eff_cd,
161+
timeout=timeout,
162+
env=merged_env,
163+
executable=self.executable,
164+
extra_args=eff_extra,
165+
)

tests/test_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import os
2+
import stat
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from codex import CodexClient, CodexNotFoundError
8+
9+
10+
def test_client_missing_binary():
11+
client = CodexClient(executable="codex-does-not-exist-xyz")
12+
with pytest.raises(CodexNotFoundError):
13+
client.ensure_available()
14+
with pytest.raises(CodexNotFoundError):
15+
client.run("hello")
16+
17+
18+
def test_client_runs_with_defaults(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
19+
# Dummy codex that echoes args and succeeds
20+
bin_dir = tmp_path / "bin"
21+
bin_dir.mkdir()
22+
codex_path = bin_dir / "codex"
23+
codex_path.write_text("""#!/bin/sh\necho \"[dummy] $@\"\n""")
24+
codex_path.chmod(codex_path.stat().st_mode | stat.S_IXUSR)
25+
26+
monkeypatch.setenv("PATH", f"{bin_dir}{os.pathsep}" + os.environ.get("PATH", ""))
27+
28+
client = CodexClient(model="test-model", full_auto=True, extra_args=["--ask-for-approval"])
29+
out = client.run("hello world")
30+
31+
# Ensure key flags and prompt are passed along; order-insensitive
32+
assert "[dummy]" in out
33+
assert "exec" in out
34+
assert "hello world" in out
35+
assert "--full-auto" in out
36+
assert "-m test-model" in out
37+
assert "--ask-for-approval" in out
38+

0 commit comments

Comments
 (0)