@testchimp/cli 0.1.35 → 0.1.36
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 +125 -27
- 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,72 @@ 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
|
+
function resolveOpencodeModel(boot) {
|
|
51
|
+
const raw = (boot.llm_model || "gpt-4o-mini").trim();
|
|
52
|
+
return raw.includes("/") ? raw : `openai/${raw}`;
|
|
53
|
+
}
|
|
54
|
+
function extractOpencodeFatalError(raw) {
|
|
55
|
+
const line = raw.trim();
|
|
56
|
+
if (!line)
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
const ev = JSON.parse(line);
|
|
60
|
+
if (ev.type === "error" || ev.name === "UnknownError") {
|
|
61
|
+
const msg = ev.data?.message || ev.message || line;
|
|
62
|
+
const ref = ev.data?.ref ? ` (ref ${ev.data.ref})` : "";
|
|
63
|
+
return `${msg}${ref}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* plain text */
|
|
68
|
+
}
|
|
69
|
+
if (line.includes("Unexpected server error") && line.includes("UnknownError")) {
|
|
70
|
+
return line;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
42
74
|
function writeOpencodeConfig(backend, apiKey, boot) {
|
|
43
75
|
const llmBase = (boot.llm_base_url || `${backend}/v1`).replace(/\/$/, "");
|
|
44
76
|
const llmKey = apiKey || boot.llm_api_key || "";
|
|
45
|
-
const
|
|
77
|
+
const model = resolveOpencodeModel(boot);
|
|
78
|
+
const mcpEnv = {
|
|
79
|
+
TESTCHIMP_API_KEY: apiKey,
|
|
80
|
+
TESTCHIMP_BACKEND_URL: backend,
|
|
81
|
+
};
|
|
82
|
+
const serviceUserId = boot.chimphands_service_account_user_id?.trim();
|
|
83
|
+
if (serviceUserId) {
|
|
84
|
+
mcpEnv.TESTCHIMP_USER_ID = serviceUserId;
|
|
85
|
+
}
|
|
46
86
|
writeFileSync("opencode.json", JSON.stringify({
|
|
47
|
-
|
|
87
|
+
$schema: "https://opencode.ai/config.json",
|
|
88
|
+
model,
|
|
89
|
+
autoupdate: false,
|
|
90
|
+
enabled_providers: ["openai"],
|
|
48
91
|
provider: {
|
|
49
92
|
openai: {
|
|
50
|
-
|
|
51
|
-
|
|
93
|
+
options: {
|
|
94
|
+
apiKey: llmKey,
|
|
95
|
+
baseURL: llmBase,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
mcp: {
|
|
100
|
+
testchimp: {
|
|
101
|
+
type: "local",
|
|
102
|
+
enabled: true,
|
|
103
|
+
command: ["npx", "-y", "@testchimp/cli@latest", "mcp"],
|
|
104
|
+
environment: mcpEnv,
|
|
52
105
|
},
|
|
53
106
|
},
|
|
54
107
|
}, null, 2));
|
|
108
|
+
return model;
|
|
55
109
|
}
|
|
56
|
-
function runOpencode(prompt, childEnv, postEvent) {
|
|
110
|
+
function runOpencode(prompt, model, childEnv, postEvent) {
|
|
57
111
|
const help = (() => {
|
|
58
112
|
try {
|
|
59
113
|
return execFileSync("opencode", ["run", "--help"], { encoding: "utf8", env: childEnv });
|
|
@@ -63,8 +117,9 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
63
117
|
}
|
|
64
118
|
})();
|
|
65
119
|
const useJson = help.includes("--format");
|
|
120
|
+
const baseArgs = ["run", prompt, "--model", model];
|
|
66
121
|
if (useJson) {
|
|
67
|
-
const child = spawn("opencode", [
|
|
122
|
+
const child = spawn("opencode", [...baseArgs, "--format", "json"], {
|
|
68
123
|
stdio: ["ignore", "pipe", "pipe"],
|
|
69
124
|
env: childEnv,
|
|
70
125
|
});
|
|
@@ -74,6 +129,7 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
74
129
|
});
|
|
75
130
|
return new Promise((resolve) => {
|
|
76
131
|
let buf = "";
|
|
132
|
+
let fatalError = null;
|
|
77
133
|
child.stdout.on("data", (chunk) => {
|
|
78
134
|
buf += chunk.toString();
|
|
79
135
|
const lines = buf.split("\n");
|
|
@@ -81,6 +137,11 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
81
137
|
for (const line of lines) {
|
|
82
138
|
if (!line.trim())
|
|
83
139
|
continue;
|
|
140
|
+
const fatal = extractOpencodeFatalError(line);
|
|
141
|
+
if (fatal) {
|
|
142
|
+
fatalError = fatal;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
84
145
|
let content = line;
|
|
85
146
|
let role = ROLE_ASSISTANT;
|
|
86
147
|
try {
|
|
@@ -98,27 +159,48 @@ function runOpencode(prompt, childEnv, postEvent) {
|
|
|
98
159
|
}
|
|
99
160
|
});
|
|
100
161
|
child.on("close", (code) => {
|
|
101
|
-
if (buf.trim())
|
|
102
|
-
|
|
162
|
+
if (buf.trim()) {
|
|
163
|
+
const fatal = extractOpencodeFatalError(buf);
|
|
164
|
+
if (fatal)
|
|
165
|
+
fatalError = fatal;
|
|
166
|
+
else if (!fatalError)
|
|
167
|
+
postEvent(ROLE_ASSISTANT, buf.trim());
|
|
168
|
+
}
|
|
169
|
+
const stderrFatal = extractOpencodeFatalError(err);
|
|
170
|
+
if (stderrFatal)
|
|
171
|
+
fatalError = stderrFatal;
|
|
172
|
+
if (fatalError) {
|
|
173
|
+
resolve({ code: 1, err: fatalError });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
103
176
|
resolve({ code: code == null ? 1 : code, err });
|
|
104
177
|
});
|
|
105
178
|
});
|
|
106
179
|
}
|
|
107
180
|
try {
|
|
108
|
-
const out = execFileSync("opencode",
|
|
181
|
+
const out = execFileSync("opencode", baseArgs, {
|
|
109
182
|
encoding: "utf8",
|
|
110
183
|
maxBuffer: 20 * 1024 * 1024,
|
|
111
184
|
stdio: ["ignore", "pipe", "pipe"],
|
|
112
185
|
env: childEnv,
|
|
113
186
|
});
|
|
187
|
+
const fatal = extractOpencodeFatalError(out);
|
|
188
|
+
if (fatal)
|
|
189
|
+
return Promise.resolve({ code: 1, err: fatal });
|
|
114
190
|
if (out)
|
|
115
191
|
postEvent(ROLE_ASSISTANT, out);
|
|
116
192
|
return Promise.resolve({ code: 0, err: "" });
|
|
117
193
|
}
|
|
118
194
|
catch (e) {
|
|
119
195
|
const errObj = e;
|
|
120
|
-
const
|
|
121
|
-
|
|
196
|
+
const stderr = errObj.stderr?.toString() || "";
|
|
197
|
+
const stdout = errObj.stdout?.toString() || "";
|
|
198
|
+
const fatal = extractOpencodeFatalError(stderr) ||
|
|
199
|
+
extractOpencodeFatalError(stdout) ||
|
|
200
|
+
stderr ||
|
|
201
|
+
errObj.message ||
|
|
202
|
+
"opencode failed";
|
|
203
|
+
return Promise.resolve({ code: errObj.status || 1, err: fatal });
|
|
122
204
|
}
|
|
123
205
|
}
|
|
124
206
|
function connectInbound(backend, apiKey, sessionId, onUserMessage, onIdle, onClosed) {
|
|
@@ -184,15 +266,23 @@ export async function runChimphands(opts) {
|
|
|
184
266
|
}
|
|
185
267
|
const promptInput = (opts.prompt ?? process.env.PROMPT ?? "").trim();
|
|
186
268
|
const bootText = await postJson(backend, apiKey, "/api/chimphands/bootstrap", {
|
|
187
|
-
|
|
269
|
+
sessionId,
|
|
188
270
|
});
|
|
189
271
|
const boot = JSON.parse(bootText);
|
|
272
|
+
const githubRunId = (process.env.GITHUB_RUN_ID || "").trim();
|
|
273
|
+
if (githubRunId) {
|
|
274
|
+
postJsonFireAndForget(backend, apiKey, "/api/chimphands/post_agent_event", {
|
|
275
|
+
sessionId,
|
|
276
|
+
githubRunId,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
190
279
|
const userId = boot.chimphands_service_account_user_id || "";
|
|
191
280
|
if (userId) {
|
|
192
281
|
process.env.TESTCHIMP_USER_ID = userId;
|
|
193
282
|
}
|
|
194
283
|
mkdirSync(".opencode", { recursive: true });
|
|
195
|
-
writeOpencodeConfig(backend, apiKey, boot);
|
|
284
|
+
const opencodeModel = writeOpencodeConfig(backend, apiKey, boot);
|
|
285
|
+
console.error(`ChimpHands OpenCode model: ${opencodeModel}`);
|
|
196
286
|
const idleMs = (Number(boot.idle_timeout_seconds) || 600) * 1000;
|
|
197
287
|
const queue = [];
|
|
198
288
|
let idle = false;
|
|
@@ -200,7 +290,7 @@ export async function runChimphands(opts) {
|
|
|
200
290
|
let lastUserActivity = Date.now();
|
|
201
291
|
const postEvent = (role, content, status) => {
|
|
202
292
|
const body = {
|
|
203
|
-
|
|
293
|
+
sessionId,
|
|
204
294
|
role,
|
|
205
295
|
content: String(content || "").slice(0, 20000),
|
|
206
296
|
};
|
|
@@ -209,9 +299,11 @@ export async function runChimphands(opts) {
|
|
|
209
299
|
postJsonFireAndForget(backend, apiKey, "/api/chimphands/post_agent_event", body);
|
|
210
300
|
};
|
|
211
301
|
const complete = (status, errorMessage) => {
|
|
212
|
-
const body = {
|
|
302
|
+
const body = { sessionId, status };
|
|
213
303
|
if (errorMessage)
|
|
214
|
-
body.
|
|
304
|
+
body.errorMessage = String(errorMessage).slice(0, 4000);
|
|
305
|
+
if (githubRunId)
|
|
306
|
+
body.githubRunId = githubRunId;
|
|
215
307
|
postJsonFireAndForget(backend, apiKey, "/api/chimphands/complete_session", body);
|
|
216
308
|
};
|
|
217
309
|
const childEnv = {
|
|
@@ -222,7 +314,7 @@ export async function runChimphands(opts) {
|
|
|
222
314
|
if (userId)
|
|
223
315
|
childEnv.TESTCHIMP_USER_ID = userId;
|
|
224
316
|
connectInbound(backend, apiKey, sessionId, (content) => {
|
|
225
|
-
queue.push(content);
|
|
317
|
+
queue.push(ensureTestchimpPrompt(content));
|
|
226
318
|
lastUserActivity = Date.now();
|
|
227
319
|
}, () => {
|
|
228
320
|
idle = true;
|
|
@@ -230,18 +322,18 @@ export async function runChimphands(opts) {
|
|
|
230
322
|
closed = true;
|
|
231
323
|
});
|
|
232
324
|
postEvent(ROLE_STATUS, "Agent ready", STATUS_RUNNING);
|
|
233
|
-
let prompt = promptInput || boot.initial_prompt || "";
|
|
325
|
+
let prompt = ensureTestchimpPrompt(promptInput || boot.initial_prompt || "");
|
|
234
326
|
if (boot.conversation_summary) {
|
|
235
327
|
prompt = `Conversation so far:\n${boot.conversation_summary}\n\nCurrent task:\n${prompt}`;
|
|
236
328
|
}
|
|
237
329
|
for (const m of boot.pending_user_messages || []) {
|
|
238
330
|
if (m?.content)
|
|
239
|
-
queue.push(m.content);
|
|
331
|
+
queue.push(ensureTestchimpPrompt(m.content));
|
|
240
332
|
}
|
|
241
333
|
const waitForNextPrompt = () => new Promise((resolve) => {
|
|
242
334
|
const tick = () => {
|
|
243
335
|
if (queue.length) {
|
|
244
|
-
resolve(queue.shift());
|
|
336
|
+
resolve(ensureTestchimpPrompt(queue.shift()));
|
|
245
337
|
return;
|
|
246
338
|
}
|
|
247
339
|
if (idle || closed || Date.now() - lastUserActivity >= idleMs) {
|
|
@@ -253,14 +345,20 @@ export async function runChimphands(opts) {
|
|
|
253
345
|
tick();
|
|
254
346
|
});
|
|
255
347
|
while (prompt) {
|
|
256
|
-
const result = await runOpencode(prompt, childEnv, postEvent);
|
|
348
|
+
const result = await runOpencode(ensureTestchimpPrompt(prompt), opencodeModel, childEnv, postEvent);
|
|
257
349
|
if (result.code !== 0) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
350
|
+
const errMsg = result.err || "opencode failed";
|
|
351
|
+
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
352
|
+
postEvent(ROLE_STATUS, errMsg, STATUS_FAILED);
|
|
353
|
+
complete(STATUS_FAILED, errMsg);
|
|
354
|
+
process.exit(result.code || 1);
|
|
261
355
|
}
|
|
262
356
|
postEvent(ROLE_STATUS, "Waiting for user input", STATUS_WAITING_USER);
|
|
357
|
+
// Idle countdown starts when the agent finishes a turn, not at job bootstrap.
|
|
358
|
+
lastUserActivity = Date.now();
|
|
359
|
+
idle = false;
|
|
263
360
|
prompt = (await waitForNextPrompt()) || "";
|
|
264
361
|
}
|
|
362
|
+
console.error("ChimpHands session idle — no user input before timeout; completing.");
|
|
265
363
|
complete(STATUS_IDLE);
|
|
266
364
|
}
|
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