ocpipe 0.6.6 → 0.6.7

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
@@ -76,11 +76,11 @@ 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.
79
+ **Codex** - Uses `@openai/codex-sdk`. Install as a peer dependency.
80
80
 
81
81
  ```typescript
82
82
  defaultModel: { backend: 'codex', modelID: 'gpt-5.4' },
83
- codex: { sandbox: 'read-only', ephemeral: true },
83
+ codex: { sandbox: 'read-only', reasoningEffort: 'high' },
84
84
  ```
85
85
 
86
86
  ### Requirements
@@ -96,10 +96,10 @@ codex: { sandbox: 'read-only', ephemeral: true },
96
96
  bun add @anthropic-ai/claude-agent-sdk
97
97
  ```
98
98
 
99
- **For Codex backend:** Install the Codex CLI package as a peer dependency:
99
+ **For Codex backend:** Install the Codex SDK package as a peer dependency:
100
100
 
101
101
  ```bash
102
- bun add @openai/codex
102
+ bun add @openai/codex-sdk
103
103
  ```
104
104
 
105
105
  ### Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "SDK for LLM pipelines with OpenCode, Codex, and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -32,20 +32,21 @@
32
32
  "peerDependencies": {
33
33
  "zod": "4.4.3",
34
34
  "@anthropic-ai/claude-agent-sdk": "0.2.126",
35
- "@openai/codex": "0.128.0"
35
+ "@openai/codex-sdk": "0.130.0"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@anthropic-ai/claude-agent-sdk": {
39
39
  "optional": true
40
40
  },
41
- "@openai/codex": {
41
+ "@openai/codex-sdk": {
42
42
  "optional": true
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
46
  "@anthropic-ai/claude-agent-sdk": "^0.2.132",
47
- "@openai/codex": "^0.128.0",
48
47
  "@eslint/js": "^10.0.1",
48
+ "@openai/codex-sdk": "0.130.0",
49
+ "@typescript/native-preview": "^7.0.0-dev.20260506.1",
49
50
  "bun-types": "^1.3.13",
50
51
  "eslint": "^10.3.0",
51
52
  "globals": "^17.6.0",
@@ -53,7 +54,6 @@
53
54
  "prettier": "^3.8.3",
54
55
  "typescript": "^6.0.3",
55
56
  "typescript-eslint": "^8.59.2",
56
- "@typescript/native-preview": "^7.0.0-dev.20260506.1",
57
57
  "vitest": "^4.1.5"
58
58
  },
59
59
  "scripts": {
package/src/agent.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ocpipe agent integration.
3
3
  *
4
- * Dispatches to OpenCode CLI, Claude Code SDK, or Codex CLI based on backend
4
+ * Dispatches to OpenCode CLI, Claude Code SDK, or Codex SDK based on backend
5
5
  * configuration.
6
6
  */
7
7
 
package/src/codex.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  /**
2
- * ocpipe Codex CLI integration.
2
+ * ocpipe Codex SDK integration.
3
3
  *
4
- * Runs Codex non-interactively through `codex exec`.
4
+ * Runs Codex through @openai/codex-sdk threads.
5
5
  */
6
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'
7
+ import {
8
+ Codex,
9
+ type CodexOptions as CodexSdkClientOptions,
10
+ type ThreadOptions,
11
+ } from '@openai/codex-sdk'
12
+ import { PROJECT_ROOT } from './paths.js'
13
+ import type { CodexOptions, RunAgentOptions, RunAgentResult } from './types.js'
12
14
 
13
15
  class CodexLogFilter {
14
16
  private buf = ''
@@ -79,157 +81,103 @@ export async function runCodexAgent(
79
81
  signal,
80
82
  } = options
81
83
 
82
- if (sessionId) {
83
- throw new Error('Codex backend does not support session resume yet')
84
- }
85
84
  if (signal?.aborted) {
86
85
  throw new Error('Request aborted')
87
86
  }
88
87
 
89
88
  const cwd = workdir ?? PROJECT_ROOT
90
- await mkdir(TMP_DIR, { recursive: true })
91
- const stamp = `${Date.now()}_${Math.random().toString(36).slice(2)}`
92
- const outputFile = join(TMP_DIR, `codex_output_${stamp}.txt`)
93
-
94
- const cmd = codex?.pathToCodexExecutable ?? 'codex'
95
- const args = [
96
- 'exec',
97
- '--color',
98
- 'never',
99
- '--model',
100
- model.modelID,
101
- '--sandbox',
102
- codex?.sandbox ?? 'read-only',
103
- '--cd',
104
- cwd,
105
- '--output-last-message',
106
- outputFile,
107
- ]
108
-
109
- if (codex?.ephemeral ?? true) {
110
- args.push('--ephemeral')
111
- }
112
- if (codex?.ignoreUserConfig) {
113
- args.push('--ignore-user-config')
114
- }
115
- if (codex?.ignoreRules) {
116
- args.push('--ignore-rules')
117
- }
118
- if (codex?.reasoningEffort) {
119
- args.push('-c', `model_reasoning_effort="${codex.reasoningEffort}"`)
120
- }
121
- for (const dir of codex?.addDirs ?? []) {
122
- args.push('--add-dir', dir)
123
- }
124
- for (const [key, value] of Object.entries(codex?.config ?? {})) {
125
- args.push('-c', `${key}=${value}`)
126
- }
127
- args.push('-')
89
+ const client = new Codex(buildCodexClientOptions(codex))
90
+ const threadOptions = buildCodexThreadOptions(model.modelID, cwd, codex)
91
+ const thread =
92
+ sessionId && !codex?.ephemeral ?
93
+ client.resumeThread(sessionId, threadOptions)
94
+ : client.startThread(threadOptions)
128
95
 
129
96
  const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
97
+ const sessionInfo =
98
+ sessionId && !codex?.ephemeral ? `[thread:${sessionId}]` : '[new thread]'
130
99
  console.error(
131
- `\n>>> Codex [${model.modelID}] [new session]: ${promptPreview}...`,
100
+ `\n>>> Codex SDK [${model.modelID}] ${sessionInfo}: ${promptPreview}...`,
132
101
  )
133
102
 
134
- return new Promise((resolve, reject) => {
135
- const proc = spawn(cmd, args, {
136
- cwd,
137
- stdio: ['pipe', 'pipe', 'pipe'],
138
- })
103
+ const abort = new AbortController()
104
+ let timedOut = false
105
+ const abortHandler = () => abort.abort()
106
+ signal?.addEventListener('abort', abortHandler, { once: true })
107
+ const timeout =
108
+ timeoutSec > 0 ?
109
+ setTimeout(() => {
110
+ timedOut = true
111
+ abort.abort()
112
+ }, timeoutSec * 1000)
113
+ : null
114
+
115
+ try {
116
+ const result = await thread.run(prompt, { signal: abort.signal })
117
+ const response = result.finalResponse.trim()
118
+ if (!response) {
119
+ throw new Error('Codex returned an empty response')
120
+ }
139
121
 
140
- const stderrChunks: string[] = []
141
- const stdoutFilter = new CodexLogFilter()
142
- const stderrFilter = new CodexLogFilter()
143
- let aborted = false
122
+ const nextSessionId = codex?.ephemeral ? '' : (thread.id ?? '')
123
+ const sessionStr = nextSessionId || 'none'
124
+ console.error(
125
+ `<<< Codex SDK done (${response.length} chars) [thread:${sessionStr}]`,
126
+ )
144
127
 
145
- const cleanup = async () => {
146
- await unlink(outputFile).catch(() => {})
128
+ return {
129
+ text: response,
130
+ sessionId: nextSessionId,
147
131
  }
148
-
149
- const abortHandler = () => {
150
- if (aborted) return
151
- aborted = true
152
- console.error('\n[abort] Killing Codex subprocess...')
153
- proc.kill('SIGTERM')
154
- setTimeout(() => {
155
- if (!proc.killed) proc.kill('SIGKILL')
156
- }, 1000)
157
- void cleanup()
158
- reject(new Error('Request aborted'))
132
+ } catch (err) {
133
+ if (timedOut) {
134
+ throw new Error(`Timeout after ${timeoutSec}s`, { cause: err })
135
+ }
136
+ if (signal?.aborted) {
137
+ throw new Error('Request aborted', { cause: err })
159
138
  }
160
- signal?.addEventListener('abort', abortHandler, { once: true })
139
+ throw err
140
+ } finally {
141
+ if (timeout) clearTimeout(timeout)
142
+ signal?.removeEventListener('abort', abortHandler)
143
+ }
144
+ }
161
145
 
162
- proc.stdout.on('data', (data: Buffer) => {
163
- const text = stdoutFilter.write(data.toString())
164
- if (text) {
165
- process.stderr.write(text)
166
- }
167
- })
168
- proc.stderr.on('data', (data: Buffer) => {
169
- const text = stderrFilter.write(data.toString())
170
- stderrChunks.push(text)
171
- if (text) {
172
- process.stderr.write(text)
173
- }
174
- })
175
-
176
- const timeout =
177
- timeoutSec > 0 ?
178
- setTimeout(() => {
179
- proc.kill()
180
- void cleanup()
181
- reject(new Error(`Timeout after ${timeoutSec}s`))
182
- }, timeoutSec * 1000)
183
- : null
184
-
185
- proc.stdin.end(prompt)
186
-
187
- proc.on('close', async (code) => {
188
- if (timeout) clearTimeout(timeout)
189
- signal?.removeEventListener('abort', abortHandler)
190
- if (aborted) return
191
-
192
- const stdoutTail = stdoutFilter.flush()
193
- if (stdoutTail) {
194
- process.stderr.write(stdoutTail)
195
- }
196
- const stderrTail = stderrFilter.flush()
197
- if (stderrTail) {
198
- stderrChunks.push(stderrTail)
199
- process.stderr.write(stderrTail)
200
- }
201
- const stderr = stderrChunks.join('').trim()
202
- if (code !== 0) {
203
- await cleanup()
204
- const detail =
205
- stderr ? `\n${stderr.split('\n').slice(-10).join('\n')}` : ''
206
- reject(new Error(`Codex exited with code ${code}${detail}`))
207
- return
208
- }
146
+ function buildCodexClientOptions(
147
+ codex: CodexOptions | undefined,
148
+ ): CodexSdkClientOptions {
149
+ return {
150
+ ...(codex?.pathToCodexExecutable ?
151
+ { codexPathOverride: codex.pathToCodexExecutable }
152
+ : {}),
153
+ ...(codex?.baseUrl ? { baseUrl: codex.baseUrl } : {}),
154
+ ...(codex?.apiKey ? { apiKey: codex.apiKey } : {}),
155
+ ...(codex?.env ? { env: codex.env } : {}),
156
+ ...(codex?.config ? { config: codex.config } : {}),
157
+ }
158
+ }
209
159
 
210
- try {
211
- const response = (await readFile(outputFile, 'utf8')).trim()
212
- await cleanup()
213
- if (!response) {
214
- reject(new Error('Codex returned an empty response'))
215
- return
216
- }
217
- console.error(`<<< Codex done (${response.length} chars)`)
218
- resolve({
219
- text: response,
220
- sessionId: '',
221
- })
222
- } catch (err) {
223
- await cleanup()
224
- reject(err)
225
- }
226
- })
227
-
228
- proc.on('error', async (err) => {
229
- if (timeout) clearTimeout(timeout)
230
- signal?.removeEventListener('abort', abortHandler)
231
- await cleanup()
232
- reject(err)
233
- })
234
- })
160
+ function buildCodexThreadOptions(
161
+ modelID: string,
162
+ cwd: string,
163
+ codex: CodexOptions | undefined,
164
+ ): ThreadOptions {
165
+ return {
166
+ model: modelID,
167
+ workingDirectory: cwd,
168
+ skipGitRepoCheck: true,
169
+ sandboxMode: codex?.sandbox ?? 'read-only',
170
+ ...(codex?.reasoningEffort ?
171
+ { modelReasoningEffort: codex.reasoningEffort }
172
+ : {}),
173
+ ...(codex?.addDirs ? { additionalDirectories: codex.addDirs } : {}),
174
+ ...(codex?.approvalPolicy ? { approvalPolicy: codex.approvalPolicy } : {}),
175
+ ...(codex?.networkAccessEnabled !== undefined ?
176
+ { networkAccessEnabled: codex.networkAccessEnabled }
177
+ : {}),
178
+ ...(codex?.webSearchMode ? { webSearchMode: codex.webSearchMode } : {}),
179
+ ...(codex?.webSearchEnabled !== undefined ?
180
+ { webSearchEnabled: codex.webSearchEnabled }
181
+ : {}),
182
+ }
235
183
  }
package/src/index.ts CHANGED
@@ -91,7 +91,12 @@ export type {
91
91
  BackendType,
92
92
  PermissionMode,
93
93
  ClaudeCodeOptions,
94
+ CodexApprovalPolicy,
95
+ CodexConfigValue,
94
96
  CodexOptions,
97
+ CodexReasoningEffort,
98
+ CodexSandboxMode,
99
+ CodexWebSearchMode,
95
100
  ModelConfig,
96
101
  ExecutionContext,
97
102
  StepResult,
package/src/types.ts CHANGED
@@ -11,6 +11,38 @@ import type { z } from 'zod/v4'
11
11
  /** Backend type for running agents. */
12
12
  export type BackendType = 'opencode' | 'claude-code' | 'codex'
13
13
 
14
+ /** Reasoning effort for Codex SDK threads. */
15
+ export type CodexReasoningEffort =
16
+ | 'minimal'
17
+ | 'low'
18
+ | 'medium'
19
+ | 'high'
20
+ | 'xhigh'
21
+
22
+ /** Codex sandbox mode. */
23
+ export type CodexSandboxMode =
24
+ | 'read-only'
25
+ | 'workspace-write'
26
+ | 'danger-full-access'
27
+
28
+ /** Codex approval policy. */
29
+ export type CodexApprovalPolicy =
30
+ | 'never'
31
+ | 'on-request'
32
+ | 'on-failure'
33
+ | 'untrusted'
34
+
35
+ /** Codex web search mode. */
36
+ export type CodexWebSearchMode = 'disabled' | 'cached' | 'live'
37
+
38
+ /** Codex SDK config override value. */
39
+ export type CodexConfigValue =
40
+ | string
41
+ | number
42
+ | boolean
43
+ | CodexConfigValue[]
44
+ | { [key: string]: CodexConfigValue }
45
+
14
46
  /** Permission mode for Claude Code sessions. */
15
47
  export type PermissionMode =
16
48
  | 'default'
@@ -57,24 +89,34 @@ export interface ClaudeCodeOptions {
57
89
  allowedTools?: string[]
58
90
  }
59
91
 
60
- /** Codex CLI specific session options. */
92
+ /** Codex SDK specific session options. */
61
93
  export interface CodexOptions {
62
- /** Path to Codex executable (default: `codex` from PATH). */
94
+ /** Path override for the Codex executable used by the SDK. */
63
95
  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). */
96
+ /** Base URL passed to the Codex SDK. */
97
+ baseUrl?: string
98
+ /** API key passed to the Codex SDK. */
99
+ apiKey?: string
100
+ /** Environment variables passed to the Codex subprocess. */
101
+ env?: Record<string, string>
102
+ /** Sandbox mode for Codex (default: `read-only`). */
103
+ sandbox?: CodexSandboxMode
104
+ /** Extra config overrides passed through the Codex SDK. */
105
+ config?: { [key: string]: CodexConfigValue }
106
+ /** Reasoning effort passed to Codex. */
107
+ reasoningEffort?: CodexReasoningEffort
108
+ /** Start a new SDK thread instead of resuming the provided session ID. */
71
109
  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
110
  /** Additional directories Codex may access alongside the working directory. */
77
111
  addDirs?: string[]
112
+ /** Approval policy passed to Codex. */
113
+ approvalPolicy?: CodexApprovalPolicy
114
+ /** Enable network access in workspace-write sandboxes. */
115
+ networkAccessEnabled?: boolean
116
+ /** Web search mode passed to Codex. */
117
+ webSearchMode?: CodexWebSearchMode
118
+ /** Legacy boolean web search control passed to Codex. */
119
+ webSearchEnabled?: boolean
78
120
  }
79
121
 
80
122
  /** Model configuration for LLM backends. */
@@ -108,9 +150,9 @@ export interface ExecutionContext {
108
150
  workdir?: string
109
151
  /** Claude Code specific options. */
110
152
  claudeCode?: ClaudeCodeOptions
111
- /** Codex CLI specific options. */
153
+ /** Codex SDK specific options. */
112
154
  codex?: CodexOptions
113
- /** AbortSignal for cancelling requests. When aborted, kills subprocesses. */
155
+ /** AbortSignal for cancelling in-flight backend requests. */
114
156
  signal?: AbortSignal
115
157
  }
116
158
 
@@ -328,7 +370,7 @@ export interface PipelineConfig {
328
370
  workdir?: string
329
371
  /** Claude Code specific options. */
330
372
  claudeCode?: ClaudeCodeOptions
331
- /** Codex CLI specific options. */
373
+ /** Codex SDK specific options. */
332
374
  codex?: CodexOptions
333
375
  }
334
376
 
@@ -364,9 +406,9 @@ export interface RunAgentOptions {
364
406
  workdir?: string
365
407
  /** Claude Code specific options. */
366
408
  claudeCode?: ClaudeCodeOptions
367
- /** Codex CLI specific options. */
409
+ /** Codex SDK specific options. */
368
410
  codex?: CodexOptions
369
- /** AbortSignal for cancelling the request. When aborted, kills the subprocess. */
411
+ /** AbortSignal for cancelling the request. */
370
412
  signal?: AbortSignal
371
413
  }
372
414