ocpipe 0.6.9 → 0.6.10

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
@@ -1,5 +1,5 @@
1
1
  <p align="center"><strong>ocpipe</strong></p>
2
- <p align="center">Build LLM pipelines with <a href="https://github.com/sst/opencode">OpenCode</a>, <a href="https://github.com/anthropics/claude-code">Claude Code</a>, Pi, and <a href="https://zod.dev">Zod</a>.</p>
2
+ <p align="center">Build LLM pipelines with <a href="https://github.com/sst/opencode">OpenCode</a>, <a href="https://github.com/anthropics/claude-code">Claude Code</a>, Oh My Pi, Pi, and <a href="https://zod.dev">Zod</a>.</p>
3
3
  <p align="center">Inspired by <a href="https://github.com/stanfordnlp/dspy">DSPy</a>.</p>
4
4
  <p align="center">
5
5
  <a href="https://www.npmjs.com/package/ocpipe"><img alt="npm" src="https://img.shields.io/npm/v/ocpipe?style=flat-square" /></a>
@@ -11,7 +11,7 @@
11
11
  - **Type-safe** Define inputs and outputs with Zod schemas
12
12
  - **Modular** Compose modules into complex pipelines
13
13
  - **Checkpoints** Resume from any step
14
- - **Multi-backend** Choose between OpenCode (75+ providers), Claude Code SDK, Codex SDK, or Pi
14
+ - **Multi-backend** Choose between OpenCode (75+ providers), Claude Code SDK, Codex SDK, Oh My Pi, or Pi
15
15
  - **Auto-correction** Fixes schema mismatches automatically
16
16
 
17
17
  ### Quick Start
@@ -49,7 +49,7 @@ type GreetOut = InferOutputs<typeof Greet> // { greeting: string }
49
49
 
50
50
  ### Backends
51
51
 
52
- ocpipe supports four backends for running LLM agents:
52
+ ocpipe supports five backends for running LLM agents:
53
53
 
54
54
  **OpenCode** (default) - Requires `opencode` CLI in your PATH. Supports 75+ providers.
55
55
 
@@ -83,6 +83,13 @@ defaultModel: { backend: 'codex', modelID: 'gpt-5.4' },
83
83
  codex: { sandbox: 'read-only', reasoningEffort: 'high' },
