codexcw 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {codexcw-0.2.2 → codexcw-0.3.0}/Cargo.lock +3 -3
  2. {codexcw-0.2.2 → codexcw-0.3.0}/Cargo.toml +2 -2
  3. {codexcw-0.2.2 → codexcw-0.3.0}/PKG-INFO +22 -10
  4. {codexcw-0.2.2/bindings/python → codexcw-0.3.0}/README.md +20 -8
  5. {codexcw-0.2.2 → codexcw-0.3.0/bindings/python}/README.md +20 -8
  6. {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/src/lib.rs +210 -15
  7. codexcw-0.3.0/bindings/python/tests/fixtures/fake_claude.sh +38 -0
  8. {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/tests/test_smoke.py +176 -1
  9. {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/uv.lock +1 -1
  10. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/Cargo.toml +1 -1
  11. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/README.md +39 -10
  12. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/args.rs +14 -2
  13. codexcw-0.3.0/crates/codexcw/src/claude.rs +890 -0
  14. codexcw-0.3.0/crates/codexcw/src/claude_account_usage.rs +184 -0
  15. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/decoder.rs +17 -1
  16. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/error.rs +39 -11
  17. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/event.rs +77 -26
  18. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/group.rs +4 -4
  19. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/lib.rs +23 -10
  20. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/request.rs +41 -9
  21. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/runner.rs +132 -44
  22. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/session.rs +5 -5
  23. codexcw-0.3.0/crates/codexcw/tests/claude_account_usage_it.rs +135 -0
  24. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/tests/runner_it.rs +115 -1
  25. {codexcw-0.2.2 → codexcw-0.3.0}/pyproject.toml +2 -2
  26. {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/__init__.py +94 -12
  27. {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/_codexcw.pyi +29 -0
  28. {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/aio.py +20 -3
  29. {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/Cargo.toml +0 -0
  30. {codexcw-0.2.2 → codexcw-0.3.0}/bindings/python/tests/fixtures/fake_codex.sh +0 -0
  31. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/examples/run.rs +0 -0
  32. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/account_usage.rs +0 -0
  33. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/src/tail.rs +0 -0
  34. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/tests/account_usage_it.rs +0 -0
  35. {codexcw-0.2.2 → codexcw-0.3.0}/crates/codexcw/tests/common/mod.rs +0 -0
  36. {codexcw-0.2.2 → codexcw-0.3.0}/python/codexcw/py.typed +0 -0
@@ -22,7 +22,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
22
22
 
23
23
  [[package]]
24
24
  name = "codexcw"
25
- version = "0.2.2"
25
+ version = "0.3.0"
26
26
  dependencies = [
27
27
  "serde",
28
28
  "serde_json",
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "codexcw-node"
38
- version = "0.2.2"
38
+ version = "0.3.0"
39
39
  dependencies = [
40
40
  "codexcw",
41
41
  "napi",
@@ -47,7 +47,7 @@ dependencies = [
47
47
 
48
48
  [[package]]
49
49
  name = "codexcw-py"
50
- version = "0.2.2"
50
+ version = "0.3.0"
51
51
  dependencies = [
52
52
  "codexcw",
53
53
  "pyo3",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/codexcw", "bindings/python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.2"
6
+ version = "0.3.0"
7
7
  edition = "2021"
8
8
  rust-version = "1.90"
9
9
  license = "CC0-1.0"
@@ -12,7 +12,7 @@ homepage = "https://github.com/c3-oss/codexcw"
12
12
  authors = ["c3-oss"]
13
13
 
14
14
  [workspace.dependencies]
15
- codexcw = { path = "crates/codexcw", version = "0.2.2" }
15
+ codexcw = { path = "crates/codexcw", version = "0.3.0" }
16
16
  tokio = { version = "1", default-features = false, features = [
17
17
  "process",
18
18
  "io-util",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codexcw
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Operating System :: POSIX
@@ -9,7 +9,7 @@ Classifier: Topic :: Software Development :: Libraries
9
9
  Requires-Dist: pytest>=8 ; extra == 'dev'
10
10
  Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
11
11
  Provides-Extra: dev
12
- Summary: Run the Codex CLI non-interactively from Python, backed by a Rust core.
12
+ Summary: Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
13
13
  Keywords: codex,automation,agent,cli
14
14
  Author: c3-oss
15
15
  License: CC0-1.0
@@ -20,14 +20,19 @@ Project-URL: Repository, https://github.com/c3-oss/codexcw
20
20
 
21
21
  # codexcw (Python)
22
22
 
23
- Run the Codex CLI non-interactively from Python, backed by a Rust core. It
24
- spawns `codex exec --json`, decodes the JSONL event stream, and exposes each run
25
- as iterables, callbacks, results, and typed errors.
23
+ Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
24
+ The Codex agent wraps `codex exec --json`; the Claude agent wraps
25
+ `claude -p --output-format stream-json`. Both expose iterables, callbacks,
26
+ results, typed usage, and typed errors through the same API.
26
27
 
27
- The `codex` executable must be on `PATH`, authenticated, and new enough to
28
- support `codex exec --json`. Defaults are automation-friendly: read-only
29
- sandbox, approval `never`, ephemeral sessions, color disabled, git check
30
- skipped.
28
+ The selected agent executable must be on `PATH` and authenticated. Codex must
29
+ support `codex exec --json`; Claude must support `--output-format stream-json`.
30
+ Defaults are automation-friendly: ephemeral sessions and non-interactive
31
+ execution, with Codex using a read-only sandbox and approval `never`.
32
+
33
+ The Claude agent is selected with `Runner(agent=codexcw.AGENT_CLAUDE)`; its
34
+ events are normalized into the same event model, with model selection via the
35
+ `haiku`/`sonnet`/`opus` aliases (`CLAUDE_MODEL_*`).
31
36
 
32
37
  ## Install
33
38
 
@@ -43,6 +48,8 @@ from codexcw import Runner, Request
43
48
  runner = Runner()
44
49
  result = runner.run(Request(prompt="diga oi"))
45
50
  print(result.final_message)
51
+ print(result.usage.total_tokens)
52
+ print(result.usage.total_cost_usd)
46
53
  ```
47
54
 
48
55
  ## Streaming
@@ -66,9 +73,14 @@ from codexcw import AccountUsageRequest, get_account_usage
66
73
  usage = get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}))
67
74
  print(usage.rate_limits.primary.used_percent if usage.rate_limits.primary else None)
68
75
  print(usage.token_usage.summary.lifetime_tokens if usage.token_usage else None)
76
+
77
+ from codexcw import get_claude_account_usage
78
+
79
+ claude_usage = get_claude_account_usage()
80
+ print(claude_usage.windows)
69
81
  ```
70
82
 
71
- ## Running many Codex instances
83
+ ## Running many agent instances
72
84
 
73
85
  ```python
74
86
  from codexcw import Runner, Request
@@ -1,13 +1,18 @@
1
1
  # codexcw (Python)
2
2
 
3
- Run the Codex CLI non-interactively from Python, backed by a Rust core. It
4
- spawns `codex exec --json`, decodes the JSONL event stream, and exposes each run
5
- as iterables, callbacks, results, and typed errors.
3
+ Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
4
+ The Codex agent wraps `codex exec --json`; the Claude agent wraps
5
+ `claude -p --output-format stream-json`. Both expose iterables, callbacks,
6
+ results, typed usage, and typed errors through the same API.
6
7
 
7
- The `codex` executable must be on `PATH`, authenticated, and new enough to
8
- support `codex exec --json`. Defaults are automation-friendly: read-only
9
- sandbox, approval `never`, ephemeral sessions, color disabled, git check
10
- skipped.
8
+ The selected agent executable must be on `PATH` and authenticated. Codex must
9
+ support `codex exec --json`; Claude must support `--output-format stream-json`.
10
+ Defaults are automation-friendly: ephemeral sessions and non-interactive
11
+ execution, with Codex using a read-only sandbox and approval `never`.
12
+
13
+ The Claude agent is selected with `Runner(agent=codexcw.AGENT_CLAUDE)`; its
14
+ events are normalized into the same event model, with model selection via the
15
+ `haiku`/`sonnet`/`opus` aliases (`CLAUDE_MODEL_*`).
11
16
 
12
17
  ## Install
13
18
 
@@ -23,6 +28,8 @@ from codexcw import Runner, Request
23
28
  runner = Runner()
24
29
  result = runner.run(Request(prompt="diga oi"))
25
30
  print(result.final_message)
31
+ print(result.usage.total_tokens)
32
+ print(result.usage.total_cost_usd)
26
33
  ```
27
34
 
28
35
  ## Streaming
@@ -46,9 +53,14 @@ from codexcw import AccountUsageRequest, get_account_usage
46
53
  usage = get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}))
47
54
  print(usage.rate_limits.primary.used_percent if usage.rate_limits.primary else None)
48
55
  print(usage.token_usage.summary.lifetime_tokens if usage.token_usage else None)
56
+
57
+ from codexcw import get_claude_account_usage
58
+
59
+ claude_usage = get_claude_account_usage()
60
+ print(claude_usage.windows)
49
61
  ```
50
62
 
51
- ## Running many Codex instances
63
+ ## Running many agent instances
52
64
 
53
65
  ```python
54
66
  from codexcw import Runner, Request
@@ -1,13 +1,18 @@
1
1
  # codexcw (Python)
2
2
 
3
- Run the Codex CLI non-interactively from Python, backed by a Rust core. It
4
- spawns `codex exec --json`, decodes the JSONL event stream, and exposes each run
5
- as iterables, callbacks, results, and typed errors.
3
+ Run Codex or Claude Code non-interactively from Python, backed by a Rust core.
4
+ The Codex agent wraps `codex exec --json`; the Claude agent wraps
5
+ `claude -p --output-format stream-json`. Both expose iterables, callbacks,
6
+ results, typed usage, and typed errors through the same API.
6
7
 
7
- The `codex` executable must be on `PATH`, authenticated, and new enough to
8
- support `codex exec --json`. Defaults are automation-friendly: read-only
9
- sandbox, approval `never`, ephemeral sessions, color disabled, git check
10
- skipped.
8
+ The selected agent executable must be on `PATH` and authenticated. Codex must
9
+ support `codex exec --json`; Claude must support `--output-format stream-json`.
10
+ Defaults are automation-friendly: ephemeral sessions and non-interactive
11
+ execution, with Codex using a read-only sandbox and approval `never`.
12
+
13
+ The Claude agent is selected with `Runner(agent=codexcw.AGENT_CLAUDE)`; its
14
+ events are normalized into the same event model, with model selection via the
15
+ `haiku`/`sonnet`/`opus` aliases (`CLAUDE_MODEL_*`).
11
16
 
12
17
  ## Install
13
18
 
@@ -23,6 +28,8 @@ from codexcw import Runner, Request
23
28
  runner = Runner()
24
29
  result = runner.run(Request(prompt="diga oi"))
25
30
  print(result.final_message)
31
+ print(result.usage.total_tokens)
32
+ print(result.usage.total_cost_usd)
26
33
  ```
27
34
 
28
35
  ## Streaming
@@ -46,9 +53,14 @@ from codexcw import AccountUsageRequest, get_account_usage
46
53
  usage = get_account_usage(AccountUsageRequest(env={"CODEX_HOME": "/tmp/codex-home"}))
47
54
  print(usage.rate_limits.primary.used_percent if usage.rate_limits.primary else None)
48
55
  print(usage.token_usage.summary.lifetime_tokens if usage.token_usage else None)
56
+
57
+ from codexcw import get_claude_account_usage
58
+
59
+ claude_usage = get_claude_account_usage()
60
+ print(claude_usage.windows)
49
61
  ```
50
62
 
51
- ## Running many Codex instances
63
+ ## Running many agent instances
52
64
 
53
65
  ```python
54
66
  from codexcw import Runner, Request
@@ -15,15 +15,19 @@ use tokio_stream::wrappers::ReceiverStream;
15
15
  use tokio_stream::StreamExt;
16
16
 
17
17
  use codexcw::{
18
- get_account_usage as core_get_account_usage, AccountCredits as CoreAccountCredits,
19
- AccountRateLimitWindow as CoreAccountRateLimitWindow,
18
+ get_account_usage as core_get_account_usage,
19
+ get_claude_account_usage as core_get_claude_account_usage,
20
+ AccountCredits as CoreAccountCredits, AccountRateLimitWindow as CoreAccountRateLimitWindow,
20
21
  AccountRateLimits as CoreAccountRateLimits, AccountSpendLimit as CoreAccountSpendLimit,
21
22
  AccountTokenUsage as CoreAccountTokenUsage,
22
23
  AccountTokenUsageDailyBucket as CoreAccountTokenUsageDailyBucket,
23
24
  AccountTokenUsageSummary as CoreAccountTokenUsageSummary, AccountUsage as CoreAccountUsage,
24
25
  AccountUsageAccount as CoreAccountUsageAccount, AccountUsageRequest as CoreAccountUsageRequest,
25
- ApprovalPolicy, ConfigOverride, Event, EventPayload, Item, ManyOptions, Request, RunResult,
26
- Runner as CoreRunner, SandboxMode, Usage,
26
+ ApprovalPolicy, ClaudeAccountUsage as CoreClaudeAccountUsage,
27
+ ClaudeAccountUsageRequest as CoreClaudeAccountUsageRequest,
28
+ ClaudeAccountUsageWindow as CoreClaudeAccountUsageWindow, ConfigOverride, Event, EventPayload,
29
+ Item, ManyOptions, ModelUsage as CoreModelUsage, Request, RunResult, Runner as CoreRunner,
30
+ SandboxMode, Usage,
27
31
  };
28
32
 
29
33
  fn runtime() -> &'static Runtime {
@@ -40,7 +44,29 @@ fn runtime() -> &'static Runtime {
40
44
  // Output value types.
41
45
  // ---------------------------------------------------------------------------
42
46
 
43
- /// Token usage reported by Codex.
47
+ /// Per-model usage reported by an agent.
48
+ #[pyclass(frozen, skip_from_py_object)]
49
+ #[derive(Clone)]
50
+ pub struct PyModelUsage {
51
+ #[pyo3(get)]
52
+ input_tokens: i64,
53
+ #[pyo3(get)]
54
+ output_tokens: i64,
55
+ #[pyo3(get)]
56
+ cache_read_input_tokens: i64,
57
+ #[pyo3(get)]
58
+ cache_creation_input_tokens: i64,
59
+ #[pyo3(get)]
60
+ web_search_requests: i64,
61
+ #[pyo3(get)]
62
+ cost_usd: f64,
63
+ #[pyo3(get)]
64
+ context_window: i64,
65
+ #[pyo3(get)]
66
+ max_output_tokens: i64,
67
+ }
68
+
69
+ /// Token usage reported by an agent.
44
70
  #[pyclass(frozen, skip_from_py_object)]
45
71
  #[derive(Clone, Default)]
46
72
  pub struct PyUsage {
@@ -49,11 +75,17 @@ pub struct PyUsage {
49
75
  #[pyo3(get)]
50
76
  cached_input_tokens: i64,
51
77
  #[pyo3(get)]
78
+ cache_creation_input_tokens: i64,
79
+ #[pyo3(get)]
52
80
  output_tokens: i64,
53
81
  #[pyo3(get)]
54
82
  reasoning_output_tokens: i64,
55
83
  #[pyo3(get)]
56
84
  total_tokens: i64,
85
+ #[pyo3(get)]
86
+ total_cost_usd: f64,
87
+ #[pyo3(get)]
88
+ model_usage: HashMap<String, PyModelUsage>,
57
89
  }
58
90
 
59
91
  /// One file edit inside a `file_change` item.
@@ -66,7 +98,7 @@ pub struct PyFileChange {
66
98
  kind: String,
67
99
  }
68
100
 
69
- /// A typed projection of a Codex item.
101
+ /// A typed projection of an agent item.
70
102
  #[pyclass(frozen, skip_from_py_object)]
71
103
  #[derive(Clone)]
72
104
  pub struct PyItem {
@@ -92,7 +124,7 @@ pub struct PyItem {
92
124
  changes: Vec<PyFileChange>,
93
125
  }
94
126
 
95
- /// One decoded Codex event.
127
+ /// One decoded agent event.
96
128
  #[pyclass(frozen, skip_from_py_object)]
97
129
  #[derive(Clone)]
98
130
  pub struct PyEvent {
@@ -300,6 +332,40 @@ pub struct PyAccountUsageOutcome {
300
332
  error: Option<PyError>,
301
333
  }
302
334
 
335
+ /// One Claude usage window.
336
+ #[pyclass(frozen, skip_from_py_object)]
337
+ #[derive(Clone)]
338
+ pub struct PyClaudeAccountUsageWindow {
339
+ #[pyo3(get)]
340
+ label: String,
341
+ #[pyo3(get)]
342
+ used_percent: f64,
343
+ #[pyo3(get)]
344
+ resets_at: String,
345
+ }
346
+
347
+ /// Claude account usage report.
348
+ #[pyclass(frozen, skip_from_py_object)]
349
+ #[derive(Clone)]
350
+ pub struct PyClaudeAccountUsage {
351
+ #[pyo3(get)]
352
+ report: String,
353
+ #[pyo3(get)]
354
+ windows: Vec<PyClaudeAccountUsageWindow>,
355
+ #[pyo3(get)]
356
+ raw: String,
357
+ }
358
+
359
+ /// Claude account usage paired with any terminal error.
360
+ #[pyclass(frozen, skip_from_py_object)]
361
+ #[derive(Clone)]
362
+ pub struct PyClaudeAccountUsageOutcome {
363
+ #[pyo3(get)]
364
+ result: Option<PyClaudeAccountUsage>,
365
+ #[pyo3(get)]
366
+ error: Option<PyError>,
367
+ }
368
+
303
369
  /// One multiplexed event from `run_many`.
304
370
  #[pyclass(frozen, skip_from_py_object)]
305
371
  #[derive(Clone)]
@@ -347,6 +413,9 @@ struct ReqData {
347
413
  profile: Option<String>,
348
414
  sandbox: Option<String>,
349
415
  approval: Option<String>,
416
+ permission_mode: Option<String>,
417
+ allowed_tools: Option<Vec<String>>,
418
+ disallowed_tools: Option<Vec<String>>,
350
419
  config: Option<Vec<ConfigData>>,
351
420
  enable: Option<Vec<String>>,
352
421
  disable: Option<Vec<String>>,
@@ -386,6 +455,9 @@ impl ReqData {
386
455
  profile: self.profile,
387
456
  sandbox,
388
457
  approval,
458
+ permission_mode: self.permission_mode,
459
+ allowed_tools: self.allowed_tools.unwrap_or_default(),
460
+ disallowed_tools: self.disallowed_tools.unwrap_or_default(),
389
461
  config: self
390
462
  .config
391
463
  .unwrap_or_default()
@@ -419,6 +491,29 @@ struct AccountUsageReqData {
419
491
  timeout: Option<f64>,
420
492
  }
421
493
 
494
+ #[derive(FromPyObject)]
495
+ struct ClaudeAccountUsageReqData {
496
+ executable: Option<String>,
497
+ env: Option<HashMap<String, String>>,
498
+ timeout: Option<f64>,
499
+ }
500
+
501
+ impl ClaudeAccountUsageReqData {
502
+ fn into_core(self) -> Result<CoreClaudeAccountUsageRequest, PyError> {
503
+ let timeout = match self.timeout {
504
+ Some(value) => Some(Duration::try_from_secs_f64(value).map_err(|_| {
505
+ invalid_request("timeout must be a finite non-negative number".to_string())
506
+ })?),
507
+ None => None,
508
+ };
509
+ Ok(CoreClaudeAccountUsageRequest {
510
+ executable: self.executable,
511
+ env: self.env.unwrap_or_default().into_iter().collect(),
512
+ timeout,
513
+ })
514
+ }
515
+ }
516
+
422
517
  impl AccountUsageReqData {
423
518
  fn into_core(self) -> Result<CoreAccountUsageRequest, PyError> {
424
519
  Ok(CoreAccountUsageRequest {
@@ -467,6 +562,14 @@ fn invalid_request(message: String) -> PyError {
467
562
  }
468
563
  }
469
564
 
565
+ fn parse_agent(value: &str) -> Result<codexcw::Agent, PyError> {
566
+ match value {
567
+ "codex" => Ok(codexcw::Agent::Codex),
568
+ "claude" => Ok(codexcw::Agent::Claude),
569
+ other => Err(invalid_request(format!("unknown agent: {other}"))),
570
+ }
571
+ }
572
+
470
573
  fn parse_account_usage_timeout(timeout: f64) -> Result<Duration, PyError> {
471
574
  Duration::try_from_secs_f64(timeout).map_err(|_| {
472
575
  invalid_request(
@@ -480,9 +583,29 @@ fn to_py_usage(usage: &Usage) -> PyUsage {
480
583
  PyUsage {
481
584
  input_tokens: usage.input_tokens,
482
585
  cached_input_tokens: usage.cached_input_tokens,
586
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
483
587
  output_tokens: usage.output_tokens,
484
588
  reasoning_output_tokens: usage.reasoning_output_tokens,
485
589
  total_tokens: usage.total_tokens,
590
+ total_cost_usd: usage.total_cost_usd,
591
+ model_usage: usage
592
+ .model_usage
593
+ .iter()
594
+ .map(|(model, usage)| (model.clone(), to_py_model_usage(usage)))
595
+ .collect(),
596
+ }
597
+ }
598
+
599
+ fn to_py_model_usage(usage: &CoreModelUsage) -> PyModelUsage {
600
+ PyModelUsage {
601
+ input_tokens: usage.input_tokens,
602
+ output_tokens: usage.output_tokens,
603
+ cache_read_input_tokens: usage.cache_read_input_tokens,
604
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
605
+ web_search_requests: usage.web_search_requests,
606
+ cost_usd: usage.cost_usd,
607
+ context_window: usage.context_window,
608
+ max_output_tokens: usage.max_output_tokens,
486
609
  }
487
610
  }
488
611
 
@@ -515,7 +638,10 @@ fn to_py_event(event: &Event) -> PyEvent {
515
638
  match &event.payload {
516
639
  EventPayload::ItemStarted(i) | EventPayload::ItemCompleted(i) => item = Some(to_py_item(i)),
517
640
  EventPayload::TurnCompleted { usage: u } => usage = Some(to_py_usage(u)),
518
- EventPayload::TurnFailed { error: e } => error = Some(e.message.clone()),
641
+ EventPayload::TurnFailed { error: e, usage: u } => {
642
+ error = Some(e.message.clone());
643
+ usage = Some(to_py_usage(u));
644
+ }
519
645
  EventPayload::Error(e) => error = Some(e.message.clone()),
520
646
  _ => {}
521
647
  }
@@ -575,6 +701,13 @@ fn to_py_error(error: &codexcw::Error) -> PyError {
575
701
  stderr: None,
576
702
  line: None,
577
703
  },
704
+ Claude { .. } => PyError {
705
+ kind: "claude".to_string(),
706
+ message: error.to_string(),
707
+ code: None,
708
+ stderr: None,
709
+ line: None,
710
+ },
578
711
  Handler(_) => PyError {
579
712
  kind: "handler".to_string(),
580
713
  message: error.to_string(),
@@ -697,6 +830,28 @@ fn to_py_account_usage(usage: &CoreAccountUsage) -> PyAccountUsage {
697
830
  }
698
831
  }
699
832
 
833
+ fn to_py_claude_account_usage_window(
834
+ window: &CoreClaudeAccountUsageWindow,
835
+ ) -> PyClaudeAccountUsageWindow {
836
+ PyClaudeAccountUsageWindow {
837
+ label: window.label.clone(),
838
+ used_percent: window.used_percent,
839
+ resets_at: window.resets_at.clone(),
840
+ }
841
+ }
842
+
843
+ fn to_py_claude_account_usage(usage: &CoreClaudeAccountUsage) -> PyClaudeAccountUsage {
844
+ PyClaudeAccountUsage {
845
+ report: usage.report.clone(),
846
+ windows: usage
847
+ .windows
848
+ .iter()
849
+ .map(to_py_claude_account_usage_window)
850
+ .collect(),
851
+ raw: usage.raw.clone(),
852
+ }
853
+ }
854
+
700
855
  #[pyfunction]
701
856
  #[pyo3(signature = (req=None))]
702
857
  fn get_account_usage(py: Python<'_>, req: Option<AccountUsageReqData>) -> PyAccountUsageOutcome {
@@ -722,6 +877,34 @@ fn get_account_usage(py: Python<'_>, req: Option<AccountUsageReqData>) -> PyAcco
722
877
  }
723
878
  }
724
879
 
880
+ #[pyfunction]
881
+ #[pyo3(signature = (req=None))]
882
+ fn get_claude_account_usage(
883
+ py: Python<'_>,
884
+ req: Option<ClaudeAccountUsageReqData>,
885
+ ) -> PyClaudeAccountUsageOutcome {
886
+ let core_req = match req.map(ClaudeAccountUsageReqData::into_core).transpose() {
887
+ Ok(req) => req.unwrap_or_default(),
888
+ Err(error) => {
889
+ return PyClaudeAccountUsageOutcome {
890
+ result: None,
891
+ error: Some(error),
892
+ };
893
+ }
894
+ };
895
+ let result = py.detach(|| runtime().block_on(core_get_claude_account_usage(core_req)));
896
+ match result {
897
+ Ok(usage) => PyClaudeAccountUsageOutcome {
898
+ result: Some(to_py_claude_account_usage(&usage)),
899
+ error: None,
900
+ },
901
+ Err(error) => PyClaudeAccountUsageOutcome {
902
+ result: None,
903
+ error: Some(to_py_error(&error)),
904
+ },
905
+ }
906
+ }
907
+
725
908
  // ---------------------------------------------------------------------------
726
909
  // Session.
727
910
  // ---------------------------------------------------------------------------
@@ -736,7 +919,7 @@ enum SessionInner {
736
919
  Failed(PyError),
737
920
  }
738
921
 
739
- /// A running `codex exec` process. Iterate it for events; call `wait()` for the
922
+ /// A running selected-agent process. Iterate it for events; call `wait()` for the
740
923
  /// final outcome.
741
924
  #[pyclass]
742
925
  pub struct Session {
@@ -812,7 +995,7 @@ impl Session {
812
995
  }
813
996
  }
814
997
 
815
- /// The Codex thread id once known.
998
+ /// The agent thread or session id once known.
816
999
  fn thread_id(&self) -> String {
817
1000
  match &self.inner {
818
1001
  SessionInner::Failed(_) => String::new(),
@@ -840,7 +1023,7 @@ struct LiveGroup {
840
1023
  total: usize,
841
1024
  }
842
1025
 
843
- /// A batch of running `codex exec` processes.
1026
+ /// A batch of running selected-agent processes.
844
1027
  #[pyclass]
845
1028
  pub struct Group {
846
1029
  inner: LiveGroup,
@@ -929,7 +1112,7 @@ impl Group {
929
1112
  // Runner.
930
1113
  // ---------------------------------------------------------------------------
931
1114
 
932
- /// Starts `codex exec` processes with safe automation defaults.
1115
+ /// Starts selected-agent processes with safe automation defaults.
933
1116
  #[pyclass]
934
1117
  pub struct Runner {
935
1118
  core: CoreRunner,
@@ -946,6 +1129,7 @@ impl Runner {
946
1129
  scan_max_bytes=None,
947
1130
  default_sandbox=None,
948
1131
  default_approval=None,
1132
+ agent=None,
949
1133
  ))]
950
1134
  #[allow(clippy::too_many_arguments)]
951
1135
  fn new(
@@ -956,8 +1140,14 @@ impl Runner {
956
1140
  scan_max_bytes: Option<u32>,
957
1141
  default_sandbox: Option<String>,
958
1142
  default_approval: Option<String>,
959
- ) -> Self {
1143
+ agent: Option<String>,
1144
+ ) -> PyResult<Self> {
960
1145
  let mut builder = CoreRunner::builder();
1146
+ if let Some(value) = agent {
1147
+ let agent = parse_agent(&value)
1148
+ .map_err(|error| pyo3::exceptions::PyValueError::new_err(error.message))?;
1149
+ builder = builder.agent(agent);
1150
+ }
961
1151
  if let Some(executable) = executable {
962
1152
  builder = builder.executable(executable);
963
1153
  }
@@ -981,9 +1171,9 @@ impl Runner {
981
1171
  builder =
982
1172
  builder.default_approval(parse_approval(&value).unwrap_or(ApprovalPolicy::Never));
983
1173
  }
984
- Runner {
1174
+ Ok(Runner {
985
1175
  core: builder.build(),
986
- }
1176
+ })
987
1177
  }
988
1178
 
989
1179
  /// Runs one process to completion and returns its outcome.
@@ -1073,6 +1263,7 @@ fn _codexcw(m: &Bound<'_, PyModule>) -> PyResult<()> {
1073
1263
  m.add_class::<Group>()?;
1074
1264
  m.add_class::<PyEvent>()?;
1075
1265
  m.add_class::<PyItem>()?;
1266
+ m.add_class::<PyModelUsage>()?;
1076
1267
  m.add_class::<PyUsage>()?;
1077
1268
  m.add_class::<PyFileChange>()?;
1078
1269
  m.add_class::<PyRunResult>()?;
@@ -1088,8 +1279,12 @@ fn _codexcw(m: &Bound<'_, PyModule>) -> PyResult<()> {
1088
1279
  m.add_class::<PyAccountTokenUsageSummary>()?;
1089
1280
  m.add_class::<PyAccountTokenUsageDailyBucket>()?;
1090
1281
  m.add_class::<PyAccountUsageOutcome>()?;
1282
+ m.add_class::<PyClaudeAccountUsage>()?;
1283
+ m.add_class::<PyClaudeAccountUsageWindow>()?;
1284
+ m.add_class::<PyClaudeAccountUsageOutcome>()?;
1091
1285
  m.add_class::<PyRunEvent>()?;
1092
1286
  m.add_class::<PyGroupResult>()?;
1093
1287
  m.add_function(wrap_pyfunction!(get_account_usage, m)?)?;
1288
+ m.add_function(wrap_pyfunction!(get_claude_account_usage, m)?)?;
1094
1289
  Ok(())
1095
1290
  }
@@ -0,0 +1,38 @@
1
+ #!/bin/sh
2
+ # Fake `claude` for tests: records argv and stdin, then emits a fixed
3
+ # stream-json event stream. Mirrors the claude cases in the Go/Rust suites.
4
+ set -eu
5
+
6
+ if [ "${CODEXCW_ARGS_FILE:-}" != "" ]; then
7
+ : >"$CODEXCW_ARGS_FILE"
8
+ for arg in "$@"; do
9
+ printf '%s\n' "$arg" >>"$CODEXCW_ARGS_FILE"
10
+ done
11
+ fi
12
+
13
+ prompt=""
14
+ if [ "${CODEXCW_STDIN_FILE:-}" != "" ]; then
15
+ cat >"$CODEXCW_STDIN_FILE"
16
+ prompt=$(cat "$CODEXCW_STDIN_FILE")
17
+ else
18
+ prompt=$(cat)
19
+ fi
20
+
21
+ if [ "$prompt" = "/usage" ]; then
22
+ printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"You are currently using your subscription\n\nCurrent session: 13% used · resets Jul 16 at 3:50pm (America/Sao_Paulo)\nCurrent week (all models): 5% used · resets Jul 18 at 9am (America/Sao_Paulo)"}'
23
+ exit 0
24
+ fi
25
+
26
+ if [ "${CODEXCW_CLAUDE_ERROR:-}" != "" ]; then
27
+ printf '%s\n' '{"type":"system","subtype":"init","cwd":"/work","session_id":"sess-error"}'
28
+ printf '%s\n' '{"type":"result","subtype":"success","is_error":true,"result":"Claude fixture failure","session_id":"sess-error","usage":{"input_tokens":1,"cache_creation_input_tokens":2,"cache_read_input_tokens":3,"output_tokens":4}}'
29
+ exit 1
30
+ fi
31
+
32
+ printf '%s\n' '{"type":"system","subtype":"init","cwd":"/work","session_id":"sess-1"}'
33
+ printf '%s\n' '{"type":"assistant","message":{"id":"msg_1","content":[{"type":"thinking","thinking":"I will create the file."}]},"session_id":"sess-1"}'
34
+ printf '%s\n' '{"type":"assistant","message":{"id":"msg_1","content":[{"type":"tool_use","id":"toolu_1","name":"Write","input":{"file_path":"/work/hello.txt","content":"hello"}}]},"session_id":"sess-1"}'
35
+ printf '%s\n' '{"type":"user","message":{"content":[{"tool_use_id":"toolu_1","type":"tool_result","content":"File created successfully"}]},"session_id":"sess-1","tool_use_result":{"type":"create"}}'
36
+ printf '%s\n' '{"type":"assistant","message":{"id":"msg_2","content":[{"type":"thinking","thinking":"The file is ready."}]},"session_id":"sess-1"}'
37
+ printf '%s\n' '{"type":"assistant","message":{"id":"msg_2","content":[{"type":"text","text":"Done."}]},"session_id":"sess-1"}'
38
+ printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"Done.","session_id":"sess-1","total_cost_usd":0.013562,"usage":{"input_tokens":18,"cache_creation_input_tokens":3944,"cache_read_input_tokens":45921,"output_tokens":380},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":18,"outputTokens":380,"cacheReadInputTokens":45921,"cacheCreationInputTokens":3944,"webSearchRequests":0,"costUSD":0.013562,"contextWindow":200000,"maxOutputTokens":32000}}}'