ocpipe 0.3.9 → 0.4.1
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 +32 -4
- package/package.json +10 -5
- package/src/agent.ts +21 -34
- package/src/claude-code.ts +102 -0
- package/src/index.ts +1 -0
- package/src/parsing.ts +53 -17
- package/src/types.ts +6 -1
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> 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>, 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-
|
|
14
|
+
- **Multi-backend** Choose between OpenCode (75+ providers) or Claude Code SDK
|
|
15
15
|
- **Auto-correction** Fixes schema mismatches automatically
|
|
16
16
|
|
|
17
17
|
### Quick Start
|
|
@@ -47,15 +47,43 @@ type GreetIn = InferInputs<typeof Greet> // { name: string }
|
|
|
47
47
|
type GreetOut = InferOutputs<typeof Greet> // { greeting: string }
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
### Backends
|
|
51
|
+
|
|
52
|
+
ocpipe supports two backends for running LLM agents:
|
|
53
|
+
|
|
54
|
+
**OpenCode** (default) - Requires `opencode` CLI in your PATH. Supports 75+ providers.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const pipeline = new Pipeline({
|
|
58
|
+
name: 'my-pipeline',
|
|
59
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-20250514' },
|
|
60
|
+
defaultAgent: 'default',
|
|
61
|
+
}, createBaseState)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Claude Code** - Uses `@anthropic-ai/claude-agent-sdk`. Install as a peer dependency.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const pipeline = new Pipeline({
|
|
68
|
+
name: 'my-pipeline',
|
|
69
|
+
defaultModel: { backend: 'claude-code', providerID: 'anthropic', modelID: 'claude-sonnet-4-20250514' },
|
|
70
|
+
defaultAgent: 'default',
|
|
71
|
+
}, createBaseState)
|
|
72
|
+
```
|
|
51
73
|
|
|
52
74
|
### Requirements
|
|
53
75
|
|
|
54
|
-
Currently requires [this OpenCode fork](https://github.com/paralin/opencode). Once the following PRs are merged, the official release will work:
|
|
76
|
+
**For OpenCode backend:** Currently requires [this OpenCode fork](https://github.com/paralin/opencode). Once the following PRs are merged, the official release will work:
|
|
55
77
|
|
|
56
78
|
- [#5426](https://github.com/anomalyco/opencode/pull/5426) - Adds `--prompt-file` flag
|
|
57
79
|
- [#5339](https://github.com/anomalyco/opencode/pull/5339) - Session export fixes
|
|
58
80
|
|
|
81
|
+
**For Claude Code backend:** Install the SDK as a peer dependency:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bun add @anthropic-ai/claude-agent-sdk
|
|
85
|
+
```
|
|
86
|
+
|
|
59
87
|
### Documentation
|
|
60
88
|
|
|
61
89
|
- [Getting Started](./GETTING_STARTED.md) - Tutorial with examples
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocpipe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "SDK for LLM pipelines with OpenCode and Zod",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -28,13 +28,18 @@
|
|
|
28
28
|
"engines": {
|
|
29
29
|
"bun": ">=1.0.0"
|
|
30
30
|
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"opencode-ai": "1.1.25"
|
|
33
|
-
},
|
|
31
|
+
"dependencies": {},
|
|
34
32
|
"peerDependencies": {
|
|
35
|
-
"zod": "4.3.
|
|
33
|
+
"zod": "4.3.6",
|
|
34
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.19"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"@anthropic-ai/claude-agent-sdk": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
36
40
|
},
|
|
37
41
|
"devDependencies": {
|
|
42
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.14",
|
|
38
43
|
"@eslint/js": "^9.39.2",
|
|
39
44
|
"bun-types": "^1.3.5",
|
|
40
45
|
"eslint": "^9.39.2",
|
package/src/agent.ts
CHANGED
|
@@ -1,53 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ocpipe
|
|
2
|
+
* ocpipe agent integration.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Dispatches to OpenCode CLI or Claude Code SDK based on backend configuration.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { spawn } from 'child_process'
|
|
8
|
-
import { existsSync } from 'fs'
|
|
9
8
|
import { mkdir, writeFile, unlink } from 'fs/promises'
|
|
10
9
|
import { join } from 'path'
|
|
11
10
|
import { PROJECT_ROOT, TMP_DIR } from './paths.js'
|
|
12
11
|
import type { RunAgentOptions, RunAgentResult } from './types.js'
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
function
|
|
16
|
-
|
|
13
|
+
/** Get command and args to invoke opencode from PATH */
|
|
14
|
+
function getOpencodeCommand(args: string[]): { cmd: string; args: string[] } {
|
|
15
|
+
return { cmd: 'opencode', args }
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return candidate
|
|
24
|
-
}
|
|
25
|
-
}
|
|
18
|
+
/** runAgent dispatches to the appropriate backend based on model configuration. */
|
|
19
|
+
export async function runAgent(
|
|
20
|
+
options: RunAgentOptions,
|
|
21
|
+
): Promise<RunAgentResult> {
|
|
22
|
+
const backend = options.model.backend ?? 'opencode'
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (existsSync(candidate)) {
|
|
32
|
-
return candidate
|
|
33
|
-
}
|
|
24
|
+
if (backend === 'claude-code') {
|
|
25
|
+
// Dynamic import to avoid requiring @anthropic-ai/claude-agent-sdk when using opencode
|
|
26
|
+
const { runClaudeCodeAgent } = await import('./claude-code.js')
|
|
27
|
+
return runClaudeCodeAgent(options)
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Get command and args to invoke opencode */
|
|
40
|
-
function getOpencodeCommand(args: string[]): { cmd: string; args: string[] } {
|
|
41
|
-
const opencode = findOpencode()
|
|
42
|
-
if (opencode) {
|
|
43
|
-
return { cmd: opencode, args }
|
|
44
|
-
}
|
|
45
|
-
// Fallback to bunx with ocpipe package (which has opencode-ai as dependency)
|
|
46
|
-
return { cmd: 'bunx', args: ['-p', 'ocpipe', 'opencode', ...args] }
|
|
30
|
+
return runOpencodeAgent(options)
|
|
47
31
|
}
|
|
48
32
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
33
|
+
/** runOpencodeAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
|
|
34
|
+
async function runOpencodeAgent(
|
|
51
35
|
options: RunAgentOptions,
|
|
52
36
|
): Promise<RunAgentResult> {
|
|
53
37
|
const { prompt, agent, model, sessionId, timeoutSec = 300, workdir } = options
|
|
@@ -69,6 +53,8 @@ export async function runAgent(
|
|
|
69
53
|
|
|
70
54
|
const args = [
|
|
71
55
|
'run',
|
|
56
|
+
'--dir',
|
|
57
|
+
cwd,
|
|
72
58
|
'--format',
|
|
73
59
|
'default',
|
|
74
60
|
'--agent',
|
|
@@ -85,6 +71,7 @@ export async function runAgent(
|
|
|
85
71
|
|
|
86
72
|
return new Promise((resolve, reject) => {
|
|
87
73
|
const opencodeCmd = getOpencodeCommand(args)
|
|
74
|
+
console.error(`[DEBUG] Running: ${opencodeCmd.cmd} ${opencodeCmd.args.join(' ')}`)
|
|
88
75
|
const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
|
|
89
76
|
cwd,
|
|
90
77
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ocpipe Claude Code agent integration.
|
|
3
|
+
*
|
|
4
|
+
* Uses the Claude Agent SDK v2 for running LLM agents with session management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
unstable_v2_createSession,
|
|
9
|
+
unstable_v2_resumeSession,
|
|
10
|
+
type SDKMessage,
|
|
11
|
+
} from '@anthropic-ai/claude-agent-sdk'
|
|
12
|
+
import type { RunAgentOptions, RunAgentResult } from './types.js'
|
|
13
|
+
|
|
14
|
+
/** Extract text from assistant messages. */
|
|
15
|
+
function getAssistantText(msg: SDKMessage): string | null {
|
|
16
|
+
if (msg.type !== 'assistant') return null
|
|
17
|
+
const textParts: string[] = []
|
|
18
|
+
for (const block of msg.message.content) {
|
|
19
|
+
if (block.type === 'text') {
|
|
20
|
+
textParts.push(block.text)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return textParts.join('')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** runClaudeCodeAgent executes a Claude Code agent with a prompt. */
|
|
27
|
+
export async function runClaudeCodeAgent(
|
|
28
|
+
options: RunAgentOptions,
|
|
29
|
+
): Promise<RunAgentResult> {
|
|
30
|
+
const { prompt, model, sessionId, timeoutSec = 300 } = options
|
|
31
|
+
|
|
32
|
+
// Claude Agent SDK only uses modelID, not providerID
|
|
33
|
+
const modelStr = model.modelID
|
|
34
|
+
const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
|
|
35
|
+
const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
|
|
36
|
+
|
|
37
|
+
console.error(
|
|
38
|
+
`\n>>> Claude Code [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Create or resume session
|
|
42
|
+
const session =
|
|
43
|
+
sessionId ?
|
|
44
|
+
unstable_v2_resumeSession(sessionId, { model: modelStr })
|
|
45
|
+
: unstable_v2_createSession({ model: modelStr })
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Send the prompt
|
|
49
|
+
await session.send(prompt)
|
|
50
|
+
|
|
51
|
+
// Collect the response
|
|
52
|
+
const textParts: string[] = []
|
|
53
|
+
let newSessionId = sessionId || ''
|
|
54
|
+
|
|
55
|
+
// Set up timeout
|
|
56
|
+
const timeoutPromise =
|
|
57
|
+
timeoutSec > 0 ?
|
|
58
|
+
new Promise<never>((_, reject) => {
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
session.close()
|
|
61
|
+
reject(new Error(`Timeout after ${timeoutSec}s`))
|
|
62
|
+
}, timeoutSec * 1000)
|
|
63
|
+
})
|
|
64
|
+
: null
|
|
65
|
+
|
|
66
|
+
// Stream the response
|
|
67
|
+
const streamPromise = (async () => {
|
|
68
|
+
for await (const msg of session.stream()) {
|
|
69
|
+
// Capture session ID from any message
|
|
70
|
+
if (msg.session_id) {
|
|
71
|
+
newSessionId = msg.session_id
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const text = getAssistantText(msg)
|
|
75
|
+
if (text) {
|
|
76
|
+
textParts.push(text)
|
|
77
|
+
process.stderr.write(text)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})()
|
|
81
|
+
|
|
82
|
+
// Race between stream and timeout
|
|
83
|
+
if (timeoutPromise) {
|
|
84
|
+
await Promise.race([streamPromise, timeoutPromise])
|
|
85
|
+
} else {
|
|
86
|
+
await streamPromise
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = textParts.join('')
|
|
90
|
+
const sessionStr = newSessionId || 'none'
|
|
91
|
+
console.error(
|
|
92
|
+
`\n<<< Claude Code done (${response.length} chars) [session:${sessionStr}]`,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
text: response,
|
|
97
|
+
sessionId: newSessionId,
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
session.close()
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/index.ts
CHANGED
package/src/parsing.ts
CHANGED
|
@@ -307,33 +307,69 @@ export function zodTypeToString(zodType: z.ZodType): string {
|
|
|
307
307
|
return 'unknown'
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/**
|
|
311
|
+
* extractBalancedObject extracts a balanced JSON object starting at startIdx.
|
|
312
|
+
* Returns the object substring or null if unbalanced.
|
|
313
|
+
*/
|
|
314
|
+
function extractBalancedObject(text: string, startIdx: number): string | null {
|
|
315
|
+
if (startIdx === -1 || startIdx >= text.length || text[startIdx] !== '{') {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let braceCount = 0
|
|
320
|
+
let endIdx = startIdx
|
|
321
|
+
for (let i = startIdx; i < text.length; i++) {
|
|
322
|
+
if (text[i] === '{') braceCount++
|
|
323
|
+
else if (text[i] === '}') {
|
|
324
|
+
braceCount--
|
|
325
|
+
if (braceCount === 0) {
|
|
326
|
+
endIdx = i + 1
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (endIdx > startIdx && braceCount === 0) {
|
|
333
|
+
return text.slice(startIdx, endIdx)
|
|
334
|
+
}
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
310
338
|
/** extractJsonString finds and extracts JSON from a response string. */
|
|
311
339
|
export function extractJsonString(response: string): string | null {
|
|
312
340
|
// Try to find JSON in code blocks first
|
|
313
341
|
const codeBlockMatch = response.match(/```(?:json)?\s*(\{[\s\S]*?)```/)
|
|
314
342
|
if (codeBlockMatch?.[1]) {
|
|
315
|
-
|
|
343
|
+
const candidate = codeBlockMatch[1].trim()
|
|
344
|
+
// Validate it's actually parseable JSON
|
|
345
|
+
try {
|
|
346
|
+
JSON.parse(candidate)
|
|
347
|
+
return candidate
|
|
348
|
+
} catch {
|
|
349
|
+
// Code block content is malformed, try other methods
|
|
350
|
+
}
|
|
316
351
|
}
|
|
317
352
|
|
|
318
|
-
// Try to find raw JSON by counting braces
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
353
|
+
// Try to find raw JSON by counting braces, starting from each { position
|
|
354
|
+
// This handles cases where the model started outputting JSON, then restarted
|
|
355
|
+
// (e.g., "{"response":{"embeds":[{"title":"Haze{"response":{"embeds":[...]}}}")
|
|
356
|
+
// We try each { position until we find one that produces valid JSON
|
|
357
|
+
let searchFrom = 0
|
|
358
|
+
while (searchFrom < response.length) {
|
|
359
|
+
const startIdx = response.indexOf('{', searchFrom)
|
|
360
|
+
if (startIdx === -1) break
|
|
361
|
+
|
|
362
|
+
const candidate = extractBalancedObject(response, startIdx)
|
|
363
|
+
if (candidate) {
|
|
364
|
+
try {
|
|
365
|
+
JSON.parse(candidate)
|
|
366
|
+
return candidate
|
|
367
|
+
} catch {
|
|
368
|
+
// This { position produced invalid JSON, try the next one
|
|
331
369
|
}
|
|
332
370
|
}
|
|
333
371
|
|
|
334
|
-
|
|
335
|
-
return response.slice(startIdx, endIdx)
|
|
336
|
-
}
|
|
372
|
+
searchFrom = startIdx + 1
|
|
337
373
|
}
|
|
338
374
|
|
|
339
375
|
return null
|
package/src/types.ts
CHANGED
|
@@ -8,8 +8,13 @@ import type { z } from 'zod/v4'
|
|
|
8
8
|
// Model Configuration
|
|
9
9
|
// ============================================================================
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/** Backend type for running agents. */
|
|
12
|
+
export type BackendType = 'opencode' | 'claude-code'
|
|
13
|
+
|
|
14
|
+
/** Model configuration for LLM backends. */
|
|
12
15
|
export interface ModelConfig {
|
|
16
|
+
/** Backend to use (default: 'opencode'). */
|
|
17
|
+
backend?: BackendType
|
|
13
18
|
providerID: string
|
|
14
19
|
modelID: string
|
|
15
20
|
}
|