84
84
  ```
85
85
 
86
+ **Oh My Pi** - Uses the `omp` CLI in headless JSON print mode.
87
+
88
+ ```typescript
89
+ defaultModel: { backend: 'omp', modelID: 'gpt-5.5' },
90
+ omp: { command: 'omp', approvalMode: 'yolo', thinking: 'high' },
91
+ ```
92
+
86
93
  **Pi** - Uses the `pi` coding-agent CLI JSONL RPC mode.
87
94
 
88
95
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "SDK for LLM pipelines with OpenCode, Codex, and Zod",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -43,18 +43,18 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
- "@anthropic-ai/claude-agent-sdk": "^0.3.0",
46
+ "@anthropic-ai/claude-agent-sdk": "^0.3.197",
47
47
  "@eslint/js": "^10.0.1",
48
- "@openai/codex-sdk": "0.141.0",
49
- "@typescript/native-preview": "^7.0.0-dev.20260506.1",
50
- "bun-types": "^1.3.13",
51
- "eslint": "^10.3.0",
52
- "globals": "^17.6.0",
48
+ "@openai/codex-sdk": "0.142.5",
49
+ "@typescript/native-preview": "^7.0.0-dev.20260630.1",
50
+ "bun-types": "^1.3.14",
51
+ "eslint": "^10.6.0",
52
+ "globals": "^17.7.0",
53
53
  "jiti": "^2.7.0",
54
- "prettier": "^3.8.3",
54
+ "prettier": "^3.9.4",
55
55
  "typescript": "^6.0.3",
56
- "typescript-eslint": "^8.59.2",
57
- "vitest": "^4.1.5"
56
+ "typescript-eslint": "^8.62.1",
57
+ "vitest": "^4.1.9"
58
58
  },
59
59
  "scripts": {
60
60
  "lint": "eslint .",
package/src/agent.ts CHANGED
@@ -9,6 +9,8 @@ import { spawn } from 'child_process'
9
9
  import { mkdir, writeFile, unlink } from 'fs/promises'
10
10
  import { join } from 'path'
11
11
  import { PROJECT_ROOT, TMP_DIR } from './paths.js'
12
+ import { runOmpAgent } from './omp.js'
13
+ import { runPiAgent } from './pi.js'
12
14
  import type { RunAgentOptions, RunAgentResult } from './types.js'
13
15
 
14
16
  /** Get command and args to invoke opencode from PATH */
@@ -23,21 +25,25 @@ export async function runAgent(
23
25
  const backend = options.model.backend ?? 'opencode'
24
26
 
25
27
  if (backend === 'claude-code') {
26
- // Dynamic import to avoid requiring @anthropic-ai/claude-agent-sdk when using opencode
28
+ // Dynamic import is required: @anthropic-ai/claude-agent-sdk is an optional peer absent for other backends.
27
29
  const { runClaudeCodeAgent } = await import('./claude-code.js')
28
30
  return runClaudeCodeAgent(options)
29
31
  }
30
32
 
31
33
  if (backend === 'codex') {
34
+ // Dynamic import is required: @openai/codex-sdk is an optional peer absent for other backends.
32
35
  const { runCodexAgent } = await import('./codex.js')
33
36
  return runCodexAgent(options)
34
37
  }
35
38
 
36
39
  if (backend === 'pi') {
37
- const { runPiAgent } = await import('./pi.js')
38
40
  return runPiAgent(options)
39
41
  }
40
42
 
43
+ if (backend === 'omp') {
44
+ return runOmpAgent(options)
45
+ }
46
+
41
47
  return runOpencodeAgent(options)
42
48
  }
43
49
 
package/src/index.ts CHANGED
@@ -93,6 +93,7 @@ export type {
93
93
  PermissionMode,
94
94
  ClaudeCodeOptions,
95
95
  PiOptions,
96
+ OmpOptions,
96
97
  CodexApprovalPolicy,
97
98
  CodexConfigValue,
98
99
  CodexOptions,
package/src/omp.ts ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * ocpipe Oh My Pi integration.
3
+ *
4
+ * Runs Oh My Pi through its headless JSON print mode.
5
+ */
6
+
7
+ import { spawn, type ChildProcess } from 'child_process'
8
+ import { isAbsolute, join } from 'path'
9
+ import { PROJECT_ROOT } from './paths.js'
10
+ import type { OmpOptions, RunAgentOptions, RunAgentResult } from './types.js'
11
+
12
+ interface OmpProcessRequest {
13
+ command: string
14
+ args: string[]
15
+ cwd: string
16
+ env: NodeJS.ProcessEnv
17
+ signal?: AbortSignal
18
+ }
19
+
20
+ interface OmpProcessResult {
21
+ stdout: string
22
+ stderr: string
23
+ exitCode: number | null
24
+ signal: NodeJS.Signals | null
25
+ }
26
+
27
+ export interface OmpProcess {
28
+ run(req: OmpProcessRequest): Promise<OmpProcessResult>
29
+ }
30
+
31
+ interface OmpRunSummary {
32
+ sawJsonEvent: boolean
33
+ sessionId: string
34
+ finalMessage: string
35
+ }
36
+
37
+ const defaultOmpCommand = 'omp'
38
+ const defaultOmpThinking = 'high'
39
+ const defaultOmpApprovalMode = 'yolo'
40
+
41
+ /** runOmpAgent executes an Oh My Pi coding-agent turn. */
42
+ export async function runOmpAgent(
43
+ options: RunAgentOptions,
44
+ processRunner: OmpProcess = commandOmpProcess,
45
+ ): Promise<RunAgentResult> {
46
+ const {
47
+ prompt,
48
+ model,
49
+ sessionId,
50
+ timeoutSec = 3600,
51
+ workdir,
52
+ omp,
53
+ signal,
54
+ } = options
55
+
56
+ if (signal?.aborted) {
57
+ throw new Error('Request aborted')
58
+ }
59
+
60
+ const cwd = workdir ?? PROJECT_ROOT
61
+ const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
62
+ const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
63
+ console.error(
64
+ `\n>>> OMP [${model.modelID}] ${sessionInfo}: ${promptPreview}...`,
65
+ )
66
+
67
+ const abort = new AbortController()
68
+ const abortHandler = () => abort.abort()
69
+ signal?.addEventListener('abort', abortHandler, { once: true })
70
+ let timedOut = false
71
+ const timeout =
72
+ timeoutSec > 0 ?
73
+ setTimeout(() => {
74
+ timedOut = true
75
+ abort.abort()
76
+ }, timeoutSec * 1000)
77
+ : null
78
+
79
+ try {
80
+ const result = await processRunner.run({
81
+ command: omp?.command ?? defaultOmpCommand,
82
+ args: buildOmpArgs(model.modelID, cwd, sessionId, prompt, omp),
83
+ cwd: omp?.processCwd ?? cwd,
84
+ env: buildOmpEnv(omp),
85
+ signal: abort.signal,
86
+ })
87
+ const summary = parseOmpOutput(result.stdout)
88
+ const detail =
89
+ result.signal ? `signal ${result.signal}` : `status ${result.exitCode}`
90
+ if (result.exitCode !== 0 || result.signal) {
91
+ const message = firstNonEmpty(
92
+ summary.finalMessage,
93
+ result.stderr.trim(),
94
+ result.stdout.trim(),
95
+ detail,
96
+ )
97
+ throw new Error(`OMP exited with ${detail}: ${message}`)
98
+ }
99
+
100
+ const response = firstNonEmpty(
101
+ summary.finalMessage,
102
+ summary.sawJsonEvent ? '' : result.stdout.trim(),
103
+ )
104
+ if (!response) {
105
+ throw new Error('OMP returned an empty final message')
106
+ }
107
+
108
+ const nextSessionId = firstNonEmpty(summary.sessionId, sessionId ?? '')
109
+ console.error(
110
+ `<<< OMP done (${response.length} chars)${nextSessionId ? ` [session:${nextSessionId}]` : ''}`,
111
+ )
112
+ return {
113
+ text: response,
114
+ sessionId: nextSessionId,
115
+ }
116
+ } catch (err) {
117
+ if (timedOut) {
118
+ throw new Error(`Timeout after ${timeoutSec}s`, { cause: err })
119
+ }
120
+ if (signal?.aborted) {
121
+ throw new Error('Request aborted', { cause: err })
122
+ }
123
+ throw err
124
+ } finally {
125
+ if (timeout) clearTimeout(timeout)
126
+ signal?.removeEventListener('abort', abortHandler)
127
+ }
128
+ }
129
+
130
+ function buildOmpArgs(
131
+ modelID: string,
132
+ cwd: string,
133
+ sessionId: string | undefined,
134
+ prompt: string,
135
+ omp: OmpOptions | undefined,
136
+ ): string[] {
137
+ const args = ['--print', '--mode', 'json', '--cwd', cwd]
138
+ if (modelID) {
139
+ args.push('--model', modelID)
140
+ }
141
+ if (hasOwn(omp, 'codexHome')) {
142
+ args.push('--codex-home', omp?.codexHome ?? '')
143
+ }
144
+ if (sessionId) {
145
+ args.push('--resume', sessionId)
146
+ }
147
+ if (omp?.goalMode || firstNonEmpty(omp?.goalObjective ?? '')) {
148
+ const objective = firstNonEmpty(omp?.goalObjective ?? '', prompt)
149
+ if (objective) {
150
+ args.push('--goal', objective)
151
+ }
152
+ }
153
+ if (
154
+ typeof omp?.contextStopPercent === 'number' &&
155
+ omp.contextStopPercent > 0
156
+ ) {
157
+ args.push('--context-stop-percent', String(omp.contextStopPercent))
158
+ }
159
+ if (typeof omp?.contextStopTokens === 'number' && omp.contextStopTokens > 0) {
160
+ args.push('--context-stop-tokens', String(omp.contextStopTokens))
161
+ }
162
+ if (omp?.scratchHandoffFile) {
163
+ args.push(
164
+ '--scratch-handoff-file',
165
+ resolveOmpPath(cwd, omp.scratchHandoffFile),
166
+ )
167
+ }
168
+ if (omp?.autoApprove ?? true) {
169
+ args.push('--auto-approve')
170
+ }
171
+ const approvalMode = omp?.approvalMode ?? defaultOmpApprovalMode
172
+ if (approvalMode) {
173
+ args.push('--approval-mode', approvalMode)
174
+ }
175
+ const thinking = omp?.thinking ?? defaultOmpThinking
176
+ if (thinking) {
177
+ args.push('--thinking', thinking)
178
+ }
179
+ args.push(...(omp?.extraArgs ?? []))
180
+ if (prompt) {
181
+ args.push('--', prompt)
182
+ }
183
+ return args
184
+ }
185
+
186
+ function buildOmpEnv(omp: OmpOptions | undefined): NodeJS.ProcessEnv {
187
+ const env: NodeJS.ProcessEnv = { ...process.env, ...omp?.env }
188
+ if (hasOwn(omp, 'codexHome')) {
189
+ delete env.CODEX_HOME
190
+ }
191
+ if (omp?.home) {
192
+ env.OMP_HOME = omp.home
193
+ }
194
+ return env
195
+ }
196
+
197
+ function resolveOmpPath(cwd: string, path: string): string {
198
+ return isAbsolute(path) ? path : join(cwd, path)
199
+ }
200
+
201
+ function parseOmpOutput(stdout: string): OmpRunSummary {
202
+ const summary: OmpRunSummary = {
203
+ sawJsonEvent: false,
204
+ sessionId: '',
205
+ finalMessage: '',
206
+ }
207
+ for (const rawLine of stdout.split('\n')) {
208
+ const line = rawLine.trim()
209
+ if (!line) continue
210
+ let event: unknown
211
+ try {
212
+ event = JSON.parse(line)
213
+ } catch {
214
+ continue
215
+ }
216
+ if (!isRecord(event)) continue
217
+ summary.sawJsonEvent = true
218
+ const type = ompString(event.type)
219
+ if (type === 'session') {
220
+ summary.sessionId = firstNonEmpty(
221
+ ompString(event.id),
222
+ ompString(event.sessionId),
223
+ ompString(event.session_id),
224
+ summary.sessionId,
225
+ )
226
+ continue
227
+ }
228
+ if (type === 'message_end') {
229
+ const text = ompAssistantText(event.message)
230
+ if (text) {
231
+ summary.finalMessage = text
232
+ }
233
+ }
234
+ }
235
+ if (!summary.sawJsonEvent) {
236
+ summary.finalMessage = stdout.trim()
237
+ }
238
+ return summary
239
+ }
240
+
241
+ function ompAssistantText(raw: unknown): string {
242
+ if (!isRecord(raw) || ompString(raw.role) !== 'assistant') return ''
243
+ if (Array.isArray(raw.content)) {
244
+ const parts: string[] = []
245
+ for (const rawPart of raw.content) {
246
+ if (!isRecord(rawPart) || ompString(rawPart.type) !== 'text') continue
247
+ const text = ompString(rawPart.text).trim()
248
+ if (text) parts.push(text)
249
+ }
250
+ return parts.join('\n\n')
251
+ }
252
+ return ompString(raw.content).trim()
253
+ }
254
+
255
+ const commandOmpProcess: OmpProcess = {
256
+ run(req) {
257
+ const { promise, resolve, reject } =
258
+ Promise.withResolvers<OmpProcessResult>()
259
+ let child: ChildProcess
260
+ try {
261
+ child = spawn(req.command, req.args, {
262
+ cwd: req.cwd,
263
+ env: req.env,
264
+ stdio: ['ignore', 'pipe', 'pipe'],
265
+ })
266
+ } catch (err) {
267
+ reject(err)
268
+ return promise
269
+ }
270
+
271
+ const stdoutChunks: Buffer[] = []
272
+ const stderrChunks: Buffer[] = []
273
+ const abortHandler = () => child.kill()
274
+ req.signal?.addEventListener('abort', abortHandler, { once: true })
275
+
276
+ child.stdout?.on('data', (chunk: Buffer) => stdoutChunks.push(chunk))
277
+ child.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk))
278
+ child.on('error', reject)
279
+ child.on('close', (exitCode, signal) => {
280
+ req.signal?.removeEventListener('abort', abortHandler)
281
+ resolve({
282
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
283
+ stderr: Buffer.concat(stderrChunks).toString('utf8'),
284
+ exitCode,
285
+ signal,
286
+ })
287
+ })
288
+ return promise
289
+ },
290
+ }
291
+
292
+ function hasOwn<T extends object, K extends PropertyKey>(
293
+ value: T | undefined,
294
+ key: K,
295
+ ): value is T & Record<K, unknown> {
296
+ return !!value && Object.prototype.hasOwnProperty.call(value, key)
297
+ }
298
+
299
+ function isRecord(value: unknown): value is Record<string, unknown> {
300
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
301
+ }
302
+
303
+ function ompString(value: unknown): string {
304
+ return typeof value === 'string' ? value : ''
305
+ }
306
+
307
+ function firstNonEmpty(...values: string[]): string {
308
+ for (const value of values) {
309
+ const trimmed = value.trim()
310
+ if (trimmed) return trimmed
311
+ }
312
+ return ''
313
+ }
package/src/pipeline.ts CHANGED
@@ -36,6 +36,7 @@ export class Pipeline<S extends BaseState> {
36
36
  claudeCode: config.claudeCode,
37
37
  codex: config.codex,
38
38
  pi: config.pi,
39
+ omp: config.omp,
39
40
  }
40
41
  }
41
42
 
package/src/predict.ts CHANGED
@@ -79,6 +79,7 @@ export class Predict<S extends AnySignature> {
79
79
  claudeCode: ctx.claudeCode,
80
80
  codex: ctx.codex,
81
81
  pi: ctx.pi,
82
+ omp: ctx.omp,
82
83
  signal: ctx.signal,
83
84
  })
84
85
 
@@ -211,6 +212,7 @@ export class Predict<S extends AnySignature> {
211
212
  claudeCode: ctx.claudeCode,
212
213
  codex: ctx.codex,
213
214
  pi: ctx.pi,
215
+ omp: ctx.omp,
214
216
  signal: ctx.signal,
215
217
  })
216
218
 
@@ -290,6 +292,7 @@ export class Predict<S extends AnySignature> {
290
292
  claudeCode: ctx.claudeCode,
291
293
  codex: ctx.codex,
292
294
  pi: ctx.pi,
295
+ omp: ctx.omp,
293
296
  signal: ctx.signal,
294
297
  })
295
298
 
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' | 'codex' | 'pi'
12
+ export type BackendType = 'opencode' | 'claude-code' | 'codex' | 'pi' | 'omp'
13
13
 
14
14
  /** Reasoning effort for Codex SDK threads. */
15
15
  export type CodexReasoningEffort =
@@ -133,6 +133,38 @@ export interface PiOptions {
133
133
  env?: Record<string, string>
134
134
  }
135
135
 
136
+ /** Oh My Pi CLI specific session options. */
137
+ export interface OmpOptions {
138
+ /** Path to the Oh My Pi executable (default: `omp` from PATH). */
139
+ command?: string
140
+ /** Process cwd used to launch Oh My Pi; defaults to the target workdir. */
141
+ processCwd?: string
142
+ /** OMP_HOME passed to the Oh My Pi subprocess. */
143
+ home?: string
144
+ /** CODEX_HOME value passed via `--codex-home`; set to empty string to clear it. */
145
+ codexHome?: string
146
+ /** Extra environment variables passed to the Oh My Pi subprocess. */
147
+ env?: Record<string, string>
148
+ /** Add `--auto-approve` (default: true). */
149
+ autoApprove?: boolean
150
+ /** Approval mode passed with `--approval-mode` (default: `yolo`). */
151
+ approvalMode?: string
152
+ /** Thinking effort passed with `--thinking` (default: `high`). */
153
+ thinking?: string
154
+ /** Run Oh My Pi as a continuing headless goal. */
155
+ goalMode?: boolean
156
+ /** Goal objective; setting this also enables goal mode. */
157
+ goalObjective?: string
158
+ /** Stop when context reaches this percent of the model window. */
159
+ contextStopPercent?: number
160
+ /** Stop when context reaches this token count. */
161
+ contextStopTokens?: number
162
+ /** Scratch handoff path passed to Oh My Pi; relative paths resolve against workdir. */
163
+ scratchHandoffFile?: string
164
+ /** Extra raw Oh My Pi arguments appended before the prompt separator. */
165
+ extraArgs?: string[]
166
+ }
167
+
136
168
  /** Model configuration for LLM backends. */
137
169
  export interface ModelConfig {
138
170
  /** Backend to use (default: 'opencode'). */
@@ -168,6 +200,8 @@ export interface ExecutionContext {
168
200
  codex?: CodexOptions
169
201
  /** Pi coding agent specific options. */
170
202
  pi?: PiOptions
203
+ /** Oh My Pi specific options. */
204
+ omp?: OmpOptions
171
205
  /** AbortSignal for cancelling in-flight backend requests. */
172
206
  signal?: AbortSignal
173
207
  }
@@ -390,6 +424,8 @@ export interface PipelineConfig {
390
424
  codex?: CodexOptions
391
425
  /** Pi coding agent specific options. */
392
426
  pi?: PiOptions
427
+ /** Oh My Pi specific options. */
428
+ omp?: OmpOptions
393
429
  }
394
430
 
395
431
  /** Options for running a pipeline step. */
@@ -428,6 +464,8 @@ export interface RunAgentOptions {
428
464
  codex?: CodexOptions
429
465
  /** Pi coding agent specific options. */
430
466
  pi?: PiOptions
467
+ /** Oh My Pi specific options. */
468
+ omp?: OmpOptions
431
469
  /** AbortSignal for cancelling the request. */
432
470
  signal?: AbortSignal
433
471
  }