ocpipe 0.6.3 → 0.6.4

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.4",
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,160 @@
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
+ /** runCodexAgent executes a Codex agent with a prompt. */
14
+ export async function runCodexAgent(
15
+ options: RunAgentOptions,
16
+ ): Promise<RunAgentResult> {
17
+ const {
18
+ prompt,
19
+ model,
20
+ sessionId,
21
+ timeoutSec = 3600,
22
+ workdir,
23
+ codex,
24
+ signal,
25
+ } = options
26
+
27
+ if (sessionId) {
28
+ throw new Error('Codex backend does not support session resume yet')
29
+ }
30
+ if (signal?.aborted) {
31
+ throw new Error('Request aborted')
32
+ }
33
+
34
+ const cwd = workdir ?? PROJECT_ROOT
35
+ await mkdir(TMP_DIR, { recursive: true })
36
+ const stamp = `${Date.now()}_${Math.random().toString(36).slice(2)}`
37
+ const outputFile = join(TMP_DIR, `codex_output_${stamp}.txt`)
38
+
39
+ const cmd = codex?.pathToCodexExecutable ?? 'codex'
40
+ const args = [
41
+ 'exec',
42
+ '--color',
43
+ 'never',
44
+ '--model',
45
+ model.modelID,
46
+ '--sandbox',
47
+ codex?.sandbox ?? 'read-only',
48
+ '--cd',
49
+ cwd,
50
+ '--output-last-message',
51
+ outputFile,
52
+ ]
53
+
54
+ if (codex?.ephemeral ?? true) {
55
+ args.push('--ephemeral')
56
+ }
57
+ if (codex?.ignoreUserConfig) {
58
+ args.push('--ignore-user-config')
59
+ }
60
+ if (codex?.ignoreRules) {
61
+ args.push('--ignore-rules')
62
+ }
63
+ for (const dir of codex?.addDirs ?? []) {
64
+ args.push('--add-dir', dir)
65
+ }
66
+ for (const [key, value] of Object.entries(codex?.config ?? {})) {
67
+ args.push('-c', `${key}=${value}`)
68
+ }
69
+ args.push('-')
70
+
71
+ const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
72
+ console.error(
73
+ `\n>>> Codex [${model.modelID}] [new session]: ${promptPreview}...`,
74
+ )
75
+
76
+ return new Promise((resolve, reject) => {
77
+ const proc = spawn(cmd, args, {
78
+ cwd,
79
+ stdio: ['pipe', 'pipe', 'pipe'],
80
+ })
81
+
82
+ const stderrChunks: string[] = []
83
+ let aborted = false
84
+
85
+ const cleanup = async () => {
86
+ await unlink(outputFile).catch(() => {})
87
+ }
88
+
89
+ const abortHandler = () => {
90
+ if (aborted) return
91
+ aborted = true
92
+ console.error('\n[abort] Killing Codex subprocess...')
93
+ proc.kill('SIGTERM')
94
+ setTimeout(() => {
95
+ if (!proc.killed) proc.kill('SIGKILL')
96
+ }, 1000)
97
+ void cleanup()
98
+ reject(new Error('Request aborted'))
99
+ }
100
+ signal?.addEventListener('abort', abortHandler, { once: true })
101
+
102
+ proc.stdout.on('data', (data: Buffer) => {
103
+ process.stderr.write(data.toString())
104
+ })
105
+ proc.stderr.on('data', (data: Buffer) => {
106
+ const text = data.toString()
107
+ stderrChunks.push(text)
108
+ process.stderr.write(text)
109
+ })
110
+
111
+ const timeout =
112
+ timeoutSec > 0 ?
113
+ setTimeout(() => {
114
+ proc.kill()
115
+ void cleanup()
116
+ reject(new Error(`Timeout after ${timeoutSec}s`))
117
+ }, timeoutSec * 1000)
118
+ : null
119
+
120
+ proc.stdin.end(prompt)
121
+
122
+ proc.on('close', async (code) => {
123
+ if (timeout) clearTimeout(timeout)
124
+ signal?.removeEventListener('abort', abortHandler)
125
+ if (aborted) return
126
+
127
+ const stderr = stderrChunks.join('').trim()
128
+ if (code !== 0) {
129
+ await cleanup()
130
+ const detail = stderr ? `\n${stderr.split('\n').slice(-10).join('\n')}` : ''
131
+ reject(new Error(`Codex exited with code ${code}${detail}`))
132
+ return
133
+ }
134
+
135
+ try {
136
+ const response = (await readFile(outputFile, 'utf8')).trim()
137
+ await cleanup()
138
+ if (!response) {
139
+ reject(new Error('Codex returned an empty response'))
140
+ return
141
+ }
142
+ console.error(`<<< Codex done (${response.length} chars)`)
143
+ resolve({
144
+ text: response,
145
+ sessionId: '',
146
+ })
147
+ } catch (err) {
148
+ await cleanup()
149
+ reject(err)
150
+ }
151
+ })
152
+
153
+ proc.on('error', async (err) => {
154
+ if (timeout) clearTimeout(timeout)
155
+ signal?.removeEventListener('abort', abortHandler)
156
+ await cleanup()
157
+ reject(err)
158
+ })
159
+ })
160
+ }
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,24 @@ 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
+ /** Run without persisting Codex session files (default: true). */
69
+ ephemeral?: boolean
70
+ /** Ignore user config for deterministic automation (default: false). */
71
+ ignoreUserConfig?: boolean
72
+ /** Ignore user and project execpolicy rules (default: false). */
73
+ ignoreRules?: boolean
74
+ /** Additional directories Codex may access alongside the working directory. */
75
+ addDirs?: string[]
76
+ }
77
+
60
78
  /** Model configuration for LLM backends. */
61
79
  export interface ModelConfig {
62
80
  /** Backend to use (default: 'opencode'). */
@@ -88,6 +106,8 @@ export interface ExecutionContext {
88
106
  workdir?: string
89
107
  /** Claude Code specific options. */
90
108
  claudeCode?: ClaudeCodeOptions
109
+ /** Codex CLI specific options. */
110
+ codex?: CodexOptions
91
111
  /** AbortSignal for cancelling requests. When aborted, kills subprocesses. */
92
112
  signal?: AbortSignal
93
113
  }
@@ -306,6 +326,8 @@ export interface PipelineConfig {
306
326
  workdir?: string
307
327
  /** Claude Code specific options. */
308
328
  claudeCode?: ClaudeCodeOptions
329
+ /** Codex CLI specific options. */
330
+ codex?: CodexOptions
309
331
  }
310
332
 
311
333
  /** Options for running a pipeline step. */
@@ -340,6 +362,8 @@ export interface RunAgentOptions {
340
362
  workdir?: string
341
363
  /** Claude Code specific options. */
342
364
  claudeCode?: ClaudeCodeOptions
365
+ /** Codex CLI specific options. */
366
+ codex?: CodexOptions
343
367
  /** AbortSignal for cancelling the request. When aborted, kills the subprocess. */
344
368
  signal?: AbortSignal
345
369
  }