ocpipe 0.1.0 → 0.3.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Christian Stewart <christian@aperture.us>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,334 +1,60 @@
1
- # DSTS
1
+ <p align="center"><strong>ocpipe</strong></p>
2
+ <p align="center">SDK for LLM pipelines with <a href="https://github.com/sst/opencode">OpenCode</a> and <a href="https://zod.dev">Zod</a>.</p>
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/ocpipe"><img alt="npm" src="https://img.shields.io/npm/v/ocpipe?style=flat-square" /></a>
5
+ <a href="https://github.com/s4wave/ocpipe/actions"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/s4wave/ocpipe/tests.yml?style=flat-square&branch=master" /></a>
6
+ </p>
2
7
 
3
- <div align="center">
4
- <h3>Declarative Self-Improving TypeScript</h3>
5
- <p>A DSPy-inspired SDK for building LLM workflow pipelines with <a href="https://opencode.ai">OpenCode</a>.</p>
6
- <p>
7
- <a href="https://github.com/s4wave/dsts">GitHub</a> |
8
- <a href="https://github.com/s4wave/dsts/blob/main/GETTING_STARTED.md">Getting Started</a> |
9
- <a href="https://github.com/s4wave/dsts/blob/main/LICENSE">MIT License</a>
10
- </p>
11
- </div>
8
+ ---
12
9
 
13
- <div align="center">
14
-
15
- ```
16
- Signature → Predict → Module → Pipeline
17
- │ │ │ │
18
- what execute compose orchestrate
19
- ```
20
-
21
- </div>
22
-
23
- DSTS separates the **what** (Signatures declare input/output contracts), the **how** (Modules compose predictors), and the **when** (Pipelines orchestrate execution). This separation enables clean composition, rich debugging, and maintainable LLM workflow code.
24
-
25
- ## Features
26
-
27
- - **Type-safe signatures** - Define input/output contracts with Zod schemas
28
- - **Automatic prompt generation** - Signatures become structured prompts
29
- - **JSON output parsing** - Automatic extraction and validation of JSON responses
30
- - **Session continuity** - Reuse OpenCode sessions across steps
31
- - **Checkpointing** - Automatic state persistence after each step
32
- - **Retry logic** - Configurable retries with parse error handling
33
- - **Sub-pipelines** - Compose complex workflows from smaller pieces
34
- - **Testing utilities** - Mock backends for unit testing
35
-
36
- ## Quick Start
10
+ ### Quick Start
37
11
 
