ocpipe 0.6.9 → 0.6.11
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 +19 -15
- package/example/correction.ts +1 -2
- package/example/index.ts +6 -7
- package/package.json +13 -12
- package/src/agent.ts +184 -209
- package/src/claude-code.ts +19 -24
- package/src/index.ts +3 -3
- package/src/omp.ts +313 -0
- package/src/pipeline.ts +8 -7
- package/src/predict.ts +9 -8
- package/src/testing.ts +7 -5
- package/src/types.ts +75 -42
package/src/agent.ts
CHANGED
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ocpipe agent integration.
|
|
3
3
|
*
|
|
4
|
-
* Dispatches to
|
|
5
|
-
* configuration.
|
|
4
|
+
* Dispatches prompts to configured coding-agent backends.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { spawn } from 'child_process'
|
|
9
|
-
import {
|
|
8
|
+
import { randomUUID } from 'crypto'
|
|
9
|
+
import { mkdir, unlink, writeFile } from 'fs/promises'
|
|
10
10
|
import { join } from 'path'
|
|
11
11
|
import { PROJECT_ROOT, TMP_DIR } from './paths.js'
|
|
12
12
|
import type { RunAgentOptions, RunAgentResult } from './types.js'
|
|
13
|
+
import { runOmpAgent } from './omp.js'
|
|
14
|
+
import { runPiAgent } from './pi.js'
|
|
13
15
|
|
|
14
|
-
/**
|
|
15
|
-
function getOpencodeCommand(args: string[]): { cmd: string; args: string[] } {
|
|
16
|
-
return { cmd: 'opencode', args }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** runAgent dispatches to the appropriate backend based on model configuration. */
|
|
16
|
+
/** runAgent dispatches to the selected backend. */
|
|
20
17
|
export async function runAgent(
|
|
21
18
|
options: RunAgentOptions,
|
|
22
19
|
): Promise<RunAgentResult> {
|
|
23
|
-
const backend = options.model.backend ?? '
|
|
20
|
+
const backend = options.model.backend ?? 'omp'
|
|
24
21
|
|
|
25
22
|
if (backend === 'claude-code') {
|
|
26
|
-
// Dynamic import to avoid requiring @anthropic-ai/claude-agent-sdk when using opencode
|
|
27
23
|
const { runClaudeCodeAgent } = await import('./claude-code.js')
|
|
28
24
|
return runClaudeCodeAgent(options)
|
|
29
25
|
}
|
|
@@ -33,57 +29,60 @@ export async function runAgent(
|
|
|
33
29
|
return runCodexAgent(options)
|
|
34
30
|
}
|
|
35
31
|
|
|
32
|
+
if (backend === 'opencode') {
|
|
33
|
+
return runOpencodeAgent(options)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
36
|
if (backend === 'pi') {
|
|
37
|
-
const { runPiAgent } = await import('./pi.js')
|
|
38
37
|
return runPiAgent(options)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
if (backend === 'omp') {
|
|
41
|
+
return runOmpAgent(options)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const unreachable: never = backend
|
|
45
|
+
throw new Error(`Unsupported backend: ${unreachable}`)
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
/** runOpencodeAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
|
|
45
48
|
async function runOpencodeAgent(
|
|
46
49
|
options: RunAgentOptions,
|
|
47
50
|
): Promise<RunAgentResult> {
|
|
48
51
|
const {
|
|
49
52
|
prompt,
|
|
50
|
-
agent,
|
|
51
53
|
model,
|
|
52
54
|
sessionId,
|
|
53
55
|
timeoutSec = 3600,
|
|
54
56
|
workdir,
|
|
57
|
+
opencode,
|
|
55
58
|
signal,
|
|
56
59
|
} = options
|
|
57
60
|
|
|
58
61
|
if (!model.providerID) {
|
|
59
62
|
throw new Error('providerID is required for OpenCode backend')
|
|
60
63
|
}
|
|
61
|
-
const modelStr = `${model.providerID}/${model.modelID}`
|
|
62
|
-
const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
|
|
63
|
-
const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
|
|
64
64
|
|
|
65
|
-
console.error(
|
|
66
|
-
`\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// Check if already aborted
|
|
70
65
|
if (signal?.aborted) {
|
|
71
66
|
throw new Error('Request aborted')
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
// Write prompt to .opencode/prompts/ within the working directory
|
|
75
69
|
const cwd = workdir ?? PROJECT_ROOT
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
70
|
+
const modelStr = `${model.providerID}/${model.modelID}`
|
|
71
|
+
const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
|
|
72
|
+
const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
|
|
73
|
+
console.error(
|
|
74
|
+
`\n>>> OpenCode [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const promptDir = opencode?.promptDir ?? TMP_DIR
|
|
78
|
+
await mkdir(promptDir, { recursive: true })
|
|
79
|
+
const promptFile = join(promptDir, `ocpipe_prompt_${randomUUID()}.txt`)
|
|
79
80
|
await writeFile(promptFile, prompt)
|
|
80
81
|
|
|
81
82
|
const args = [
|
|
82
83
|
'run',
|
|
83
84
|
'--format',
|
|
84
85
|
'default',
|
|
85
|
-
'--agent',
|
|
86
|
-
agent,
|
|
87
86
|
'--model',
|
|
88
87
|
modelStr,
|
|
89
88
|
'--prompt-file',
|
|
@@ -93,11 +92,9 @@ async function runOpencodeAgent(
|
|
|
93
92
|
if (sessionId) {
|
|
94
93
|
args.push('--session', sessionId)
|
|
95
94
|
}
|
|
96
|
-
|
|
97
95
|
if (model.variant) {
|
|
98
96
|
args.push('--model-variant', model.variant)
|
|
99
97
|
}
|
|
100
|
-
|
|
101
98
|
if (model.variantThinkingBudget !== undefined) {
|
|
102
99
|
args.push(
|
|
103
100
|
'--model-variant-thinking-budget',
|
|
@@ -105,194 +102,164 @@ async function runOpencodeAgent(
|
|
|
105
102
|
)
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
let newSessionId = sessionId || ''
|
|
119
|
-
const stdoutChunks: string[] = []
|
|
120
|
-
const stderrChunks: string[] = []
|
|
121
|
-
let aborted = false
|
|
122
|
-
|
|
123
|
-
// Handle abort signal - kill subprocess when aborted
|
|
124
|
-
const abortHandler = async () => {
|
|
125
|
-
if (aborted) return
|
|
126
|
-
aborted = true
|
|
127
|
-
console.error(`\n[abort] Killing OpenCode subprocess...`)
|
|
128
|
-
proc.kill('SIGTERM')
|
|
129
|
-
// Give it a moment to clean up, then force kill
|
|
130
|
-
setTimeout(() => {
|
|
131
|
-
if (!proc.killed) proc.kill('SIGKILL')
|
|
132
|
-
}, 1000)
|
|
133
|
-
await unlink(promptFile).catch(() => {})
|
|
134
|
-
reject(new Error('Request aborted'))
|
|
135
|
-
}
|
|
136
|
-
signal?.addEventListener('abort', abortHandler, { once: true })
|
|
137
|
-
|
|
138
|
-
// Stream stderr in real-time (OpenCode progress output)
|
|
139
|
-
proc.stderr.on('data', (data: Buffer) => {
|
|
140
|
-
const text = data.toString()
|
|
141
|
-
stderrChunks.push(text)
|
|
142
|
-
|
|
143
|
-
// Parse session ID from output
|
|
144
|
-
for (const line of text.split('\n')) {
|
|
145
|
-
if (line.startsWith('[session:')) {
|
|
146
|
-
newSessionId = line.trim().slice(9, -1)
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
// Filter noise
|
|
150
|
-
if (line.includes('baseline-browser-mapping')) continue
|
|
151
|
-
if (line.startsWith('$ bun run')) continue
|
|
152
|
-
if (line.trim()) {
|
|
153
|
-
process.stderr.write(line + '\n')
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
// Collect stdout
|
|
159
|
-
proc.stdout.on('data', (data: Buffer) => {
|
|
160
|
-
const text = data.toString()
|
|
161
|
-
stdoutChunks.push(text)
|
|
162
|
-
process.stderr.write(text)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// Timeout handling (0 = no timeout)
|
|
166
|
-
const timeout =
|
|
167
|
-
timeoutSec > 0 ?
|
|
168
|
-
setTimeout(async () => {
|
|
169
|
-
proc.kill()
|
|
170
|
-
await unlink(promptFile).catch(() => {})
|
|
171
|
-
reject(new Error(`Timeout after ${timeoutSec}s`))
|
|
172
|
-
}, timeoutSec * 1000)
|
|
173
|
-
: null
|
|
174
|
-
|
|
175
|
-
proc.on('close', async (code) => {
|
|
176
|
-
if (timeout) clearTimeout(timeout)
|
|
177
|
-
signal?.removeEventListener('abort', abortHandler)
|
|
178
|
-
|
|
179
|
-
// If aborted, we already rejected
|
|
180
|
-
if (aborted) return
|
|
181
|
-
|
|
182
|
-
// Clean up prompt file
|
|
183
|
-
await unlink(promptFile).catch(() => {})
|
|
184
|
-
|
|
185
|
-
const stderr = stderrChunks.join('').trim()
|
|
186
|
-
|
|
187
|
-
if (code !== 0) {
|
|
188
|
-
const lastLines = stderr.split('\n').slice(-5).join('\n')
|
|
189
|
-
const detail = lastLines ? `\n${lastLines}` : ''
|
|
190
|
-
reject(new Error(`OpenCode exited with code ${code}${detail}`))
|
|
191
|
-
return
|
|
192
|
-
}
|
|
105
|
+
const command = opencode?.command ?? 'opencode'
|
|
106
|
+
console.error(`[DEBUG] Running: ${command} ${args.join(' ')}`)
|
|
107
|
+
const proc = spawn(command, args, {
|
|
108
|
+
cwd,
|
|
109
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
+
env: { ...process.env, ...opencode?.env },
|
|
111
|
+
})
|
|
193
112
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const errorLines = stderr
|
|
213
|
-
.split('\n')
|
|
214
|
-
.filter(
|
|
215
|
-
(line) =>
|
|
216
|
-
pattern.test(line) ||
|
|
217
|
-
line.includes('Error') ||
|
|
218
|
-
line.includes('error:'),
|
|
219
|
-
)
|
|
220
|
-
.slice(0, 5)
|
|
221
|
-
.join('\n')
|
|
222
|
-
reject(new Error(`OpenCode ${message}:\n${errorLines}`))
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
}
|
|
113
|
+
const { promise, resolve, reject } = Promise.withResolvers<RunAgentResult>()
|
|
114
|
+
let newSessionId = sessionId || ''
|
|
115
|
+
const stdoutChunks: string[] = []
|
|
116
|
+
const stderrChunks: string[] = []
|
|
117
|
+
let aborted = false
|
|
118
|
+
|
|
119
|
+
const abortHandler = () => {
|
|
120
|
+
if (aborted) return
|
|
121
|
+
aborted = true
|
|
122
|
+
console.error(`\n[abort] Killing OpenCode subprocess...`)
|
|
123
|
+
proc.kill('SIGTERM')
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
if (!proc.killed) proc.kill('SIGKILL')
|
|
126
|
+
}, 1000)
|
|
127
|
+
void unlink(promptFile).catch(() => {})
|
|
128
|
+
reject(new Error('Request aborted'))
|
|
129
|
+
}
|
|
130
|
+
signal?.addEventListener('abort', abortHandler, { once: true })
|
|
226
131
|
|
|
227
|
-
|
|
228
|
-
|
|
132
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
133
|
+
const text = data.toString()
|
|
134
|
+
stderrChunks.push(text)
|
|
229
135
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
136
|
+
for (const line of text.split('\n')) {
|
|
137
|
+
if (line.startsWith('[session:')) {
|
|
138
|
+
newSessionId = line.trim().slice(9, -1)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if (line.includes('baseline-browser-mapping')) continue
|
|
142
|
+
if (line.startsWith('$ bun run')) continue
|
|
143
|
+
if (line.trim()) {
|
|
144
|
+
process.stderr.write(line + '\n')
|
|
235
145
|
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
150
|
+
const text = data.toString()
|
|
151
|
+
stdoutChunks.push(text)
|
|
152
|
+
process.stderr.write(text)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const timeout =
|
|
156
|
+
timeoutSec > 0 ?
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
proc.kill()
|
|
159
|
+
void unlink(promptFile).catch(() => {})
|
|
160
|
+
reject(new Error(`Timeout after ${timeoutSec}s`))
|
|
161
|
+
}, timeoutSec * 1000)
|
|
162
|
+
: undefined
|
|
163
|
+
|
|
164
|
+
proc.on('close', async (code) => {
|
|
165
|
+
clearTimeout(timeout)
|
|
166
|
+
signal?.removeEventListener('abort', abortHandler)
|
|
167
|
+
if (aborted) return
|
|
168
|
+
|
|
169
|
+
void unlink(promptFile).catch(() => {})
|
|
170
|
+
const stderr = stderrChunks.join('').trim()
|
|
171
|
+
|
|
172
|
+
if (code !== 0) {
|
|
173
|
+
const lastLines = stderr.split('\n').slice(-5).join('\n')
|
|
174
|
+
const detail = lastLines ? `\n${lastLines}` : ''
|
|
175
|
+
reject(new Error(`OpenCode exited with code ${code}${detail}`))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
236
178
|
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
179
|
+
for (const { pattern, message } of knownOpenCodeErrors) {
|
|
180
|
+
if (pattern.test(stderr)) {
|
|
181
|
+
const errorLines = stderr
|
|
182
|
+
.split('\n')
|
|
183
|
+
.filter(
|
|
184
|
+
(line) =>
|
|
185
|
+
pattern.test(line) ||
|
|
186
|
+
line.includes('Error') ||
|
|
187
|
+
line.includes('error:'),
|
|
188
|
+
)
|
|
189
|
+
.slice(0, 5)
|
|
190
|
+
.join('\n')
|
|
191
|
+
reject(new Error(`OpenCode ${message}:\n${errorLines}`))
|
|
245
192
|
return
|
|
246
193
|
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let response = stdoutChunks.join('').trim()
|
|
197
|
+
if (newSessionId) {
|
|
198
|
+
response =
|
|
199
|
+
(await exportSession(newSessionId, cwd, command, opencode?.env)) ??
|
|
200
|
+
response
|
|
201
|
+
}
|
|
247
202
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
203
|
+
if (response.length === 0 && stderr.includes('Error')) {
|
|
204
|
+
const lastLines = stderr.split('\n').slice(-10).join('\n')
|
|
205
|
+
reject(
|
|
206
|
+
new Error(
|
|
207
|
+
`OpenCode returned empty response with errors:\n${lastLines}`,
|
|
208
|
+
),
|
|
251
209
|
)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const sessionStr = newSessionId || 'none'
|
|
214
|
+
console.error(
|
|
215
|
+
`<<< OpenCode done (${response.length} chars) [session:${sessionStr}]`,
|
|
216
|
+
)
|
|
217
|
+
resolve({ text: response, sessionId: newSessionId })
|
|
218
|
+
})
|
|
252
219
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
proc.on('error', async (err) => {
|
|
260
|
-
if (timeout) clearTimeout(timeout)
|
|
261
|
-
await unlink(promptFile).catch(() => {})
|
|
262
|
-
reject(err)
|
|
263
|
-
})
|
|
220
|
+
proc.on('error', (err) => {
|
|
221
|
+
clearTimeout(timeout)
|
|
222
|
+
signal?.removeEventListener('abort', abortHandler)
|
|
223
|
+
void unlink(promptFile).catch(() => {})
|
|
224
|
+
reject(err)
|
|
264
225
|
})
|
|
226
|
+
|
|
227
|
+
return promise
|
|
265
228
|
}
|
|
266
229
|
|
|
267
|
-
/** exportSession exports a session and extracts assistant text responses. */
|
|
268
230
|
async function exportSession(
|
|
269
231
|
sessionId: string,
|
|
270
|
-
workdir
|
|
232
|
+
workdir: string,
|
|
233
|
+
command: string,
|
|
234
|
+
env: Record<string, string> | undefined,
|
|
271
235
|
): Promise<string | null> {
|
|
272
|
-
const
|
|
236
|
+
const exportFile = join(TMP_DIR, `opencode_export_${randomUUID()}.json`)
|
|
273
237
|
|
|
274
238
|
try {
|
|
275
239
|
await mkdir(TMP_DIR, { recursive: true })
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
240
|
+
const proc = Bun.spawn(
|
|
241
|
+
[
|
|
242
|
+
command,
|
|
243
|
+
'session',
|
|
244
|
+
'export',
|
|
245
|
+
sessionId,
|
|
246
|
+
'--format',
|
|
247
|
+
'json',
|
|
248
|
+
'--turn',
|
|
249
|
+
'-1',
|
|
250
|
+
'-o',
|
|
251
|
+
exportFile,
|
|
252
|
+
],
|
|
253
|
+
{
|
|
254
|
+
cwd: workdir,
|
|
255
|
+
stdout: 'pipe',
|
|
256
|
+
stderr: 'pipe',
|
|
257
|
+
env: { ...process.env, ...env },
|
|
258
|
+
},
|
|
259
|
+
)
|
|
292
260
|
|
|
293
261
|
await proc.exited
|
|
294
|
-
|
|
295
|
-
const file = Bun.file(tmpPath)
|
|
262
|
+
const file = Bun.file(exportFile)
|
|
296
263
|
if (!(await file.exists())) return null
|
|
297
264
|
|
|
298
265
|
const data = (await file.json()) as {
|
|
@@ -301,29 +268,37 @@ async function exportSession(
|
|
|
301
268
|
parts?: Array<{ type?: string; text?: string }>
|
|
302
269
|
}>
|
|
303
270
|
}
|
|
304
|
-
|
|
271
|
+
void unlink(exportFile).catch(() => {})
|
|
305
272
|
|
|
306
|
-
// Extract all assistant text parts from the exported turn.
|
|
307
|
-
// The --turn -1 flag ensures we only get the last turn's messages.
|
|
308
|
-
const messages = data.messages || []
|
|
309
273
|
const textParts: string[] = []
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
textParts.push(part.text)
|
|
316
|
-
}
|
|
274
|
+
for (const msg of data.messages ?? []) {
|
|
275
|
+
if (msg.info?.role !== 'assistant') continue
|
|
276
|
+
for (const part of msg.parts ?? []) {
|
|
277
|
+
if (part.type === 'text' && part.text) {
|
|
278
|
+
textParts.push(part.text)
|
|
317
279
|
}
|
|
318
280
|
}
|
|
319
281
|
}
|
|
320
282
|
|
|
321
283
|
return textParts.length > 0 ? textParts.join('\n') : null
|
|
322
284
|
} catch {
|
|
285
|
+
void unlink(exportFile).catch(() => {})
|
|
323
286
|
return null
|
|
324
287
|
}
|
|
325
288
|
}
|
|
326
289
|
|
|
290
|
+
const knownOpenCodeErrors: ReadonlyArray<{ pattern: RegExp; message: string }> =
|
|
291
|
+
[
|
|
292
|
+
{
|
|
293
|
+
pattern: /ProviderModelNotFoundError/,
|
|
294
|
+
message: 'Provider/model not found',
|
|
295
|
+
},
|
|
296
|
+
{ pattern: /ModelNotFoundError/, message: 'Model not found' },
|
|
297
|
+
{ pattern: /ProviderNotFoundError/, message: 'Provider not found' },
|
|
298
|
+
{ pattern: /API key.*not.*found/i, message: 'API key not configured' },
|
|
299
|
+
{ pattern: /authentication.*failed/i, message: 'Authentication failed' },
|
|
300
|
+
]
|
|
301
|
+
|
|
327
302
|
/** logStep logs a step header for workflow progress. */
|
|
328
303
|
export function logStep(step: number, title: string, detail = ''): void {
|
|
329
304
|
const detailStr = detail ? ` (${detail})` : ''
|
package/src/claude-code.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { execSync } from 'child_process'
|
|
14
|
-
import { existsSync
|
|
14
|
+
import { existsSync } from 'fs'
|
|
15
15
|
import { join } from 'path'
|
|
16
16
|
import { homedir } from 'os'
|
|
17
17
|
import {
|
|
@@ -66,9 +66,14 @@ function resolveClaudeCodePath(explicit?: string): string | undefined {
|
|
|
66
66
|
if (explicit) return explicit
|
|
67
67
|
|
|
68
68
|
try {
|
|
69
|
-
const found = execSync('which claude', {
|
|
69
|
+
const found = execSync('which claude', {
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
timeout: 3000,
|
|
72
|
+
}).trim()
|
|
70
73
|
if (found) return found
|
|
71
|
-
} catch {
|
|
74
|
+
} catch {
|
|
75
|
+
/* which not found or timed out — fall through to defaults */
|
|
76
|
+
}
|
|
72
77
|
|
|
73
78
|
const defaults = [
|
|
74
79
|
join(homedir(), '.local', 'bin', 'claude'),
|
|
@@ -90,21 +95,6 @@ function normalizeModelId(modelId: string): string {
|
|
|
90
95
|
return modelId
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
/** loadAgentDefinition loads an OpenCode agent definition file and extracts the
|
|
94
|
-
* markdown body (stripping the YAML frontmatter) for use as a system prompt. */
|
|
95
|
-
function loadAgentDefinition(agent: string, workdir?: string): string | undefined {
|
|
96
|
-
if (!agent || !workdir) return undefined
|
|
97
|
-
|
|
98
|
-
const agentPath = join(workdir, '.opencode', 'agents', `${agent}.md`)
|
|
99
|
-
if (!existsSync(agentPath)) return undefined
|
|
100
|
-
|
|
101
|
-
const content = readFileSync(agentPath, 'utf8')
|
|
102
|
-
|
|
103
|
-
// Strip YAML frontmatter (--- delimited block at the start).
|
|
104
|
-
const stripped = content.replace(/^---\n[\s\S]*?\n---\n*/, '')
|
|
105
|
-
return stripped.trim() || undefined
|
|
106
|
-
}
|
|
107
|
-
|
|
108
98
|
/** Extract text from assistant messages. */
|
|
109
99
|
function getAssistantText(msg: SDKMessage): string | null {
|
|
110
100
|
if (msg.type !== 'assistant') return null
|
|
@@ -147,7 +137,6 @@ export async function runClaudeCodeAgent(
|
|
|
147
137
|
): Promise<RunAgentResult> {
|
|
148
138
|
const {
|
|
149
139
|
prompt,
|
|
150
|
-
agent,
|
|
151
140
|
model,
|
|
152
141
|
sessionId,
|
|
153
142
|
timeoutSec = 3600,
|
|
@@ -169,8 +158,8 @@ export async function runClaudeCodeAgent(
|
|
|
169
158
|
// Build query options with configurable permission mode (default: acceptEdits)
|
|
170
159
|
const permissionMode = claudeCode?.permissionMode ?? 'acceptEdits'
|
|
171
160
|
|
|
172
|
-
// Resolve system prompt
|
|
173
|
-
const systemPrompt = claudeCode?.systemPrompt
|
|
161
|
+
// Resolve system prompt from explicit configuration only.
|
|
162
|
+
const systemPrompt = claudeCode?.systemPrompt
|
|
174
163
|
|
|
175
164
|
// Bridge external abort signal to an AbortController for the SDK
|
|
176
165
|
const abortController = new AbortController()
|
|
@@ -217,16 +206,22 @@ export async function runClaudeCodeAgent(
|
|
|
217
206
|
}),
|
|
218
207
|
// Resolve executable path: explicit option > PATH lookup > default locations
|
|
219
208
|
...(() => {
|
|
220
|
-
const resolved = resolveClaudeCodePath(
|
|
209
|
+
const resolved = resolveClaudeCodePath(
|
|
210
|
+
claudeCode?.pathToClaudeCodeExecutable,
|
|
211
|
+
)
|
|
221
212
|
return resolved ? { pathToClaudeCodeExecutable: resolved } : {}
|
|
222
213
|
})(),
|
|
223
214
|
}
|
|
224
215
|
|
|
225
216
|
if (claudeCode?.agents) {
|
|
226
|
-
console.error(
|
|
217
|
+
console.error(
|
|
218
|
+
`[ocpipe] Subagents defined: ${Object.keys(claudeCode.agents).join(', ')}`,
|
|
219
|
+
)
|
|
227
220
|
}
|
|
228
221
|
if (claudeCode?.allowedTools) {
|
|
229
|
-
console.error(
|
|
222
|
+
console.error(
|
|
223
|
+
`[ocpipe] Allowed tools: ${claudeCode.allowedTools.join(', ')}`,
|
|
224
|
+
)
|
|
230
225
|
}
|
|
231
226
|
console.error(
|
|
232
227
|
`\n>>> Claude Code [${modelStr}] [${permissionMode}] ${sessionInfo}: ${promptPreview}...`,
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ocpipe: LLM pipelines with
|
|
2
|
+
* ocpipe: LLM pipelines with coding-agent backends and Zod.
|
|
3
3
|
*
|
|
4
4
|
* Inspired by DSPy.
|
|
5
5
|
*
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
* // Run in a pipeline
|
|
23
23
|
* const pipeline = new Pipeline({
|
|
24
24
|
* name: 'my-workflow',
|
|
25
|
-
* defaultModel: {
|
|
26
|
-
* defaultAgent: 'general',
|
|
25
|
+
* defaultModel: { backend: 'omp', modelID: 'gpt-5.5' },
|
|
27
26
|
* checkpointDir: './ckpt',
|
|
28
27
|
* logDir: './logs',
|
|
29
28
|
* }, createBaseState)
|
|
@@ -93,6 +92,7 @@ export type {
|
|
|
93
92
|
PermissionMode,
|
|
94
93
|
ClaudeCodeOptions,
|
|
95
94
|
PiOptions,
|
|
95
|
+
OmpOptions,
|
|
96
96
|
CodexApprovalPolicy,
|
|
97
97
|
CodexConfigValue,
|
|
98
98
|
CodexOptions,
|