ocpipe 0.3.2 → 0.3.3
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 +5 -5
- package/src/agent.ts +166 -177
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocpipe",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "SDK for LLM pipelines with OpenCode and Zod",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -29,20 +29,20 @@
|
|
|
29
29
|
"bun": ">=1.0.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"opencode-ai": "
|
|
32
|
+
"opencode-ai": "1.1.3"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"zod": "
|
|
35
|
+
"zod": "4.3.5"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@eslint/js": "^9.39.2",
|
|
39
39
|
"bun-types": "^1.3.5",
|
|
40
40
|
"eslint": "^9.39.2",
|
|
41
|
-
"globals": "^
|
|
41
|
+
"globals": "^17.0.0",
|
|
42
42
|
"jiti": "^2.6.1",
|
|
43
43
|
"prettier": "^3.7.4",
|
|
44
44
|
"typescript": "^5.0.0",
|
|
45
|
-
"typescript-eslint": "^8.
|
|
45
|
+
"typescript-eslint": "^8.52.0",
|
|
46
46
|
"vitest": "^4.0.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
package/src/agent.ts
CHANGED
|
@@ -4,198 +4,187 @@
|
|
|
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
|
-
|
|
128
|
-
resolve({
|
|
129
|
-
text: response,
|
|
130
|
-
sessionId: newSessionId,
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
proc.on('error', (err) => {
|
|
135
|
-
clearTimeout(timeout)
|
|
136
|
-
reject(err)
|
|
137
|
-
})
|
|
138
|
-
})
|
|
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
|
+
});
|
|
139
128
|
}
|
|
140
129
|
|
|
141
130
|
/** exportSession exports a session and extracts assistant text responses. */
|
|
142
131
|
async function exportSession(sessionId: string): Promise<string | null> {
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
}
|
|
193
182
|
}
|
|
194
183
|
|
|
195
184
|
/** logStep logs a step header for workflow progress. */
|
|
196
|
-
export function logStep(step: number, title: string, detail =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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));
|
|
201
190
|
}
|