38
12
  ```typescript
39
- import { signature, field, SignatureModule, Pipeline, createBaseState } from 'dsts'
40
- import { z } from 'zod'
13
+ import { signature, field, module, Pipeline, createBaseState } from 'ocpipe'
41
14
 
42
- // 1. Define a signature (the contract)
43
- const ParseIntent = signature({
44
- doc: 'Parse user intent from a natural language description.',
15
+ const Greet = signature({
16
+ doc: 'Generate a friendly greeting for the given name.',
45
17
  inputs: {
46
- description: field.string('User description in natural language'),
18
+ name: field.string('The name of the person to greet'),
47
19
  },
48
20
  outputs: {
49
- intent: field.string('Parsed intent category'),
50
- confidence: field.number('Confidence score 0-1'),
51
- keywords: field.array(z.string(), 'Extracted keywords'),
21
+ greeting: field.string('A friendly greeting message'),
22
+ emoji: field.string('An appropriate emoji for the greeting'),
52
23
  },
53
24
  })
54
25
 
55
- // 2. Create a module (the logic)
56
- class IntentParser extends SignatureModule<typeof ParseIntent> {
57
- constructor() {
58
- super(ParseIntent)
59
- }
60
-
61
- async forward(input, ctx) {
62
- const result = await this.predictor.execute(input, ctx)
63
- return result.data // Full signature output
64
- }
65
- }
66
-
67
- // 3. Run in a pipeline (the orchestration)
68
- const pipeline = new Pipeline({
69
- name: 'my-workflow',
70
- defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
71
- defaultAgent: 'general',
72
- checkpointDir: './ckpt',
73
- logDir: './logs',
74
- }, createBaseState)
75
-
76
- const result = await pipeline.run(new IntentParser(), { description: 'Hello world' })
77
- console.log(result.data.intent)
78
- ```
79
-
80
- ## Core Concepts
81
-
82
- ### Signatures
83
-
84
- A Signature declares **what** an LLM interaction does - its inputs, outputs, and purpose. This is separate from *how* it executes.
85
-
86
- ```typescript
87
- import { signature, field } from 'dsts'
88
- import { z } from 'zod'
89
-
90
- const AnalyzeCode = signature({
91
- doc: 'Analyze code for potential issues and improvements.',
92
- inputs: {
93
- code: field.string('Source code to analyze'),
94
- language: field.enum(['typescript', 'python', 'rust'] as const),
26
+ const pipeline = new Pipeline(
27
+ {
28
+ name: 'hello-world',
29
+ defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
30
+ defaultAgent: 'code',
31
+ checkpointDir: './ckpt',
32
+ logDir: './logs',
95
33
  },
96
- outputs: {
97
- issues: field.array(z.object({
98
- severity: z.enum(['error', 'warning', 'info']),
99
- message: z.string(),
100
- line: z.number(),
101
- }), 'List of issues found'),
102
- suggestions: field.array(z.string(), 'Improvement suggestions'),
103
- score: field.number('Code quality score 0-100'),
104
- },
105
- })
106
- ```
107
-
108
- **Field helpers:**
109
- - `field.string(desc?)` - String field
110
- - `field.number(desc?)` - Number field
111
- - `field.boolean(desc?)` - Boolean field
112
- - `field.array(itemType, desc?)` - Array field
113
- - `field.object(shape, desc?)` - Object field
114
- - `field.enum(values, desc?)` - Enum field
115
- - `field.optional(field)` - Optional wrapper
116
- - `field.nullable(field)` - Nullable wrapper
117
- - `field.custom(zodType, desc?)` - Custom Zod type
118
-
119
- ### Predict
120
-
121
- `Predict` is the bridge between a Signature (the contract) and OpenCode (the execution). It handles prompt generation, response parsing, and validation.
122
-
123
- ```typescript
124
- import { Predict } from 'dsts'
125
-
126
- // Basic usage
127
- const predict = new Predict(AnalyzeCode)
128
- const result = await predict.execute({ code: '...', language: 'typescript' }, ctx)
129
-
130
- // With configuration
131
- const predict = new Predict(AnalyzeCode, {
132
- agent: 'code-reviewer', // Override default agent
133
- model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' },
134
- newSession: true, // Don't reuse existing session
135
- template: (inputs) => `...`, // Custom prompt template
136
- })
137
- ```
138
-
139
- **Output format:**
140
-
141
- The LLM is prompted to return a JSON object:
142
- ```
143
- OUTPUT FORMAT:
144
- Return a JSON object with EXACTLY these field names and types.
145
-
146
- ```json
147
- {
148
- "issues": <array<object{severity, message, line}>>, // List of issues found
149
- "suggestions": <array<string>>, // Improvement suggestions
150
- "score": <number> // Code quality score 0-100
151
- }
152
- ```
153
- ```
154
-
155
- ### Module
156
-
157
- A Module encapsulates a logical unit of work that may use one or more Predictors. Modules can call other Modules, enabling composition.
158
-
159
- **SignatureModule** - For simple modules that wrap a single signature with pass-through types:
160
-
161
- ```typescript
162
- import { SignatureModule } from 'dsts'
163
-
164
- class IntentParser extends SignatureModule<typeof ParseIntent> {
165
- constructor() {
166
- super(ParseIntent)
167
- }
34
+ createBaseState,
35
+ )
168
36
 
169
- async forward(input, ctx) {
170
- const result = await this.predictor.execute(input, ctx)
171
- return result.data // Types inferred from ParseIntent
172
- }
173
- }
37
+ const result = await pipeline.run(module(Greet), { name: 'World' })
38
+ console.log(result.data.greeting) // "Hello, World! It's wonderful to meet you!"
174
39
  ```
175
40
 
176
- **Module** - For complex modules with multiple predictors or transformed outputs:
41
+ ### Installation
177
42
 
