ocpipe 0.3.8 → 0.4.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/README.md +38 -3
- package/package.json +10 -5
- package/src/agent.ts +36 -41
- package/src/claude-code.ts +102 -0
- package/src/index.ts +1 -0
- 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,7 +47,42 @@ 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
|
+
```
|
|
73
|
+
|
|
74
|
+
### Requirements
|
|
75
|
+
|
|
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:
|
|
77
|
+
|
|
78
|
+
- [#5426](https://github.com/anomalyco/opencode/pull/5426) - Adds `--prompt-file` flag
|
|
79
|
+
- [#5339](https://github.com/anomalyco/opencode/pull/5339) - Session export fixes
|
|
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
|
+
```
|
|
51
86
|
|
|
52
87
|
### Documentation
|
|
53
88
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocpipe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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 {
|
|
9
|
-
import { mkdir } from 'fs/promises'
|
|
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
|
|
30
|
+
return runOpencodeAgent(options)
|
|
37
31
|
}
|
|
38
32
|
|
|
39
|
-
/**
|
|
40
|
-
function
|
|
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] }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** runAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
|
|
50
|
-
export async function runAgent(
|
|
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
|
|
@@ -60,6 +44,13 @@ export async function runAgent(
|
|
|
60
44
|
`\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
|
|
61
45
|
)
|
|
62
46
|
|
|
47
|
+
// Write prompt to .opencode/prompts/ within the working directory
|
|
48
|
+
const cwd = workdir ?? PROJECT_ROOT
|
|
49
|
+
const promptsDir = join(cwd, '.opencode', 'prompts')
|
|
50
|
+
await mkdir(promptsDir, { recursive: true })
|
|
51
|
+
const promptFile = join(promptsDir, `prompt_${Date.now()}.txt`)
|
|
52
|
+
await writeFile(promptFile, prompt)
|
|
53
|
+
|
|
63
54
|
const args = [
|
|
64
55
|
'run',
|
|
65
56
|
'--format',
|
|
@@ -68,19 +59,18 @@ export async function runAgent(
|
|
|
68
59
|
agent,
|
|
69
60
|
'--model',
|
|
70
61
|
modelStr,
|
|
62
|
+
'--prompt-file',
|
|
63
|
+
promptFile,
|
|
71
64
|
]
|
|
72
65
|
|
|
73
66
|
if (sessionId) {
|
|
74
67
|
args.push('--session', sessionId)
|
|
75
68
|
}
|
|
76
69
|
|
|
77
|
-
// Pass prompt as positional argument (stdin doesn't work without TTY)
|
|
78
|
-
args.push(prompt)
|
|
79
|
-
|
|
80
70
|
return new Promise((resolve, reject) => {
|
|
81
71
|
const opencodeCmd = getOpencodeCommand(args)
|
|
82
72
|
const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
|
|
83
|
-
cwd
|
|
73
|
+
cwd,
|
|
84
74
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
85
75
|
})
|
|
86
76
|
|
|
@@ -118,8 +108,9 @@ export async function runAgent(
|
|
|
118
108
|
// Timeout handling (0 = no timeout)
|
|
119
109
|
const timeout =
|
|
120
110
|
timeoutSec > 0 ?
|
|
121
|
-
setTimeout(() => {
|
|
111
|
+
setTimeout(async () => {
|
|
122
112
|
proc.kill()
|
|
113
|
+
await unlink(promptFile).catch(() => {})
|
|
123
114
|
reject(new Error(`Timeout after ${timeoutSec}s`))
|
|
124
115
|
}, timeoutSec * 1000)
|
|
125
116
|
: null
|
|
@@ -127,6 +118,9 @@ export async function runAgent(
|
|
|
127
118
|
proc.on('close', async (code) => {
|
|
128
119
|
if (timeout) clearTimeout(timeout)
|
|
129
120
|
|
|
121
|
+
// Clean up prompt file
|
|
122
|
+
await unlink(promptFile).catch(() => {})
|
|
123
|
+
|
|
130
124
|
if (code !== 0) {
|
|
131
125
|
const stderr = stderrChunks.join('').trim()
|
|
132
126
|
const lastLines = stderr.split('\n').slice(-5).join('\n')
|
|
@@ -156,8 +150,9 @@ export async function runAgent(
|
|
|
156
150
|
})
|
|
157
151
|
})
|
|
158
152
|
|
|
159
|
-
proc.on('error', (err) => {
|
|
153
|
+
proc.on('error', async (err) => {
|
|
160
154
|
if (timeout) clearTimeout(timeout)
|
|
155
|
+
await unlink(promptFile).catch(() => {})
|
|
161
156
|
reject(err)
|
|
162
157
|
})
|
|
163
158
|
})
|
|
@@ -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/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
|
}
|