@testchimp/cli 0.1.35 → 0.1.37
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.d.ts +1 -1
- package/dist/chimphands/run.js +184 -28
- package/dist/cli/program.js +1 -1
- package/package.json +1 -1
package/dist/chimphands/run.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChimpHands GitHub Actions bridge: bootstrap → OpenCode → inbound SSE turns.
|
|
3
3
|
* Relies on TESTCHIMP_API_KEY (+ optional TESTCHIMP_BACKEND_URL; defaults to prod).
|
|
4
|
-
* Does not write mcp.json —
|
|
4
|
+
* Does not write mcp.json — TestChimp MCP is wired via opencode.json for OpenCode.
|
|
5
5
|
*/
|
|
6
6
|
type RunOptions = {
|
|
7
7
|
sessionId: string;
|
package/dist/chimphands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChimpHands GitHub Actions bridge: bootstrap → OpenCode → inbound SSE turns.
|
|
3
3
|
* Relies on TESTCHIMP_API_KEY (+ optional TESTCHIMP_BACKEND_URL; defaults to prod).
|
|
4
|
-
* Does not write mcp.json —
|
|
4
|
+
* Does not write mcp.json — TestChimp MCP is wired via opencode.json for OpenCode.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn, execFileSync } from "node:child_process";
|
|
7
7
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -16,6 +16,13 @@ const STATUS_RUNNING = "CHIMPHANDS_SESSION_STATUS_RUNNING";
|
|
|
16
16
|
const STATUS_WAITING_USER = "CHIMPHANDS_SESSION_STATUS_WAITING_USER";
|
|
17
17
|
const STATUS_IDLE = "CHIMPHANDS_SESSION_STATUS_IDLE";
|
|
18
18
|
const STATUS_FAILED = "CHIMPHANDS_SESSION_STATUS_FAILED";
|
|
19
|
+
function ensureTestchimpPrompt(content) {
|
|
20
|
+
const trimmed = content.trim();
|
|
21
|
+
if (!trimmed)
|
|
22
|
+
return trimmed;
|
|
23
|
+
const rest = trimmed.replace(/^\/testchimp\s*/i, "").trim();
|
|
24
|
+
return rest ? `/testchimp ${rest}` : "/testchimp";
|
|
25
|
+
}
|
|
19
26
|
function apiHeaders(apiKey) {
|
|
20
27
|
return {
|
|
21
28
|
"Content-Type": "application/json",
|
|
@@ -35,25 +42,111 @@ async function postJson(backend, apiKey, path, body) {
|
|
|
35
42
|
return text;
|
|
36
43
|
}
|
|
37
44
|
function postJsonFireAndForget(backend, apiKey, path, body) {
|
|
38
|
-
void postJson(backend, apiKey, path, body).catch(() => {
|
|
39
|
-
|
|
45
|
+
void postJson(backend, apiKey, path, body).catch((err) => {
|
|
46
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
47
|
+
console.error(`ChimpHands API telemetry failed ${path}: ${detail}`);
|
|
40
48
|
});
|
|
41
49
|
}
|
|
50
|
+
const TESTCHIMP_PROVIDER_ID = "testchimp";
|
|
51
|
+
function resolveOpencodeModelId(boot) {
|
|
52
|
+
const raw = (boot.llm_model || "gpt-4o-mini").trim();
|
|
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)}`;
|
|
58
|
+
}
|
|
59
|
+
function extractOpencodeFatalError(raw) {
|
|
60
|
+
const line = raw.trim();
|
|
61
|
+
if (!line)
|
|
62
|
+
return null;
|
|
63
|
+
try {
|
|
64
|
+
const ev = JSON.parse(line);
|
|
65
|
+
if (ev.type === "error" || ev.name === "UnknownError") {
|
|
66
|
+
const msg = ev.data?.message || ev.message || line;
|
|
67
|
+
const ref = ev.data?.ref ? ` (ref ${ev.data.ref})` : "";
|
|
68
|
+
return `${msg}${ref}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* plain text */
|
|
73
|
+
}
|
|
74
|
+
if (line.includes("Unexpected server error") && line.includes("UnknownError")) {
|
|
75
|
+
return line;
|
|
76
|
+
}
|
|
77
|
+
const errorLine = line.match(/^Error:\s*(.+)$/i);
|
|
78
|
+
if (errorLine?.[1]?.trim()) {
|
|
79
|
+
return errorLine[1].trim();
|
|
80
|
+
}
|
|
81
|
+
if (/not found/i.test(line) && line.length < 240) {
|
|
82
|
+
return line.trim();
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function summarizeOpencodeFailure(stderr, stdout, exitCode) {
|
|
87
|
+
for (const chunk of [stderr, stdout]) {
|
|
88
|
+
for (const line of chunk.split("\n")) {
|
|
89
|
+
const fatal = extractOpencodeFatalError(line);
|
|
90
|
+
if (fatal)
|
|
91
|
+
return fatal;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const merged = `${stderr}\n${stdout}`.trim();
|
|
95
|
+
if (merged) {
|
|
96
|
+
const errorLines = merged
|
|
97
|
+
.split("\n")
|
|
98
|
+
.map((l) => l.trim())
|
|
99
|
+
.filter((l) => /^error:/i.test(l) || /not found/i.test(l));
|
|
100
|
+
if (errorLines.length)
|
|
101
|
+
return errorLines[errorLines.length - 1].replace(/^error:\s*/i, "").trim();
|
|
102
|
+
return merged.slice(0, 1200);
|
|
103
|
+
}
|
|
104
|
+
return exitCode ? `opencode exited with code ${exitCode}` : "opencode failed";
|
|
105
|
+
}
|
|
42
106
|
function writeOpencodeConfig(backend, apiKey, boot) {
|
|
43
107
|
const llmBase = (boot.llm_base_url || `${backend}/v1`).replace(/\/$/, "");
|
|
44
108
|
const llmKey = apiKey || boot.llm_api_key || "";
|
|
45
|
-
const
|
|
109
|
+
const modelId = resolveOpencodeModelId(boot);
|
|
110
|
+
const model = `${TESTCHIMP_PROVIDER_ID}/${modelId}`;
|
|
111
|
+
const mcpEnv = {
|
|
112
|
+
TESTCHIMP_API_KEY: apiKey,
|
|
113
|
+
TESTCHIMP_BACKEND_URL: backend,
|
|
114
|
+
};
|
|
115
|
+
const serviceUserId = boot.chimphands_service_account_user_id?.trim();
|
|
116
|
+
if (serviceUserId) {
|
|
117
|
+
mcpEnv.TESTCHIMP_USER_ID = serviceUserId;
|
|
118
|
+
}
|
|
46
119
|
writeFileSync("opencode.json", JSON.stringify({
|
|
47
|
-
|
|
120
|
+
$schema: "https://opencode.ai/config.json",
|
|
121
|
+
model,
|
|
122
|
+
autoupdate: false,
|
|
48
123
|
provider: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
124
|
+
[TESTCHIMP_PROVIDER_ID]: {
|
|
125
|
+
npm: "@ai-sdk/openai-compatible",
|
|
126
|
+
name: "TestChimp",
|
|
127
|
+
options: {
|
|
128
|
+
apiKey: llmKey,
|
|
129
|
+
baseURL: llmBase,
|
|
130
|
+
},
|
|
131
|
+
models: {
|
|
132
|
+
[modelId]: {
|
|
133
|
+
name: modelId,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
mcp: {
|
|
139
|
+
testchimp: {
|
|
140
|
+
type: "local",
|
|
141
|
+
enabled: true,
|
|
142
|
+
command: ["npx", "-y", "@testchimp/cli@latest", "mcp"],
|
|
143
|
+
environment: mcpEnv,
|
|
52
144
|
},
|
|
53
145
|
},
|
|
54
146
|
}, null, 2));
|
|
147
|
+
return model;
|
|
55
148
|
}
|
|
56
|
-
function runOpencode(prompt, childEnv, postEvent) {
|
|
149
|
+
function runOpencode(prompt, model, childEnv, postEvent) {
|
|
57
150
|
const help = (() => {
|
|
58
151
|
try {
|
|
59
152
|
return execFileSync("opencode", ["run", "--help"], { encoding: "utf8", env: childEnv });
|
|
@@ -63,8 +156,9 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
63
156
|
}
|
|
64
157
|
})();
|
|
65
158
|
const useJson = help.includes("--format");
|
|
159
|
+
const baseArgs = ["run", prompt, "--model", model];
|
|
66
160
|
if (useJson) {
|
|
67
|
-
const child = spawn("opencode", [
|
|
161
|
+
const child = spawn("opencode", [...baseArgs, "--format", "json"], {
|
|
68
162
|
stdio: ["ignore", "pipe", "pipe"],
|
|
69
163
|
env: childEnv,
|
|
70
164
|
});
|
|
@@ -74,6 +168,7 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
74
168
|
});
|
|
75
169
|
return new Promise((resolve) => {
|
|
76
170
|
let buf = "";
|
|
171
|
+
let fatalError = null;
|
|
77
172
|
child.stdout.on("data", (chunk) => {
|
|
78
173
|
buf += chunk.toString();
|
|
79
174
|
const lines = buf.split("\n");
|
|
@@ -81,6 +176,11 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
81
176
|
for (const line of lines) {
|
|
82
177
|
if (!line.trim())
|
|
83
178
|
continue;
|
|
179
|
+
const fatal = extractOpencodeFatalError(line);
|
|
180
|
+
if (fatal) {
|
|
181
|
+
fatalError = fatal;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
84
184
|
let content = line;
|
|
85
185
|
let role = ROLE_ASSISTANT;
|
|
86
186
|
try {
|
|
@@ -98,27 +198,48 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
98
198
|
}
|
|
99
199
|
});
|
|
100
200
|
child.on("close", (code) => {
|
|
101
|
-
if (buf.trim())
|
|
102
|
-
|
|
201
|
+
if (buf.trim()) {
|
|
202
|
+
const fatal = extractOpencodeFatalError(buf);
|
|
203
|
+
if (fatal)
|
|
204
|
+
fatalError = fatal;
|
|
205
|
+
else if (!fatalError)
|
|
206
|
+
postEvent(ROLE_ASSISTANT, buf.trim());
|
|
207
|
+
}
|
|
208
|
+
const stderrFatal = extractOpencodeFatalError(err);
|
|
209
|
+
if (stderrFatal)
|
|
210
|
+
fatalError = stderrFatal;
|
|
211
|
+
if (fatalError) {
|
|
212
|
+
resolve({ code: 1, err: fatalError });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (code != null && code !== 0) {
|
|
216
|
+
resolve({ code, err: summarizeOpencodeFailure(err, buf, code) });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
103
219
|
resolve({ code: code == null ? 1 : code, err });
|
|
104
220
|
});
|
|
105
221
|
});
|
|
106
222
|
}
|
|
107
223
|
try {
|
|
108
|
-
const out = execFileSync("opencode",
|
|
224
|
+
const out = execFileSync("opencode", baseArgs, {
|
|
109
225
|
encoding: "utf8",
|
|
110
226
|
maxBuffer: 20 * 1024 * 1024,
|
|
111
227
|
stdio: ["ignore", "pipe", "pipe"],
|
|
112
228
|
env: childEnv,
|
|
113
229
|
});
|
|
230
|
+
const fatal = extractOpencodeFatalError(out);
|
|
231
|
+
if (fatal)
|
|
232
|
+
return Promise.resolve({ code: 1, err: fatal });
|
|
114
233
|
if (out)
|
|
115
234
|
postEvent(ROLE_ASSISTANT, out);
|
|
116
235
|
return Promise.resolve({ code: 0, err: "" });
|
|
117
236
|
}
|
|
118
237
|
catch (e) {
|
|
119
238
|
const errObj = e;
|
|
120
|
-
const
|
|
121
|
-
|
|
239
|
+
const stderr = errObj.stderr?.toString() || "";
|
|
240
|
+
const stdout = errObj.stdout?.toString() || "";
|
|
241
|
+
const fatal = summarizeOpencodeFailure(stderr, stdout, errObj.status ?? 1);
|
|
242
|
+
return Promise.resolve({ code: errObj.status || 1, err: fatal });
|
|
122
243
|
}
|
|
123
244
|
}
|
|
124
245
|
function connectInbound(backend, apiKey, sessionId, onUserMessage, onIdle, onClosed) {
|
|
@@ -184,15 +305,23 @@ export async function runChimphands(opts) {
|
|
|
184
305
|
}
|
|
185
306
|
const promptInput = (opts.prompt ?? process.env.PROMPT ?? "").trim();
|
|
186
307
|
const bootText = await postJson(backend, apiKey, "/api/chimphands/bootstrap", {
|
|
187
|
-
|
|
308
|
+
sessionId,
|
|
188
309
|
});
|
|
189
310
|
const boot = JSON.parse(bootText);
|
|
311
|
+
const githubRunId = (process.env.GITHUB_RUN_ID || "").trim();
|
|
312
|
+
if (githubRunId) {
|
|
313
|
+
postJsonFireAndForget(backend, apiKey, "/api/chimphands/post_agent_event", {
|
|
314
|
+
sessionId,
|
|
315
|
+
githubRunId,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
190
318
|
const userId = boot.chimphands_service_account_user_id || "";
|
|
191
319
|
if (userId) {
|
|
192
320
|
process.env.TESTCHIMP_USER_ID = userId;
|
|
193
321
|
}
|
|
194
322
|
mkdirSync(".opencode", { recursive: true });
|
|
195
|
-
writeOpencodeConfig(backend, apiKey, boot);
|
|
323
|
+
const opencodeModel = writeOpencodeConfig(backend, apiKey, boot);
|
|
324
|
+
console.error(`ChimpHands OpenCode model: ${opencodeModel}`);
|
|
196
325
|
const idleMs = (Number(boot.idle_timeout_seconds) || 600) * 1000;
|
|
197
326
|
const queue = [];
|
|
198
327
|
let idle = false;
|
|
@@ -200,7 +329,7 @@ export async function runChimphands(opts) {
|
|
|
200
329
|
let lastUserActivity = Date.now();
|
|
201
330
|
const postEvent = (role, content, status) => {
|
|
202
331
|
const body = {
|
|
203
|
-
|
|
332
|
+
sessionId,
|
|
204
333
|
role,
|
|
205
334
|
content: String(content || "").slice(0, 20000),
|
|
206
335
|
};
|
|
@@ -209,9 +338,11 @@ export async function runChimphands(opts) {
|
|
|
209
338
|
postJsonFireAndForget(backend, apiKey, "/api/chimphands/post_agent_event", body);
|
|
210
339
|
};
|
|
211
340
|
const complete = (status, errorMessage) => {
|
|
212
|
-
const body = {
|
|
341
|
+
const body = { sessionId, status };
|
|
213
342
|
if (errorMessage)
|
|
214
|
-
body.
|
|
343
|
+
body.errorMessage = String(errorMessage).slice(0, 4000);
|
|
344
|
+
if (githubRunId)
|
|
345
|
+
body.githubRunId = githubRunId;
|
|
215
346
|
postJsonFireAndForget(backend, apiKey, "/api/chimphands/complete_session", body);
|
|
216
347
|
};
|
|
217
348
|
const childEnv = {
|
|
@@ -222,7 +353,7 @@ export async function runChimphands(opts) {
|
|
|
222
353
|
if (userId)
|
|
223
354
|
childEnv.TESTCHIMP_USER_ID = userId;
|
|
224
355
|
connectInbound(backend, apiKey, sessionId, (content) => {
|
|
225
|
-
queue.push(content);
|
|
356
|
+
queue.push(ensureTestchimpPrompt(content));
|
|
226
357
|
lastUserActivity = Date.now();
|
|
227
358
|
}, () => {
|
|
228
359
|
idle = true;
|
|
@@ -230,18 +361,18 @@ export async function runChimphands(opts) {
|
|
|
230
361
|
closed = true;
|
|
231
362
|
});
|
|
232
363
|
postEvent(ROLE_STATUS, "Agent ready", STATUS_RUNNING);
|
|
233
|
-
let prompt = promptInput || boot.initial_prompt || "";
|
|
364
|
+
let prompt = ensureTestchimpPrompt(promptInput || boot.initial_prompt || "");
|
|
234
365
|
if (boot.conversation_summary) {
|
|
235
366
|
prompt = `Conversation so far:\n${boot.conversation_summary}\n\nCurrent task:\n${prompt}`;
|
|
236
367
|
}
|
|
237
368
|
for (const m of boot.pending_user_messages || []) {
|
|
238
369
|
if (m?.content)
|
|
239
|
-
queue.push(m.content);
|
|
370
|
+
queue.push(ensureTestchimpPrompt(m.content));
|
|
240
371
|
}
|
|
241
372
|
const waitForNextPrompt = () => new Promise((resolve) => {
|
|
242
373
|
const tick = () => {
|
|
243
374
|
if (queue.length) {
|
|
244
|
-
resolve(queue.shift());
|
|
375
|
+
resolve(ensureTestchimpPrompt(queue.shift()));
|
|
245
376
|
return;
|
|
246
377
|
}
|
|
247
378
|
if (idle || closed || Date.now() - lastUserActivity >= idleMs) {
|
|
@@ -253,14 +384,39 @@ export async function runChimphands(opts) {
|
|
|
253
384
|
tick();
|
|
254
385
|
});
|
|
255
386
|
while (prompt) {
|
|
256
|
-
const result = await runOpencode(prompt, childEnv, postEvent);
|
|
387
|
+
const result = await runOpencode(ensureTestchimpPrompt(prompt), opencodeModel, childEnv, postEvent);
|
|
257
388
|
if (result.code !== 0) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
389
|
+
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
390
|
+
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
391
|
+
try {
|
|
392
|
+
await postJson(backend, apiKey, "/api/chimphands/post_agent_event", {
|
|
393
|
+
sessionId,
|
|
394
|
+
role: ROLE_STATUS,
|
|
395
|
+
content: errMsg,
|
|
396
|
+
status: STATUS_FAILED,
|
|
397
|
+
githubRunId: githubRunId || undefined,
|
|
398
|
+
});
|
|
399
|
+
await postJson(backend, apiKey, "/api/chimphands/complete_session", {
|
|
400
|
+
sessionId,
|
|
401
|
+
status: STATUS_FAILED,
|
|
402
|
+
errorMessage: errMsg,
|
|
403
|
+
githubRunId: githubRunId || undefined,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (reportErr) {
|
|
407
|
+
const detail = reportErr instanceof Error ? reportErr.message : String(reportErr);
|
|
408
|
+
console.error(`ChimpHands failed to report OpenCode error to backend: ${detail}`);
|
|
409
|
+
postEvent(ROLE_STATUS, errMsg, STATUS_FAILED);
|
|
410
|
+
complete(STATUS_FAILED, errMsg);
|
|
411
|
+
}
|
|
412
|
+
process.exit(result.code || 1);
|
|
261
413
|
}
|
|
262
414
|
postEvent(ROLE_STATUS, "Waiting for user input", STATUS_WAITING_USER);
|
|
415
|
+
// Idle countdown starts when the agent finishes a turn, not at job bootstrap.
|
|
416
|
+
lastUserActivity = Date.now();
|
|
417
|
+
idle = false;
|
|
263
418
|
prompt = (await waitForNextPrompt()) || "";
|
|
264
419
|
}
|
|
420
|
+
console.error("ChimpHands session idle — no user input before timeout; completing.");
|
|
265
421
|
complete(STATUS_IDLE);
|
|
266
422
|
}
|
package/dist/cli/program.js
CHANGED
|
@@ -1578,7 +1578,7 @@ export function buildCliProgram() {
|
|
|
1578
1578
|
catch (e) {
|
|
1579
1579
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1580
1580
|
console.error(`[testchimp chimphands] ${msg}`);
|
|
1581
|
-
process.
|
|
1581
|
+
process.exit(1);
|
|
1582
1582
|
}
|
|
1583
1583
|
});
|
|
1584
1584
|
program.on("--help", () => {
|
package/package.json
CHANGED