ocpipe 0.6.5 → 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.5",
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,17 +1,20 @@
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 = ''
17
+ private suppressHtml = false
15
18
 
16
19
  write(text: string): string {
17
20
  this.buf += text
@@ -23,19 +26,30 @@ class CodexLogFilter {
23
26
  }
24
27
  const line = this.buf.slice(0, idx + 1)
25
28
  this.buf = this.buf.slice(idx + 1)
26
- if (suppressCodexLogLine(line)) {
27
- continue
28
- }
29
- out += line
29
+ out += this.filterLine(line)
30
30
  }
31
31
  }
32
32
 
33
33
  flush(): string {
34
34
  const line = this.buf
35
35
  this.buf = ''
36
+ return this.filterLine(line)
37
+ }
38
+
39
+ private filterLine(line: string): string {
40
+ if (this.suppressHtml) {
41
+ if (line.includes('</html>')) {
42
+ this.suppressHtml = false
43
+ }
44
+ return ''
45
+ }
36
46
  if (suppressCodexLogLine(line)) {
37
47
  return ''
38
48
  }
49
+ if (suppressCodexHtmlLine(line)) {
50
+ this.suppressHtml = !line.includes('</html>')
51
+ return ''
52
+ }
39
53
  return line
40
54
  }
41
55
  }
@@ -49,6 +63,10 @@ function suppressCodexLogLine(line: string): boolean {
49
63
  return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+WARN\s+codex_/.test(line)
50
64
  }
51
65
 
66
+ function suppressCodexHtmlLine(line: string): boolean {
67
+ return /^\s*(<!doctype html>|<html\b|<head\b)/i.test(line)
68
+ }
69
+
52
70
  /** runCodexAgent executes a Codex agent with a prompt. */
53
71
  export async function runCodexAgent(
54
72
  options: RunAgentOptions,
@@ -63,157 +81,103 @@ export async function runCodexAgent(
63
81
  signal,
64
82
  } = options
65
83
 
66
- if (sessionId) {
67
- throw new Error('Codex backend does not support session resume yet')
68
- }
69
84
  if (signal?.aborted) {
70
85
  throw new Error('Request aborted')
71
86
  }
72
87
 
73
88
  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('-')
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)
112
95
 
113
96
  const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
97
+ const sessionInfo =
98
+ sessionId && !codex?.ephemeral ? `[thread:${sessionId}]` : '[new thread]'
114
99
  console.error(
115
- `\n>>> Codex [${model.modelID}] [new session]: ${promptPreview}...`,
100
+ `\n>>> Codex SDK [${model.modelID}] ${sessionInfo}: ${promptPreview}...`,
116
101
  )
117
102
 
118
- return new Promise((resolve, reject) => {
119
- const proc = spawn(cmd, args, {
120
- cwd,
121
- stdio: ['pipe', 'pipe', 'pipe'],
122
- })
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
+ }
123
121
 
124
- const stderrChunks: string[] = []
125
- const stdoutFilter = new CodexLogFilter()
126
- const stderrFilter = new CodexLogFilter()
127
- 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
+ )
128
127
 
129
- const cleanup = async () => {
130
- await unlink(outputFile).catch(() => {})
128
+ return {
129
+ text: response,
130
+ sessionId: nextSessionId,
131
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'))
132
+ } catch (err) {
133
+ if (timedOut) {
134
+ throw new Error(`Timeout after ${timeoutSec}s`, { cause: err })
143
135
  }
144
- signal?.addEventListener('abort', abortHandler, { once: true })
136
+ if (signal?.aborted) {
137
+ throw new Error('Request aborted', { cause: err })
138
+ }
139
+ throw err
140
+ } finally {
141
+ if (timeout) clearTimeout(timeout)
142
+ signal?.removeEventListener('abort', abortHandler)
143
+ }
144
+ }
145
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
- }
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
+ }
193
159
 
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
- })
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
+ }
219
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/predict.ts CHANGED
@@ -77,6 +77,7 @@ export class Predict<S extends AnySignature> {
77
77
  timeoutSec: ctx.timeoutSec,
78
78
  workdir: ctx.workdir,
79
79
  claudeCode: ctx.claudeCode,
80
+ codex: ctx.codex,
80
81
  signal: ctx.signal,
81
82
  })
82
83
 
@@ -207,6 +208,7 @@ export class Predict<S extends AnySignature> {
207
208
  timeoutSec: ctx.timeoutSec,
208
209
  workdir: ctx.workdir,
209
210
  claudeCode: ctx.claudeCode,
211
+ codex: ctx.codex,
210
212
  signal: ctx.signal,
211
213
  })
212
214
 
@@ -284,6 +286,7 @@ export class Predict<S extends AnySignature> {
284
286
  timeoutSec: ctx.timeoutSec,
285
287
  workdir: ctx.workdir,
286
288
  claudeCode: ctx.claudeCode,
289
+ codex: ctx.codex,
287
290
  signal: ctx.signal,
288
291
  })
289
292
 
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