@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.
@@ -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
- function resolveOpencodeModel(boot) {
50
+ const TESTCHIMP_PROVIDER_ID = "testchimp";
51
+ function resolveOpencodeModelId(boot) {
51
52
  const raw = (boot.llm_model || "gpt-4o-mini").trim();
52
- return raw.includes("/") ? raw : `openai/${raw}`;
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 || ev.message || line;
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 model = resolveOpencodeModel(boot);
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
- openai: {
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
- if (!line.trim())
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
- const fatal = extractOpencodeFatalError(buf);
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 = extractOpencodeFatalError(stderr) ||
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
- postEvent(ROLE_STATUS, errMsg, STATUS_FAILED);
353
- complete(STATUS_FAILED, errMsg);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "TestChimp CLI and MCP server — coverage, plans, EaaS, TrueCoverage, API operations (calls /api/mcp/*)",
5
5
  "type": "module",
6
6
  "main": "dist/bin/testchimp.js",