@testchimp/cli 0.1.36 → 0.1.38
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/dist/chimphands/run.js +150 -41
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -47,9 +47,14 @@ function postJsonFireAndForget(backend, apiKey, path, body) {
|
|
|
47
47
|
console.error(`ChimpHands API telemetry failed ${path}: ${detail}`);
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
const TESTCHIMP_PROVIDER_ID = "testchimp";
|
|
51
|
+
function resolveOpencodeModelId(boot) {
|
|
51
52
|
const raw = (boot.llm_model || "gpt-4o-mini").trim();
|
|
52
|
-
|
|
53
|
+
const modelId = raw.includes("/") ? raw.split("/").pop() || "gpt-4o-mini" : raw;
|
|
54
|
+
return modelId;
|
|
55
|
+
}
|
|
56
|
+
function resolveOpencodeModel(boot) {
|
|
57
|
+
return `${TESTCHIMP_PROVIDER_ID}/${resolveOpencodeModelId(boot)}`;
|
|
53
58
|
}
|
|
54
59
|
function extractOpencodeFatalError(raw) {
|
|
55
60
|
const line = raw.trim();
|
|
@@ -58,7 +63,11 @@ function extractOpencodeFatalError(raw) {
|
|
|
58
63
|
try {
|
|
59
64
|
const ev = JSON.parse(line);
|
|
60
65
|
if (ev.type === "error" || ev.name === "UnknownError") {
|
|
61
|
-
const msg = ev.data?.message ||
|
|
66
|
+
const msg = ev.error?.data?.message ||
|
|
67
|
+
ev.error?.message ||
|
|
68
|
+
ev.data?.message ||
|
|
69
|
+
ev.message ||
|
|
70
|
+
line;
|
|
62
71
|
const ref = ev.data?.ref ? ` (ref ${ev.data.ref})` : "";
|
|
63
72
|
return `${msg}${ref}`;
|
|
64
73
|
}
|
|
@@ -69,12 +78,61 @@ function extractOpencodeFatalError(raw) {
|
|
|
69
78
|
if (line.includes("Unexpected server error") && line.includes("UnknownError")) {
|
|
70
79
|
return line;
|
|
71
80
|
}
|
|
81
|
+
const errorLine = line.match(/^Error:\s*(.+)$/i);
|
|
82
|
+
if (errorLine?.[1]?.trim()) {
|
|
83
|
+
return errorLine[1].trim();
|
|
84
|
+
}
|
|
85
|
+
if (/not found/i.test(line) && line.length < 240) {
|
|
86
|
+
return line.trim();
|
|
87
|
+
}
|
|
72
88
|
return null;
|
|
73
89
|
}
|
|
90
|
+
function parseOpencodeEvent(line) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(line);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function formatToolUseContent(part) {
|
|
99
|
+
const title = part.state?.title || part.tool || "tool";
|
|
100
|
+
const output = part.state?.output?.trim();
|
|
101
|
+
if (output)
|
|
102
|
+
return `[${title}]\n${output}`;
|
|
103
|
+
return `[${title}]`;
|
|
104
|
+
}
|
|
105
|
+
function opencodeMessageId(prefix, part) {
|
|
106
|
+
const raw = part?.id || part?.messageID;
|
|
107
|
+
if (!raw)
|
|
108
|
+
return undefined;
|
|
109
|
+
return `${prefix}${raw}`;
|
|
110
|
+
}
|
|
111
|
+
function summarizeOpencodeFailure(stderr, stdout, exitCode) {
|
|
112
|
+
for (const chunk of [stderr, stdout]) {
|
|
113
|
+
for (const line of chunk.split("\n")) {
|
|
114
|
+
const fatal = extractOpencodeFatalError(line);
|
|
115
|
+
if (fatal)
|
|
116
|
+
return fatal;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const merged = `${stderr}\n${stdout}`.trim();
|
|
120
|
+
if (merged) {
|
|
121
|
+
const errorLines = merged
|
|
122
|
+
.split("\n")
|
|
123
|
+
.map((l) => l.trim())
|
|
124
|
+
.filter((l) => /^error:/i.test(l) || /not found/i.test(l));
|
|
125
|
+
if (errorLines.length)
|
|
126
|
+
return errorLines[errorLines.length - 1].replace(/^error:\s*/i, "").trim();
|
|
127
|
+
return merged.slice(0, 1200);
|
|
128
|
+
}
|
|
129
|
+
return exitCode ? `opencode exited with code ${exitCode}` : "opencode failed";
|
|
130
|
+
}
|
|
74
131
|
function writeOpencodeConfig(backend, apiKey, boot) {
|
|
75
132
|
const llmBase = (boot.llm_base_url || `${backend}/v1`).replace(/\/$/, "");
|
|
76
133
|
const llmKey = apiKey || boot.llm_api_key || "";
|
|
77
|
-
const
|
|
134
|
+
const modelId = resolveOpencodeModelId(boot);
|
|
135
|
+
const model = `${TESTCHIMP_PROVIDER_ID}/${modelId}`;
|
|
78
136
|
const mcpEnv = {
|
|
79
137
|
TESTCHIMP_API_KEY: apiKey,
|
|
80
138
|
TESTCHIMP_BACKEND_URL: backend,
|
|
@@ -87,13 +145,19 @@ function writeOpencodeConfig(backend, apiKey, boot) {
|
|
|
87
145
|
$schema: "https://opencode.ai/config.json",
|
|
88
146
|
model,
|
|
89
147
|
autoupdate: false,
|
|
90
|
-
enabled_providers: ["openai"],
|
|
91
148
|
provider: {
|
|
92
|
-
|
|
149
|
+
[TESTCHIMP_PROVIDER_ID]: {
|
|
150
|
+
npm: "@ai-sdk/openai-compatible",
|
|
151
|
+
name: "TestChimp",
|
|
93
152
|
options: {
|
|
94
153
|
apiKey: llmKey,
|
|
95
154
|
baseURL: llmBase,
|
|
96
155
|
},
|
|
156
|
+
models: {
|
|
157
|
+
[modelId]: {
|
|
158
|
+
name: modelId,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
97
161
|
},
|
|
98
162
|
},
|
|
99
163
|
mcp: {
|
|
@@ -130,41 +194,65 @@ function runOpencode(prompt, model, childEnv, postEvent) {
|
|
|
130
194
|
return new Promise((resolve) => {
|
|
131
195
|
let buf = "";
|
|
132
196
|
let fatalError = null;
|
|
197
|
+
const textByPartId = new Map();
|
|
198
|
+
const handleOpencodeLine = (line) => {
|
|
199
|
+
if (!line.trim())
|
|
200
|
+
return;
|
|
201
|
+
const fatal = extractOpencodeFatalError(line);
|
|
202
|
+
if (fatal) {
|
|
203
|
+
fatalError = fatal;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const ev = parseOpencodeEvent(line);
|
|
207
|
+
if (!ev?.type)
|
|
208
|
+
return;
|
|
209
|
+
switch (ev.type) {
|
|
210
|
+
case "text": {
|
|
211
|
+
const chunk = ev.part?.text;
|
|
212
|
+
if (!chunk)
|
|
213
|
+
return;
|
|
214
|
+
const partId = ev.part?.id || ev.part?.messageID;
|
|
215
|
+
if (!partId) {
|
|
216
|
+
postEvent(ROLE_ASSISTANT, chunk);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const next = (textByPartId.get(partId) || "") + chunk;
|
|
220
|
+
textByPartId.set(partId, next);
|
|
221
|
+
postEvent(ROLE_ASSISTANT, next, undefined, opencodeMessageId("oc_text_", ev.part));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case "tool_use": {
|
|
225
|
+
if (ev.part?.state?.status !== "completed")
|
|
226
|
+
return;
|
|
227
|
+
postEvent(ROLE_TOOL, formatToolUseContent(ev.part), undefined, opencodeMessageId("oc_tool_", ev.part));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
case "error": {
|
|
231
|
+
const msg = ev.error?.data?.message ||
|
|
232
|
+
ev.error?.message ||
|
|
233
|
+
line.trim();
|
|
234
|
+
if (msg)
|
|
235
|
+
fatalError = msg;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
case "step_start":
|
|
239
|
+
case "step_finish":
|
|
240
|
+
return;
|
|
241
|
+
default:
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
133
245
|
child.stdout.on("data", (chunk) => {
|
|
134
246
|
buf += chunk.toString();
|
|
135
247
|
const lines = buf.split("\n");
|
|
136
248
|
buf = lines.pop() || "";
|
|
137
249
|
for (const line of lines) {
|
|
138
|
-
|
|
139
|
-
continue;
|
|
140
|
-
const fatal = extractOpencodeFatalError(line);
|
|
141
|
-
if (fatal) {
|
|
142
|
-
fatalError = fatal;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
let content = line;
|
|
146
|
-
let role = ROLE_ASSISTANT;
|
|
147
|
-
try {
|
|
148
|
-
const ev = JSON.parse(line);
|
|
149
|
-
content = ev.content || ev.message || ev.text || JSON.stringify(ev);
|
|
150
|
-
if (ev.type === "tool" || ev.role === "tool")
|
|
151
|
-
role = ROLE_TOOL;
|
|
152
|
-
if (ev.type === "status")
|
|
153
|
-
role = ROLE_STATUS;
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
/* plain line */
|
|
157
|
-
}
|
|
158
|
-
postEvent(role, content);
|
|
250
|
+
handleOpencodeLine(line);
|
|
159
251
|
}
|
|
160
252
|
});
|
|
161
253
|
child.on("close", (code) => {
|
|
162
254
|
if (buf.trim()) {
|
|
163
|
-
|
|
164
|
-
if (fatal)
|
|
165
|
-
fatalError = fatal;
|
|
166
|
-
else if (!fatalError)
|
|
167
|
-
postEvent(ROLE_ASSISTANT, buf.trim());
|
|
255
|
+
handleOpencodeLine(buf.trim());
|
|
168
256
|
}
|
|
169
257
|
const stderrFatal = extractOpencodeFatalError(err);
|
|
170
258
|
if (stderrFatal)
|
|
@@ -173,6 +261,10 @@ function runOpencode(prompt, model, childEnv, postEvent) {
|
|
|
173
261
|
resolve({ code: 1, err: fatalError });
|
|
174
262
|
return;
|
|
175
263
|
}
|
|
264
|
+
if (code != null && code !== 0) {
|
|
265
|
+
resolve({ code, err: summarizeOpencodeFailure(err, buf, code) });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
176
268
|
resolve({ code: code == null ? 1 : code, err });
|
|
177
269
|
});
|
|
178
270
|
});
|
|
@@ -195,11 +287,7 @@ function runOpencode(prompt, model, childEnv, postEvent) {
|
|
|
195
287
|
const errObj = e;
|
|
196
288
|
const stderr = errObj.stderr?.toString() || "";
|
|
197
289
|
const stdout = errObj.stdout?.toString() || "";
|
|
198
|
-
const fatal =
|
|
199
|
-
extractOpencodeFatalError(stdout) ||
|
|
200
|
-
stderr ||
|
|
201
|
-
errObj.message ||
|
|
202
|
-
"opencode failed";
|
|
290
|
+
const fatal = summarizeOpencodeFailure(stderr, stdout, errObj.status ?? 1);
|
|
203
291
|
return Promise.resolve({ code: errObj.status || 1, err: fatal });
|
|
204
292
|
}
|
|
205
293
|
}
|
|
@@ -288,12 +376,14 @@ export async function runChimphands(opts) {
|
|
|
288
376
|
let idle = false;
|
|
289
377
|
let closed = false;
|
|
290
378
|
let lastUserActivity = Date.now();
|
|
291
|
-
const postEvent = (role, content, status) => {
|
|
379
|
+
const postEvent = (role, content, status, messageId) => {
|
|
292
380
|
const body = {
|
|
293
381
|
sessionId,
|
|
294
382
|
role,
|
|
295
383
|
content: String(content || "").slice(0, 20000),
|
|
296
384
|
};
|
|
385
|
+
if (messageId)
|
|
386
|
+
body.messageId = messageId;
|
|
297
387
|
if (status != null)
|
|
298
388
|
body.status = status;
|
|
299
389
|
postJsonFireAndForget(backend, apiKey, "/api/chimphands/post_agent_event", body);
|
|
@@ -347,10 +437,29 @@ export async function runChimphands(opts) {
|
|
|
347
437
|
while (prompt) {
|
|
348
438
|
const result = await runOpencode(ensureTestchimpPrompt(prompt), opencodeModel, childEnv, postEvent);
|
|
349
439
|
if (result.code !== 0) {
|
|
350
|
-
const errMsg = result.err || "opencode failed";
|
|
440
|
+
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
351
441
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
352
|
-
|
|
353
|
-
|
|
442
|
+
try {
|
|
443
|
+
await postJson(backend, apiKey, "/api/chimphands/post_agent_event", {
|
|
444
|
+
sessionId,
|
|
445
|
+
role: ROLE_STATUS,
|
|
446
|
+
content: errMsg,
|
|
447
|
+
status: STATUS_FAILED,
|
|
448
|
+
githubRunId: githubRunId || undefined,
|
|
449
|
+
});
|
|
450
|
+
await postJson(backend, apiKey, "/api/chimphands/complete_session", {
|
|
451
|
+
sessionId,
|
|
452
|
+
status: STATUS_FAILED,
|
|
453
|
+
errorMessage: errMsg,
|
|
454
|
+
githubRunId: githubRunId || undefined,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
catch (reportErr) {
|
|
458
|
+
const detail = reportErr instanceof Error ? reportErr.message : String(reportErr);
|
|
459
|
+
console.error(`ChimpHands failed to report OpenCode error to backend: ${detail}`);
|
|
460
|
+
postEvent(ROLE_STATUS, errMsg, STATUS_FAILED);
|
|
461
|
+
complete(STATUS_FAILED, errMsg);
|
|
462
|
+
}
|
|
354
463
|
process.exit(result.code || 1);
|
|
355
464
|
}
|
|
356
465
|
postEvent(ROLE_STATUS, "Waiting for user input", STATUS_WAITING_USER);
|
package/package.json
CHANGED