ocpipe 0.3.3 → 0.3.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpipe",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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.3"
32
+ "opencode-ai": "1.1.16"
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": "tsc --noEmit",
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",
@@ -56,7 +57,7 @@
56
57
  "release:version": "npm version patch -m \"release: v%s\" --no-git-tag-version",
57
58
  "release:version:minor": "npm version minor -m \"release: v%s\" --no-git-tag-version",
58
59
  "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\")",
59
- "release:publish": "git push && git push --tags && npm publish"
60
+ "release:publish": "git push && git push --tags"
60
61
  },
61
62
  "files": [
62
63
  "src/",
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 "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";
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
- try {
15
- execSync("which opencode", { stdio: "ignore" });
16
- return true;
17
- } catch {
18
- return false;
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
- 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] };
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(options: RunAgentOptions): Promise<RunAgentResult> {
33
- const { prompt, agent, model, sessionId, timeoutSec = 300 } = options;
34
-
35
- const modelStr = `${model.providerID}/${model.modelID}`;
36
- const sessionInfo = sessionId ? `[session:${sessionId}]` : "[new session]";
37
- const promptPreview = prompt.slice(0, 50).replace(/\n/g, " ");
38
-
39
- console.error(`\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`);
40
-
41
- const args = ["run", "--format", "default", "--agent", agent, "--model", modelStr];
42
-
43
- if (sessionId) {
44
- args.push("--session", sessionId);
45
- }
46
-
47
- return new Promise((resolve, reject) => {
48
- const opencodeCmd = getOpencodeCommand(args);
49
- const proc = spawn(opencodeCmd.cmd, opencodeCmd.args, {
50
- cwd: PROJECT_ROOT,
51
- stdio: ["pipe", "pipe", "pipe"],
52
- });
53
-
54
- let newSessionId = sessionId || "";
55
- const stdoutChunks: string[] = [];
56
-
57
- // Stream stderr in real-time (OpenCode progress output)
58
- proc.stderr.on("data", (data: Buffer) => {
59
- const text = data.toString();
60
-
61
- // Parse session ID from output
62
- for (const line of text.split("\n")) {
63
- if (line.startsWith("[session:")) {
64
- newSessionId = line.trim().slice(9, -1);
65
- continue;
66
- }
67
- // Filter noise
68
- if (line.includes("baseline-browser-mapping")) continue;
69
- if (line.startsWith("$ bun run")) continue;
70
- if (line.trim()) {
71
- process.stderr.write(line + "\n");
72
- }
73
- }
74
- });
75
-
76
- // Collect stdout
77
- proc.stdout.on("data", (data: Buffer) => {
78
- const text = data.toString();
79
- stdoutChunks.push(text);
80
- process.stderr.write(text);
81
- });
82
-
83
- // Send prompt to stdin
84
- proc.stdin.write(prompt);
85
- proc.stdin.end();
86
-
87
- // Timeout handling (0 = no timeout)
88
- const timeout =
89
- timeoutSec > 0
90
- ? setTimeout(() => {
91
- proc.kill();
92
- reject(new Error(`Timeout after ${timeoutSec}s`));
93
- }, timeoutSec * 1000)
94
- : null;
95
-
96
- proc.on("close", async (code) => {
97
- if (timeout) clearTimeout(timeout);
98
-
99
- if (code !== 0) {
100
- reject(new Error(`OpenCode exited with code ${code}`));
101
- return;
102
- }
103
-
104
- // Export session to get structured response
105
- let response = stdoutChunks.join("").trim();
106
-
107
- if (newSessionId) {
108
- const exported = await exportSession(newSessionId);
109
- if (exported) {
110
- response = exported;
111
- }
112
- }
113
-
114
- const sessionStr = newSessionId || "none";
115
- console.error(`<<< OpenCode done (${response.length} chars) [session:${sessionStr}]`);
116
-
117
- resolve({
118
- text: response,
119
- sessionId: newSessionId,
120
- });
121
- });
122
-
123
- proc.on("error", (err) => {
124
- if (timeout) clearTimeout(timeout);
125
- reject(err);
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(sessionId: string): Promise<string | null> {
132
- const tmpPath = `${TMP_DIR}/opencode_export_${Date.now()}.json`;
133
-
134
- try {
135
- await mkdir(TMP_DIR, { recursive: true });
136
- const opencodeCmd = getOpencodeCommand([
137
- "session",
138
- "export",
139
- sessionId,
140
- "--format",
141
- "json",
142
- "-o",
143
- tmpPath,
144
- ]);
145
- const proc = Bun.spawn([opencodeCmd.cmd, ...opencodeCmd.args], {
146
- cwd: PROJECT_ROOT,
147
- stdout: "pipe",
148
- stderr: "pipe",
149
- });
150
-
151
- await proc.exited;
152
-
153
- const file = Bun.file(tmpPath);
154
- if (!(await file.exists())) return null;
155
-
156
- const data = (await file.json()) as {
157
- messages?: Array<{
158
- info?: { role?: string };
159
- parts?: Array<{ type?: string; text?: string }>;
160
- }>;
161
- };
162
- await Bun.write(tmpPath, ""); // Clean up
163
-
164
- // Extract all assistant text parts
165
- const messages = data.messages || [];
166
- const textParts: string[] = [];
167
-
168
- for (const msg of messages) {
169
- if (msg.info?.role === "assistant") {
170
- for (const part of msg.parts || []) {
171
- if (part.type === "text" && part.text) {
172
- textParts.push(part.text);
173
- }
174
- }
175
- }
176
- }
177
-
178
- return textParts.length > 0 ? textParts.join("\n") : null;
179
- } catch {
180
- return null;
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 = ""): void {
186
- const detailStr = detail ? ` (${detail})` : "";
187
- console.log(`\n${"=".repeat(60)}`);
188
- console.log(`STEP ${step}: ${title}${detailStr}`);
189
- console.log("=".repeat(60));
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
@@ -32,6 +32,7 @@ export class Pipeline<S extends BaseState> {
32
32
  defaultModel: config.defaultModel,
33
33
  defaultAgent: config.defaultAgent,
34
34
  timeoutSec: config.timeoutSec ?? 300,
35
+ workdir: config.workdir,
35
36
  }
36
37
  }
37
38
 
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. */
@@ -1,55 +0,0 @@
1
- {
2
- "sessionId": "20251231_092022",
3
- "startedAt": "2025-12-31T09:20:22.199Z",
4
- "phase": "init",
5
- "steps": [
6
- {
7
- "stepName": "MoodAnalyzer",
8
- "timestamp": "2025-12-31T09:20:25.808Z",
9
- "result": {
10
- "data": {
11
- "mood": "happy",
12
- "keywords": [
13
- "happy",
14
- "so",
15
- "today"
16
- ]
17
- },
18
- "stepName": "MoodAnalyzer",
19
- "duration": 3608,
20
- "sessionId": "ses_48c4aa762ffeP1pxFsOURK4cw2",
21
- "model": {
22
- "providerID": "github-copilot",
23
- "modelID": "grok-code-fast-1"
24
- },
25
- "attempt": 1
26
- }
27
- },
28
- {
29
- "stepName": "ResponseSuggester",
30
- "timestamp": "2025-12-31T09:20:30.186Z",
31
- "result": {
32
- "data": {
33
- "suggestion": "That's wonderful! What's making you so happy today?"
34
- },
35
- "stepName": "ResponseSuggester",
36
- "duration": 4377,
37
- "sessionId": "ses_48c4aa762ffeP1pxFsOURK4cw2",
38
- "model": {
39
- "providerID": "github-copilot",
40
- "modelID": "grok-code-fast-1"
41
- },
42
- "attempt": 1
43
- }
44
- }
45
- ],
46
- "subPipelines": [],
47
- "inputText": "I am so happy today!",
48
- "opencodeSessionId": "ses_48c4aa762ffeP1pxFsOURK4cw2",
49
- "mood": "happy",
50
- "keywords": [
51
- "happy",
52
- "so",
53
- "today"
54
- ]
55
- }