ocpipe 0.6.3 → 0.6.5

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.
package/README.md CHANGED
@@ -49,7 +49,7 @@ type GreetOut = InferOutputs<typeof Greet> // { greeting: string }
49
49
 
50
50
  ### Backends
51
51
 
52
- ocpipe supports two backends for running LLM agents:
52
+ ocpipe supports three backends for running LLM agents:
53
53
 
54
54
  **OpenCode** (default) - Requires `opencode` CLI in your PATH. Supports 75+ providers.
55
55
 
@@ -76,6 +76,13 @@ defaultModel: { backend: 'claude-code', modelID: 'sonnet' },
76
76
  claudeCode: { permissionMode: 'acceptEdits' },
77
77
  ```
78
78
 
79
+ **Codex** - Uses `@openai/codex`. Install as a peer dependency.
80
+
81
+ ```typescript
82
+ defaultModel: { backend: 'codex', modelID: 'gpt-5.4' },
83
+ codex: { sandbox: 'read-only', ephemeral: true },
84
+ ```
85
+
79
86
  ### Requirements
80
87
 
81
88
  **For OpenCode backend:** Currently requires [this OpenCode fork](https://github.com/paralin/opencode). Once the following PRs are merged, the official release will work:
@@ -89,6 +96,12 @@ claudeCode: { permissionMode: 'acceptEdits' },
89
96
  bun add @anthropic-ai/claude-agent-sdk
90
97
  ```
91
98
 
99
+ **For Codex backend:** Install the Codex CLI package as a peer dependency:
100
+
101
+ ```bash
102
+ bun add @openai/codex
103
+ ```
104
+
92
105
  ### Documentation
93
106
 
94
107
  - [Getting Started](./GETTING_STARTED.md) - Tutorial with examples
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.6.3",
4
- "description": "SDK for LLM pipelines with OpenCode and Zod",
3
+ "version": "0.6.5",
4
+ "description": "SDK for LLM pipelines with OpenCode, Codex, and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -19,6 +19,7 @@
19
19
  "dspy",
20
20
  "llm",
21
21
  "opencode",
22
+ "codex",
22
23
  "typescript",
23
24
  "ai",
24
25
  "workflow",
@@ -28,27 +29,31 @@
28
29
  "engines": {
29
30
  "bun": ">=1.0.0"
30
31
  },
31
- "dependencies": {},
32
32
  "peerDependencies": {
33
- "zod": "4.4.1",
34
- "@anthropic-ai/claude-agent-sdk": "0.2.126"
33
+ "zod": "4.4.3",
34
+ "@anthropic-ai/claude-agent-sdk": "0.2.126",
35
+ "@openai/codex": "0.128.0"
35
36
  },
36
37
  "peerDependenciesMeta": {
37
38
  "@anthropic-ai/claude-agent-sdk": {
38
39
  "optional": true
40
+ },
41
+ "@openai/codex": {
42
+ "optional": true
39
43
  }
40
44
  },
41
45
  "devDependencies": {
42
- "@anthropic-ai/claude-agent-sdk": "^0.2.126",
46
+ "@anthropic-ai/claude-agent-sdk": "^0.2.132",
47
+ "@openai/codex": "^0.128.0",
43
48
  "@eslint/js": "^10.0.1",
44
49
  "bun-types": "^1.3.13",
45
- "eslint": "^10.2.1",
46
- "globals": "^17.5.0",
47
- "jiti": "^2.6.1",
50
+ "eslint": "^10.3.0",
51
+ "globals": "^17.6.0",
52
+ "jiti": "^2.7.0",
48
53
  "prettier": "^3.8.3",
49
54
  "typescript": "^6.0.3",
50
- "typescript-eslint": "^8.59.1",
51
- "@typescript/native-preview": "^7.0.0-dev.20260430.1",
55
+ "typescript-eslint": "^8.59.2",
56
+ "@typescript/native-preview": "^7.0.0-dev.20260506.1",
52
57
  "vitest": "^4.1.5"
53
58
  },