178
- ```typescript
179
- import { Module } from 'dsts'
180
-
181
- class CodeAnalyzer extends Module<
182
- { code: string; language: string },
183
- { issues: Issue[]; score: number }
184
- > {
185
- private analyze = this.predict(AnalyzeCode)
186
- private suggest = this.predict(SuggestFixes, { agent: 'code-fixer' })
187
-
188
- async forward(input: { code: string; language: string }, ctx: ExecutionContext) {
189
- // First, analyze the code
190
- const analysis = await this.analyze.execute(input, ctx)
191
-
192
- // If there are critical issues, get fix suggestions
193
- if (analysis.data.issues.some(i => i.severity === 'error')) {
194
- const fixes = await this.suggest.execute({
195
- code: input.code,
196
- issues: analysis.data.issues,
197
- }, ctx)
198
-
199
- return {
200
- issues: analysis.data.issues,
201
- fixes: fixes.data.suggestions,
202
- score: analysis.data.score,
203
- }
204
- }
205
-
206
- return {
207
- issues: analysis.data.issues,
208
- score: analysis.data.score,
209
- }
210
- }
211
- }
212
- ```
213
-
214
- ### Pipeline
215
-
216
- Pipeline is the top-level orchestrator. It manages execution context, state, checkpointing, logging, and retry logic.
217
-
218
- ```typescript
219
- import { Pipeline, createBaseState } from 'dsts'
220
-
221
- // Create pipeline with configuration
222
- const pipeline = new Pipeline({
223
- name: 'code-review',
224
- defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
225
- defaultAgent: 'general',
226
- checkpointDir: './ckpt',
227
- logDir: './logs',
228
- retry: { maxAttempts: 2, onParseError: true },
229
- timeoutSec: 300,
230
- }, createBaseState)
231
-
232
- // Run modules
233
- const result = await pipeline.run(new CodeAnalyzer(), {
234
- code: sourceCode,
235
- language: 'typescript',
236
- })
237
-
238
- // Run with step options
239
- const result = await pipeline.run(new CodeAnalyzer(), input, {
240
- name: 'analyze-main', // Custom step name
241
- model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' }, // Override model
242
- newSession: true, // Fresh session
243
- retry: { maxAttempts: 3 }, // Override retry
244
- })
245
-
246
- // Access state
247
- console.log(pipeline.state.steps) // Completed steps
248
- console.log(pipeline.getSessionId()) // Current OpenCode session
249
-
250
- // Resume from checkpoint
251
- const resumed = await Pipeline.loadCheckpoint(config, sessionId)
252
- ```
253
-
254
- ### State Management
255
-
256
- DSTS automatically checkpoints state after each step:
257
-
258
- ```typescript
259
- import { createBaseState, extendBaseState } from 'dsts'
260
-
261
- // Basic state
262
- const state = createBaseState()
263
- // { sessionId, startedAt, phase, steps, subPipelines }
264
-
265
- // Extended state for your workflow
266
- interface MyState extends BaseState {
267
- inputPath: string
268
- results: AnalysisResult[]
269
- }
270
-
271
- const pipeline = new Pipeline(config, () => ({
272
- ...createBaseState(),
273
- inputPath: '/path/to/input',
274
- results: [],
275
- }))
276
- ```
277
-
278
- ## Testing
279
-
280
- DSTS provides testing utilities for unit testing without hitting real LLMs:
281
-
282
- ```typescript
283
- import { MockAgentBackend, createMockContext, generateMockOutputs } from 'dsts'
284
- import { vi } from 'vitest'
285
-
286
- // Create mock backend
287
- const mock = new MockAgentBackend()
288
-
289
- // Add mock responses
290
- mock.addJsonResponse({
291
- intent: 'greeting',
292
- confidence: 0.95,
293
- keywords: ['hello', 'world'],
294
- })
295
-
296
- // Mock the agent module
297
- vi.mock('./agent.js', () => ({
298
- runAgent: mock.createRunner(),
299
- }))
300
-
301
- // Create test context
302
- const ctx = createMockContext({
303
- defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
304
- })
305
-
306
- // Auto-generate mock outputs from schema
307
- const mockData = generateMockOutputs(ParseIntent.outputs)
43
+ ```bash
44
+ bun init
45
+ bun add ocpipe
308
46
  ```
309
47
 
310
- ## Why Not ChainOfThought or ReAct?
311
-
312
- Unlike DSPy, DSTS does not provide `ChainOfThought` or `ReAct` variants. This is intentional:
48
+ OpenCode CLI is bundled — run `bun run opencode` or use your system `opencode` if installed (preferred).
313
49
 
314
- - **OpenCode agents already do chain-of-thought reasoning** - they think before acting
315
- - **OpenCode agents already do ReAct** - they have access to tools and use them iteratively
316
- - **Adding these would duplicate functionality** and create confusion
50
+ See [example/](./example) for a complete example.
317
51
 
318
- If you need tool access, configure your OpenCode agent appropriately. The agent handles the complexity; DSTS just structures the input/output contract.
52
+ ### Documentation
319
53
 
320
- ## Requirements
321
-
322
- - [Bun](https://bun.sh) runtime
323
- - [OpenCode](https://opencode.ai) CLI installed and configured
324
- - [Zod](https://zod.dev) for schema validation
325
-
326
- ## Installation
327
-
328
- ```bash
329
- bun add dsts zod
330
- ```
54
+ - [Getting Started](./GETTING_STARTED.md) - Tutorial with examples
55
+ - [Design](./DESIGN.md) - Architecture and concepts
56
+ - [Contributing](./CONTRIBUTING.md) - Development setup
331
57
 
332
- ## License
58
+ ---
333
59
 
334
- MIT - see [LICENSE](https://github.com/s4wave/dsts/blob/main/LICENSE) for details.
60
+ **Join the OpenCode community** [Discord](https://opencode.ai/discord) | follow on [X.com](https://x.com/opencode)
@@ -0,0 +1,27 @@
1
+ {
2
+ "sessionId": "20251227_044217",
3
+ "startedAt": "2025-12-27T04:42:17.756Z",
4
+ "phase": "init",
5
+ "steps": [
6
+ {
7
+ "stepName": "Greeter",
8
+ "timestamp": "2025-12-27T04:42:22.145Z",
9
+ "result": {
10
+ "data": {
11
+ "greeting": "Hello, World! Welcome!",
12
+ "emoji": "👋"
13
+ },
14
+ "stepName": "Greeter",
15
+ "duration": 4389,
16
+ "sessionId": "ses_4a1e2aaceffedog5Q2374azn79",
17
+ "model": {
18
+ "providerID": "anthropic",
19
+ "modelID": "claude-haiku-4-5"
20
+ },
21
+ "attempt": 1
22
+ }
23
+ }
24
+ ],
25
+ "subPipelines": [],
26
+ "opencodeSessionId": "ses_4a1e2aaceffedog5Q2374azn79"
27
+ }
@@ -14,9 +14,15 @@
14
14
  * bun run example/correction.ts --jq # Uses jq method
15
15
  */
16
16
 
17
- import { z } from 'zod'
18
- import { Pipeline, createBaseState, signature, field, SignatureModule } from '../index.js'
19
- import type { CorrectionMethod, ExecutionContext } from '../types.js'
17
+ import { z } from 'zod/v4'
18
+ import {
19
+ Pipeline,
20
+ createBaseState,
21
+ signature,
22
+ field,
23
+ SignatureModule,
24
+ } from '../src/index.js'
25
+ import type { CorrectionMethod, ExecutionContext } from '../src/types.js'
20
26
 
21
27
  // A signature with field names that LLMs often get wrong
22
28
  const AnalyzeIssue = signature({
@@ -28,9 +34,15 @@ IMPORTANT: Use the EXACT field names specified in the schema.`,
28
34
  },
