ocpipe 0.3.3 → 0.3.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/package.json +4 -3
- package/src/agent.ts +184 -167
- package/src/pipeline.ts +1 -0
- package/src/predict.ts +3 -0
- package/src/types.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocpipe",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "SDK for LLM pipelines with OpenCode and Zod",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"bun": ">=1.0.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"opencode-ai": "1.1.
|
|
32
|
+
"opencode-ai": "1.1.8"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"zod": "4.3.5"
|
|
@@ -43,12 +43,13 @@
|
|
|
43
43
|
"prettier": "^3.7.4",
|
|
44
44
|
"typescript": "^5.0.0",
|
|
45
45
|
"typescript-eslint": "^8.52.0",
|
|
46
|
+
"@typescript/native-preview": "^7.0.0-dev.20251226.1",
|
|
46
47
|
"vitest": "^4.0.0"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
50
|
"lint": "eslint .",
|
|
50
51
|
"format": "bun run prettier --write .",
|
|
51
|
-
"typecheck": "
|
|
52
|
+
"typecheck": "tsgo --noEmit",
|
|
52
53
|
"test": "vitest run",
|
|
53
54
|
"test:watch": "vitest",
|
|
54
55
|
"release": "npm run release:version && npm run release:commit",
|
package/src/agent.ts
CHANGED
|
@@ -4,187 +4,204 @@
|
|
|
4
4
|
* Wraps the OpenCode CLI for running LLM agents with session management.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn, execSync } from
|
|
8
|
-
import { mkdir } from
|
|
9
|
-
import { PROJECT_ROOT, TMP_DIR } from
|
|
10
|
-
import type { RunAgentOptions, RunAgentResult } from
|
|
7
|
+
import { spawn, execSync } from 'child_process'
|
|
8
|
+
import { mkdir } from 'fs/promises'
|
|
9
|
+
import { PROJECT_ROOT, TMP_DIR } from './paths.js'
|
|
10
|
+
import type { RunAgentOptions, RunAgentResult } from './types.js'
|
|
11
11
|
|
|
12
12
|
/** Check if opencode is available in system PATH */
|
|
13
13
|
function hasSystemOpencode(): boolean {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
try {
|
|
15
|
+
execSync('which opencode', { stdio: 'ignore' })
|
|
16
|
+
return true
|
|
17
|
+
} catch {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** Get command and args to invoke opencode */
|
|
23
23
|
function getOpencodeCommand(args: string[]): { cmd: string; args: string[] } {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
29
|
}
|
|
30
30
|
|
|
31
31
|
/** runAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
|
|
32
|
-
export async function runAgent(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
32
|
+
export async function runAgent(
|
|
33
|
+
options: RunAgentOptions,
|
|
34
|
+
): Promise<RunAgentResult> {
|
|
35
|
+
const { prompt, agent, model, sessionId, timeoutSec = 300, workdir } = options
|
|
36
|
+
|
|
37
|
+
const modelStr = `${model.providerID}/${model.modelID}`
|
|
38
|
+
const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
|
|
39
|
+
const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
|
|
40
|
+
|
|
41
|
+
console.error(
|
|
42
|
+
`\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const args = [
|
|
46
|
+
'run',
|
|
47
|
+
'--format',
|
|
48
|
+
'default',
|
|
49
|
+
'--agent',
|
|
50
|
+
agent,
|
|
51
|
+
'--model',
|
|
52
|
+
modelStr,
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
if (sessionId) {
|
|
56
|
+
args.push('--session', sessionId)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const opencodeCmd = getOpencodeCommand(args)
|
|
61
|
+
const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
|
|
62
|
+
cwd: workdir ?? PROJECT_ROOT,
|
|
63
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
let newSessionId = sessionId || ''
|
|
67
|
+
const stdoutChunks: string[] = []
|
|
68
|
+
|
|
69
|
+
// Stream stderr in real-time (OpenCode progress output)
|
|
70
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
71
|
+
const text = data.toString()
|
|
72
|
+
|
|
73
|
+
// Parse session ID from output
|
|
74
|
+
for (const line of text.split('\n')) {
|
|
75
|
+
if (line.startsWith('[session:')) {
|
|
76
|
+
newSessionId = line.trim().slice(9, -1)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
// Filter noise
|
|
80
|
+
if (line.includes('baseline-browser-mapping')) continue
|
|
81
|
+
if (line.startsWith('$ bun run')) continue
|
|
82
|
+
if (line.trim()) {
|
|
83
|
+
process.stderr.write(line + '\n')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Collect stdout
|
|
89
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
90
|
+
const text = data.toString()
|
|
91
|
+
stdoutChunks.push(text)
|
|
92
|
+
process.stderr.write(text)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Send prompt to stdin
|
|
96
|
+
proc.stdin.write(prompt)
|
|
97
|
+
proc.stdin.end()
|
|
98
|
+
|
|
99
|
+
// Timeout handling (0 = no timeout)
|
|
100
|
+
const timeout =
|
|
101
|
+
timeoutSec > 0 ?
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
proc.kill()
|
|
104
|
+
reject(new Error(`Timeout after ${timeoutSec}s`))
|
|
105
|
+
}, timeoutSec * 1000)
|
|
106
|
+
: null
|
|
107
|
+
|
|
108
|
+
proc.on('close', async (code) => {
|
|
109
|
+
if (timeout) clearTimeout(timeout)
|
|
110
|
+
|
|
111
|
+
if (code !== 0) {
|
|
112
|
+
reject(new Error(`OpenCode exited with code ${code}`))
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Export session to get structured response
|
|
117
|
+
let response = stdoutChunks.join('').trim()
|
|
118
|
+
|
|
119
|
+
if (newSessionId) {
|
|
120
|
+
const exported = await exportSession(newSessionId, workdir)
|
|
121
|
+
if (exported) {
|
|
122
|
+
response = exported
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const sessionStr = newSessionId || 'none'
|
|
127
|
+
console.error(
|
|
128
|
+
`<<< OpenCode done (${response.length} chars) [session:${sessionStr}]`,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
resolve({
|
|
132
|
+
text: response,
|
|
133
|
+
sessionId: newSessionId,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
proc.on('error', (err) => {
|
|
138
|
+
if (timeout) clearTimeout(timeout)
|
|
139
|
+
reject(err)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
/** exportSession exports a session and extracts assistant text responses. */
|
|
131
|
-
async function exportSession(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
145
|
+
async function exportSession(
|
|
146
|
+
sessionId: string,
|
|
147
|
+
workdir?: string,
|
|
148
|
+
): Promise<string | null> {
|
|
149
|
+
const tmpPath = `${TMP_DIR}/opencode_export_${Date.now()}.json`
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await mkdir(TMP_DIR, { recursive: true })
|
|
153
|
+
const opencodeCmd = getOpencodeCommand([
|
|
154
|
+
'session',
|
|
155
|
+
'export',
|
|
156
|
+
sessionId,
|
|
157
|
+
'--format',
|
|
158
|
+
'json',
|
|
159
|
+
'-o',
|
|
160
|
+
tmpPath,
|
|
161
|
+
])
|
|
162
|
+
const proc = Bun.spawn([opencodeCmd.cmd, ...opencodeCmd.args], {
|
|
163
|
+
cwd: workdir ?? PROJECT_ROOT,
|
|
164
|
+
stdout: 'pipe',
|
|
165
|
+
stderr: 'pipe',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await proc.exited
|
|
169
|
+
|
|
170
|
+
const file = Bun.file(tmpPath)
|
|
171
|
+
if (!(await file.exists())) return null
|
|
172
|
+
|
|
173
|
+
const data = (await file.json()) as {
|
|
174
|
+
messages?: Array<{
|
|
175
|
+
info?: { role?: string }
|
|
176
|
+
parts?: Array<{ type?: string; text?: string }>
|
|
177
|
+
}>
|
|
178
|
+
}
|
|
179
|
+
await Bun.write(tmpPath, '') // Clean up
|
|
180
|
+
|
|
181
|
+
// Extract all assistant text parts
|
|
182
|
+
const messages = data.messages || []
|
|
183
|
+
const textParts: string[] = []
|
|
184
|
+
|
|
185
|
+
for (const msg of messages) {
|
|
186
|
+
if (msg.info?.role === 'assistant') {
|
|
187
|
+
for (const part of msg.parts || []) {
|
|
188
|
+
if (part.type === 'text' && part.text) {
|
|
189
|
+
textParts.push(part.text)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return textParts.length > 0 ? textParts.join('\n') : null
|
|
196
|
+
} catch {
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
/** logStep logs a step header for workflow progress. */
|
|
185
|
-
export function logStep(step: number, title: string, detail =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
export function logStep(step: number, title: string, detail = ''): void {
|
|
203
|
+
const detailStr = detail ? ` (${detail})` : ''
|
|
204
|
+
console.log(`\n${'='.repeat(60)}`)
|
|
205
|
+
console.log(`STEP ${step}: ${title}${detailStr}`)
|
|
206
|
+
console.log('='.repeat(60))
|
|
190
207
|
}
|
package/src/pipeline.ts
CHANGED
package/src/predict.ts
CHANGED
|
@@ -75,6 +75,7 @@ export class Predict<S extends AnySignature> {
|
|
|
75
75
|
model: this.config.model ?? ctx.defaultModel,
|
|
76
76
|
sessionId: this.config.newSession ? undefined : ctx.sessionId,
|
|
77
77
|
timeoutSec: ctx.timeoutSec,
|
|
78
|
+
workdir: ctx.workdir,
|
|
78
79
|
})
|
|
79
80
|
|
|
80
81
|
// Update context with new session ID for continuity
|
|
@@ -202,6 +203,7 @@ export class Predict<S extends AnySignature> {
|
|
|
202
203
|
sessionId: correctionModel ? undefined : sessionId,
|
|
203
204
|
agent: ctx.defaultAgent,
|
|
204
205
|
timeoutSec: 60,
|
|
206
|
+
workdir: ctx.workdir,
|
|
205
207
|
})
|
|
206
208
|
|
|
207
209
|
// Try to parse the repaired JSON
|
|
@@ -276,6 +278,7 @@ export class Predict<S extends AnySignature> {
|
|
|
276
278
|
sessionId: correctionModel ? undefined : sessionId,
|
|
277
279
|
agent: ctx.defaultAgent,
|
|
278
280
|
timeoutSec: 60, // Short timeout for simple patches
|
|
281
|
+
workdir: ctx.workdir,
|
|
279
282
|
})
|
|
280
283
|
|
|
281
284
|
// Extract and apply the patch based on method
|
package/src/types.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface ExecutionContext {
|
|
|
28
28
|
defaultAgent: string
|
|
29
29
|
/** Timeout in seconds for agent calls. */
|
|
30
30
|
timeoutSec: number
|
|
31
|
+
/** Working directory for opencode (where .opencode/agents/ lives). */
|
|
32
|
+
workdir?: string
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
// ============================================================================
|
|
@@ -240,6 +242,8 @@ export interface PipelineConfig {
|
|
|
240
242
|
retry?: RetryConfig
|
|
241
243
|
/** Default timeout in seconds. */
|
|
242
244
|
timeoutSec?: number
|
|
245
|
+
/** Working directory for opencode (where .opencode/agents/ lives). */
|
|
246
|
+
workdir?: string
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
/** Options for running a pipeline step. */
|
|
@@ -270,6 +274,8 @@ export interface RunAgentOptions {
|
|
|
270
274
|
sessionId?: string
|
|
271
275
|
/** Timeout in seconds. */
|
|
272
276
|
timeoutSec?: number
|
|
277
|
+
/** Working directory for opencode (where .opencode/agents/ lives). */
|
|
278
|
+
workdir?: string
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
/** Result from running an OpenCode agent. */
|