54
59
  "scripts": {
package/src/agent.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * ocpipe agent integration.
3
3
  *
4
- * Dispatches to OpenCode CLI or Claude Code SDK based on backend configuration.
4
+ * Dispatches to OpenCode CLI, Claude Code SDK, or Codex CLI based on backend
5
+ * configuration.
5
6
  */
6
7
 
7
8
  import { spawn } from 'child_process'
@@ -27,6 +28,11 @@ export async function runAgent(
27
28
  return runClaudeCodeAgent(options)
28
29
  }
29
30
 
31
+ if (backend === 'codex') {
32
+ const { runCodexAgent } = await import('./codex.js')
33
+ return runCodexAgent(options)
34
+ }
35
+
30
36
  return runOpencodeAgent(options)
31
37
  }
32
38
 
package/src/codex.ts ADDED
@@ -0,0 +1,219 @@
1
+ /**
2
+ * ocpipe Codex CLI integration.
3
+ *
4
+ * Runs Codex non-interactively through `codex exec`.
5
+ */
6
+
7
+ import { spawn } from 'child_process'
8
+ import { mkdir, readFile, unlink } from 'fs/promises'
9
+ import { join } from 'path'
10
+ import { PROJECT_ROOT, TMP_DIR } from './paths.js'
11
+ import type { RunAgentOptions, RunAgentResult } from './types.js'
12
+
13
+ class CodexLogFilter {
14
+ private buf = ''
15
+
16
+ write(text: string): string {
17
+ this.buf += text
18
+ let out = ''
19
+ for (;;) {
20
+ const idx = this.buf.indexOf('\n')
21
+ if (idx < 0) {
22
+ return out
23
+ }
24
+ const line = this.buf.slice(0, idx + 1)
25
+ this.buf = this.buf.slice(idx + 1)
26
+ if (suppressCodexLogLine(line)) {
27
+ continue
28
+ }
29
+ out += line
30
+ }
31
+ }
32
+
33
+ flush(): string {
34
+ const line = this.buf
35
+ this.buf = ''
36
+ if (suppressCodexLogLine(line)) {
37
+ return ''
38
+ }
39
+ return line
40
+ }
41
+ }
42
+
43
+ export function filterCodexLogText(text: string): string {
44
+ const filter = new CodexLogFilter()
45
+ return filter.write(text) + filter.flush()
46
+ }
47
+
48
+ function suppressCodexLogLine(line: string): boolean {
49
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+WARN\s+codex_/.test(line)
50
+ }
51
+
52
+ /** runCodexAgent executes a Codex agent with a prompt. */
53
+ export async function runCodexAgent(
54
+ options: RunAgentOptions,
55
+ ): Promise<RunAgentResult> {
56
+ const {
57
+ prompt,
58
+ model,
59
+ sessionId,
60
+ timeoutSec = 3600,
61
+ workdir,
62
+ codex,
63
+ signal,
64
+ } = options
65
+
66
+ if (sessionId) {
67
+ throw new Error('Codex backend does not support session resume yet')
68
+ }
69
+ if (signal?.aborted) {
70
+ throw new Error('Request aborted')
71
+ }
72
+
73
+ const cwd = workdir ?? PROJECT_ROOT
74
+ await mkdir(TMP_DIR, { recursive: true })
75
+ const stamp = `${Date.now()}_${Math.random().toString(36).slice(2)}`
76
+ const outputFile = join(TMP_DIR, `codex_output_${stamp}.txt`)
77
+
78
+ const cmd = codex?.pathToCodexExecutable ?? 'codex'
79
+ const args = [
80
+ 'exec',
81
+ '--color',
82
+ 'never',
83
+ '--model',
84
+ model.modelID,
85
+ '--sandbox',
86
+ codex?.sandbox ?? 'read-only',
87
+ '--cd',
88
+ cwd,
89
+ '--output-last-message',
90
+ outputFile,
91
+ ]
92
+
93
+ if (codex?.ephemeral ?? true) {
94
+ args.push('--ephemeral')
95
+ }
96
+ if (codex?.ignoreUserConfig) {
97
+ args.push('--ignore-user-config')
98
+ }
99
+ if (codex?.ignoreRules) {
100
+ args.push('--ignore-rules')
101
+ }
102
+ if (codex?.reasoningEffort) {
103
+ args.push('-c', `model_reasoning_effort="${codex.reasoningEffort}"`)
104
+ }
105
+ for (const dir of codex?.addDirs ?? []) {
106
+ args.push('--add-dir', dir)
107
+ }
108
+ for (const [key, value] of Object.entries(codex?.config ?? {})) {
109
+ args.push('-c', `${key}=${value}`)
110
+ }
111
+ args.push('-')
112
+
113
+ const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
114
+ console.error(
115
+ `\n>>> Codex [${model.modelID}] [new session]: ${promptPreview}...`,
116
+ )
117
+
118
+ return new Promise((resolve, reject) => {
119
+ const proc = spawn(cmd, args, {
120
+ cwd,
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ })
123
+
124
+ const stderrChunks: string[] = []
125
+ const stdoutFilter = new CodexLogFilter()
126
+ const stderrFilter = new CodexLogFilter()
127
+ let aborted = false
128
+
129
+ const cleanup = async () => {
130
+ await unlink(outputFile).catch(() => {})
131
+ }
132
+
133
+ const abortHandler = () => {
134
+ if (aborted) return
135
+ aborted = true
136
+ console.error('\n[abort] Killing Codex subprocess...')
137
+ proc.kill('SIGTERM')
138
+ setTimeout(() => {
139
+ if (!proc.killed) proc.kill('SIGKILL')
140
+ }, 1000)
141
+ void cleanup()
142
+ reject(new Error('Request aborted'))
143
+ }
144
+ signal?.addEventListener('abort', abortHandler, { once: true })
145
+
146
+ proc.stdout.on('data', (data: Buffer) => {
147
+ const text = stdoutFilter.write(data.toString())
148
+ if (text) {
149
+ process.stderr.write(text)
150
+ }
151
+ })
152
+ proc.stderr.on('data', (data: Buffer) => {
153
+ const text = stderrFilter.write(data.toString())
154
+ stderrChunks.push(text)
155
+ if (text) {
156
+ process.stderr.write(text)
157
+ }
158
+ })
159
+
160
+ const timeout =
161
+ timeoutSec > 0 ?
162
+ setTimeout(() => {
163
+ proc.kill()
164
+ void cleanup()
165
+ reject(new Error(`Timeout after ${timeoutSec}s`))
166
+ }, timeoutSec * 1000)
167
+ : null
168
+
169
+ proc.stdin.end(prompt)
170
+
171
+ proc.on('close', async (code) => {
172
+ if (timeout) clearTimeout(timeout)
173
+ signal?.removeEventListener('abort', abortHandler)
174
+ if (aborted) return
175
+
176
+ const stdoutTail = stdoutFilter.flush()
177
+ if (stdoutTail) {
178
+ process.stderr.write(stdoutTail)
179
+ }
180
+ const stderrTail = stderrFilter.flush()
181
+ if (stderrTail) {
182
+ stderrChunks.push(stderrTail)
183
+ process.stderr.write(stderrTail)
184
+ }
185
+ const stderr = stderrChunks.join('').trim()
186
+ if (code !== 0) {
187
+ await cleanup()
188
+ const detail =
189
+ stderr ? `\n${stderr.split('\n').slice(-10).join('\n')}` : ''
190
+ reject(new Error(`Codex exited with code ${code}${detail}`))
191
+ return
192
+ }
193
+
194
+ try {
195
+ const response = (await readFile(outputFile, 'utf8')).trim()
196
+ await cleanup()
197
+ if (!response) {
198
+ reject(new Error('Codex returned an empty response'))
199
+ return
200
+ }
201
+ console.error(`<<< Codex done (${response.length} chars)`)
202
+ resolve({
203
+ text: response,
204
+ sessionId: '',
205
+ })
206
+ } catch (err) {
207
+ await cleanup()
208
+ reject(err)
209
+ }
210
+ })
211
+
212
+ proc.on('error', async (err) => {
213
+ if (timeout) clearTimeout(timeout)
214
+ signal?.removeEventListener('abort', abortHandler)
215
+ await cleanup()
216
+ reject(err)
217
+ })
218
+ })
219
+ }
package/src/index.ts CHANGED
@@ -91,6 +91,7 @@ export type {
91
91
  BackendType,
92
92
  PermissionMode,
93
93
  ClaudeCodeOptions,
94
+ CodexOptions,
94
95
  ModelConfig,
95
96
  ExecutionContext,
96
97
  StepResult,
package/src/pipeline.ts CHANGED
@@ -34,6 +34,7 @@ export class Pipeline<S extends BaseState> {
34
34
  timeoutSec: config.timeoutSec ?? 3600,
35
35
  workdir: config.workdir,
36
36
  claudeCode: config.claudeCode,
37
+ codex: config.codex,
37
38
  }
38
39
  }