29
35
  outputs: {
30
36
  // LLMs often return "type" instead of "issue_type"
31
- issue_type: field.enum(['bug', 'feature', 'refactor', 'docs'] as const, 'Category of the issue'),
37
+ issue_type: field.enum(
38
+ ['bug', 'feature', 'refactor', 'docs'] as const,
39
+ 'Category of the issue',
40
+ ),
32
41
  // LLMs often return "priority" instead of "severity"
33
- severity: field.enum(['low', 'medium', 'high', 'critical'] as const, 'How severe is the issue'),
42
+ severity: field.enum(
43
+ ['low', 'medium', 'high', 'critical'] as const,
44
+ 'How severe is the issue',
45
+ ),
34
46
  // LLMs often return "description" or "reason" instead of "explanation"
35
47
  explanation: field.string('Detailed explanation of the issue'),
36
48
  // LLMs often return just "tags" or "labels"
@@ -53,7 +65,8 @@ class IssueAnalyzer extends SignatureModule<typeof AnalyzeIssue> {
53
65
 
54
66
  async function main() {
55
67
  // Check for --jq flag
56
- const method: CorrectionMethod = process.argv.includes('--jq') ? 'jq' : 'json-patch'
68
+ const method: CorrectionMethod =
69
+ process.argv.includes('--jq') ? 'jq' : 'json-patch'
57
70
 
58
71
  const pipeline = new Pipeline(
59
72
  {
@@ -72,7 +85,8 @@ async function main() {
72
85
  console.log('Watch the correction rounds fix schema mismatches.\n')
73
86
 
74
87
  const result = await pipeline.run(new IssueAnalyzer(method), {
75
- description: 'The login button does not respond when clicked on mobile devices',
88
+ description:
89
+ 'The login button does not respond when clicked on mobile devices',
76
90
  })
77
91
 
78
92
  console.log('\n=== Final Result ===')
package/example/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Demonstrates running a DSTS module in a pipeline.
5
5
  */
6
6
 
7
- import { Pipeline, createBaseState } from '../index.js'
7
+ import { Pipeline, createBaseState } from '../src/index.js'
8
8
  import { Greeter } from './module.js'
9
9
 
10
10
  async function main() {
package/example/module.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Wraps the Greet signature with execution logic.
5
5
  */
6
6
 
7
- import { SignatureModule } from '../index.js'
8
- import type { ExecutionContext } from '../types.js'
7
+ import { SignatureModule } from '../src/index.js'
8
+ import type { ExecutionContext } from '../src/types.js'
9
9
  import { Greet } from './signature.js'
10
10
 
11
11
  export class Greeter extends SignatureModule<typeof Greet> {
@@ -4,7 +4,7 @@
4
4
  * Defines the input/output contract for greeting generation.
5
5
  */
6
6
 
7
- import { signature, field } from '../index.js'
7
+ import { signature, field } from '../src/index.js'
8
8
 
9
9
  export const Greet = signature({
10
10
  doc: 'Generate a friendly greeting for the given name.',
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.1.0",
4
- "description": "Declarative Self-Improving TypeScript - A DSPy-inspired SDK for building LLM workflow pipelines with OpenCode",
3
+ "version": "0.3.0",
4
+ "description": "SDK for LLM pipelines with OpenCode and Zod",
5
5
  "type": "module",
6
- "main": "index.ts",
7
- "types": "index.ts",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/s4wave/dsts"
10
+ "url": "git+https://github.com/s4wave/ocpipe.git"
11
11
  },
12
- "homepage": "https://github.com/s4wave/dsts",
12
+ "homepage": "https://github.com/s4wave/ocpipe",
13
13
  "bugs": {
14
- "url": "https://github.com/s4wave/dsts/issues"
14
+ "url": "https://github.com/s4wave/ocpipe/issues"
15
15
  },
16
16
  "license": "MIT",
17
17
  "author": "s4wave",
@@ -28,21 +28,33 @@
28
28
  "engines": {
29
29
  "bun": ">=1.0.0"
30
30
  },
31
+ "dependencies": {
32
+ "opencode-ai": "latest"
33
+ },
31
34
  "peerDependencies": {
32
- "zod": "^3.24.0"
35
+ "zod": "^4.0.0"
33
36
  },
34
37
  "devDependencies": {
38
+ "bun-types": "^1.3.5",
39
+ "prettier": "^3.7.4",
35
40
  "typescript": "^5.0.0",
36
- "vitest": "^2.0.0"
41
+ "vitest": "^4.0.0"
37
42
  },
38
43
  "scripts": {
44
+ "format": "bun run prettier --write .",
39
45
  "typecheck": "tsc --noEmit",
40
46
  "test": "vitest run",
41
- "test:watch": "vitest"
47
+ "test:watch": "vitest",
48
+ "release": "npm run release:version && npm run release:commit",
49
+ "release:minor": "npm run release:version:minor && npm run release:commit",
50
+ "release:version": "npm version patch -m \"release: v%s\" --no-git-tag-version",
51
+ "release:version:minor": "npm version minor -m \"release: v%s\" --no-git-tag-version",
52
+ "release:commit": "git reset && git add package.json && git commit -s -m \"release: v$(node -p \"require('./package.json').version\")\" && git tag v$(node -p \"require('./package.json').version\")",
53
+ "release:publish": "git push && git push --tags && npm publish"
42
54
  },
43
55
  "files": [
44
- "*.ts",
45
- "!*.test.ts",
56
+ "src/",
57
+ "!src/*.test.ts",
46
58
  "example/"
47
59
  ]
48
60
  }
@@ -4,11 +4,30 @@
4
4
  * Wraps the OpenCode CLI for running LLM agents with session management.
5
5
  */
6
6
 
7
- import { spawn } from 'child_process'
7
+ import { spawn, execSync } from 'child_process'
8
8
  import { mkdir } from 'fs/promises'
9
- import { PROJECT_ROOT, TMP_DIR } from '../paths.js'
9
+ import { PROJECT_ROOT, TMP_DIR } from './paths.js'
10
10
  import type { RunAgentOptions, RunAgentResult } from './types.js'
11
11
 
12
+ /** Check if opencode is available in system PATH */
13
+ function hasSystemOpencode(): boolean {
14
+ try {
15
+ execSync('which opencode', { stdio: 'ignore' })
16
+ return true
17
+ } catch {
18
+ return false
19
+ }
20
+ }
21
+
22
+ /** Get command and args to invoke opencode */
23
+ function getOpencodeCommand(args: string[]): { cmd: string; args: string[] } {
24
+ if (hasSystemOpencode()) {
25
+ return { cmd: 'opencode', args }
26
+ }
27
+ // Fallback to bunx with ocpipe package (which has opencode-ai as dependency)
28
+ return { cmd: 'bunx', args: ['-p', 'ocpipe', 'opencode', ...args] }
29
+ }
30
+
12
31
  /** runAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
13
32
  export async function runAgent(
14
33
  options: RunAgentOptions,
@@ -23,14 +42,23 @@ export async function runAgent(
23
42
  `\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
24
43
  )
25
44
 
26
- const args = ['run', '--format', 'default', '--agent', agent, '--model', modelStr]
45
+ const args = [
46
+ 'run',
47
+ '--format',
48
+ 'default',
49
+ '--agent',
50
+ agent,
51
+ '--model',
52
+ modelStr,
53
+ ]
27
54
 
28
55
  if (sessionId) {
29
56
  args.push('--session', sessionId)
30
57
  }
31
58
 
32
59
  return new Promise((resolve, reject) => {
33
- const proc = spawn('opencode', args, {
60
+ const opencodeCmd = getOpencodeCommand(args)
61
+ const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
34
62
  cwd: PROJECT_ROOT,
35
63
  stdio: ['pipe', 'pipe', 'pipe'],
36
64
  })
@@ -116,23 +144,20 @@ async function exportSession(sessionId: string): Promise<string | null> {
116
144
 
117
145
  try {
118
146
  await mkdir(TMP_DIR, { recursive: true })
119
- const proc = Bun.spawn(
120
- [
121
- 'opencode',
122
- 'session',
123
- 'export',
124
- sessionId,
125
- '--format',
126
- 'json',
127
- '-o',
128
- tmpPath,
129
- ],
130
- {
131
- cwd: PROJECT_ROOT,
132
- stdout: 'pipe',
133
- stderr: 'pipe',
134
- },
135
- )
147
+ const opencodeCmd = getOpencodeCommand([
148
+ 'session',
149
+ 'export',
150
+ sessionId,
151
+ '--format',
152
+ 'json',
153
+ '-o',
154
+ tmpPath,
155
+ ])
156
+ const proc = Bun.spawn([opencodeCmd.cmd, ...opencodeCmd.args], {
157
+ cwd: PROJECT_ROOT,
158
+ stdout: 'pipe',
159
+ stderr: 'pipe',
160
+ })
136
161
 
137
162
  await proc.exited
138
163