39
40
 
package/src/types.ts CHANGED
@@ -9,7 +9,7 @@ import type { z } from 'zod/v4'
9
9
  // ============================================================================
10
10
 
11
11
  /** Backend type for running agents. */
12
- export type BackendType = 'opencode' | 'claude-code'
12
+ export type BackendType = 'opencode' | 'claude-code' | 'codex'
13
13
 
14
14
  /** Permission mode for Claude Code sessions. */
15
15
  export type PermissionMode =
@@ -57,6 +57,26 @@ export interface ClaudeCodeOptions {
57
57
  allowedTools?: string[]
58
58
  }
59
59
 
60
+ /** Codex CLI specific session options. */
61
+ export interface CodexOptions {
62
+ /** Path to Codex executable (default: `codex` from PATH). */
63
+ pathToCodexExecutable?: string
64
+ /** Sandbox mode for `codex exec` (default: `read-only`). */
65
+ sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access'
66
+ /** Extra config overrides passed as repeated `-c key=value` flags. */
67
+ config?: Record<string, string>
68
+ /** Reasoning effort passed to Codex (for example: `low`, `medium`, `high`, `xhigh`). */
69
+ reasoningEffort?: string
70
+ /** Run without persisting Codex session files (default: true). */
71
+ ephemeral?: boolean
72
+ /** Ignore user config for deterministic automation (default: false). */
73
+ ignoreUserConfig?: boolean
74
+ /** Ignore user and project execpolicy rules (default: false). */
75
+ ignoreRules?: boolean
76
+ /** Additional directories Codex may access alongside the working directory. */
77
+ addDirs?: string[]
78
+ }
79
+
60
80
  /** Model configuration for LLM backends. */
61
81
  export interface ModelConfig {
62
82
  /** Backend to use (default: 'opencode'). */
@@ -88,6 +108,8 @@ export interface ExecutionContext {
88
108
  workdir?: string
89
109
  /** Claude Code specific options. */
90
110
  claudeCode?: ClaudeCodeOptions
111
+ /** Codex CLI specific options. */
112
+ codex?: CodexOptions
91
113
  /** AbortSignal for cancelling requests. When aborted, kills subprocesses. */
92
114
  signal?: AbortSignal
93
115
  }
@@ -306,6 +328,8 @@ export interface PipelineConfig {
306
328
  workdir?: string
307
329
  /** Claude Code specific options. */
308
330
  claudeCode?: ClaudeCodeOptions
331
+ /** Codex CLI specific options. */
332
+ codex?: CodexOptions
309
333
  }
310
334
 
311
335
  /** Options for running a pipeline step. */
@@ -340,6 +364,8 @@ export interface RunAgentOptions {
340
364
  workdir?: string
341
365
  /** Claude Code specific options. */
342
366
  claudeCode?: ClaudeCodeOptions
367
+ /** Codex CLI specific options. */
368
+ codex?: CodexOptions
343
369
  /** AbortSignal for cancelling the request. When aborted, kills the subprocess. */
344
370
  signal?: AbortSignal
345
371
  }