@testchimp/cli 0.1.61 → 0.1.62

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.
@@ -168,6 +168,8 @@ class AgentEventPoster {
168
168
  body.workingBranch = opts.workingBranch;
169
169
  if (opts?.pullRequestUrl)
170
170
  body.pullRequestUrl = opts.pullRequestUrl;
171
+ if (opts?.toolExecution)
172
+ body.toolExecution = opts.toolExecution;
171
173
  const streamRole = isStreamFanoutRole(role);
172
174
  this.chain = this.chain.then(async () => {
173
175
  const live = !!opts?.liveStream && !opts?.durable;
@@ -193,6 +195,8 @@ class AgentEventPoster {
193
195
  eph.messageId = opts.messageId;
194
196
  if (opts?.opencodeSessionId)
195
197
  eph.opencodeSessionId = opts.opencodeSessionId;
198
+ if (opts?.toolExecution)
199
+ eph.toolExecution = opts.toolExecution;
196
200
  try {
197
201
  await postJson(this.backend, this.apiKey, "/api/chimphands/post_ephemeral_agent_event", eph);
198
202
  this.ephemeralFailCount = 0;
@@ -282,6 +286,62 @@ function extractOpencodeFatalError(raw) {
282
286
  }
283
287
  return null;
284
288
  }
289
+ const TOOL_STATUS_RUNNING = "CHIMPHANDS_TOOL_EXECUTION_STATUS_RUNNING";
290
+ const TOOL_STATUS_PENDING = "CHIMPHANDS_TOOL_EXECUTION_STATUS_PENDING";
291
+ const TOOL_STATUS_COMPLETED = "CHIMPHANDS_TOOL_EXECUTION_STATUS_COMPLETED";
292
+ const TOOL_STATUS_ERROR = "CHIMPHANDS_TOOL_EXECUTION_STATUS_ERROR";
293
+ function normalizeEpochMillis(raw) {
294
+ if (raw == null || !Number.isFinite(raw) || raw <= 0)
295
+ return undefined;
296
+ // OpenCode uses epoch millis; guard seconds.
297
+ return raw < 1_000_000_000_000 ? Math.round(raw * 1000) : Math.round(raw);
298
+ }
299
+ function coerceTimeoutMillis(input) {
300
+ if (!input)
301
+ return undefined;
302
+ const raw = input.timeout ?? input.timeoutMs ?? input.timeout_ms;
303
+ const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN;
304
+ if (!Number.isFinite(n) || n <= 0)
305
+ return undefined;
306
+ return Math.round(n);
307
+ }
308
+ function mapToolExecutionStatus(status) {
309
+ const s = String(status || "").trim().toLowerCase();
310
+ if (!s)
311
+ return undefined;
312
+ if (s === "pending")
313
+ return TOOL_STATUS_PENDING;
314
+ if (s === "running")
315
+ return TOOL_STATUS_RUNNING;
316
+ if (s === "completed")
317
+ return TOOL_STATUS_COMPLETED;
318
+ if (s === "error" || s === "failed" || s === "cancelled")
319
+ return TOOL_STATUS_ERROR;
320
+ return undefined;
321
+ }
322
+ function toolExecutionFromOpencodePart(part) {
323
+ const status = mapToolExecutionStatus(part.state?.status);
324
+ if (!status)
325
+ return undefined;
326
+ const startedAtMillis = normalizeEpochMillis(part.state?.time?.start) ??
327
+ (status === TOOL_STATUS_RUNNING ? Date.now() : undefined);
328
+ const endedAtMillis = normalizeEpochMillis(part.state?.time?.end);
329
+ const toolName = String(part.state?.title || part.tool || "").trim() || undefined;
330
+ const timeoutMillis = coerceTimeoutMillis(part.state?.input);
331
+ return {
332
+ status,
333
+ toolName,
334
+ startedAtMillis,
335
+ timeoutMillis,
336
+ endedAtMillis,
337
+ };
338
+ }
339
+ function toolEventOptions(part, extra) {
340
+ const toolExecution = toolExecutionFromOpencodePart(part);
341
+ if (!toolExecution)
342
+ return extra ?? {};
343
+ return { ...extra, toolExecution };
344
+ }
285
345
  /**
286
346
  * OpenCode `message.part.delta` always sends `field: "text"` for both reasoning and
287
347
  * answer parts. Resolve kind from partID → type learned via `message.part.updated`.
@@ -482,9 +542,9 @@ async function reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, po
482
542
  if (!status || status === "pending" || status === "running")
483
543
  continue;
484
544
  const toolContent = formatToolUseContent(part);
485
- postEvent(ROLE_TOOL, toolContent, {
545
+ postEvent(ROLE_TOOL, toolContent, toolEventOptions(part, {
486
546
  messageId: opencodeMessageId("oc_tool_", part),
487
- });
547
+ }));
488
548
  posted += 1;
489
549
  if (status === "completed") {
490
550
  const detected = detectWorkingBranchFromToolOutput(toolContent);
@@ -663,17 +723,17 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
663
723
  // Ephemeral "(running)" bubbles while UI attached (liveStream gated in poster).
664
724
  if (status === "running") {
665
725
  const toolContent = formatToolUseContent(part);
666
- callbacks.postEvent(ROLE_TOOL, toolContent, liveOpts({
726
+ callbacks.postEvent(ROLE_TOOL, toolContent, toolEventOptions(part, liveOpts({
667
727
  messageId: opencodeMessageId("oc_tool_", part),
668
728
  throttle: false,
669
- }));
729
+ })));
670
730
  return;
671
731
  }
672
732
  const toolContent = formatToolUseContent(part);
673
- callbacks.postEvent(ROLE_TOOL, toolContent, liveOpts({
733
+ callbacks.postEvent(ROLE_TOOL, toolContent, toolEventOptions(part, liveOpts({
674
734
  messageId: opencodeMessageId("oc_tool_", part),
675
735
  throttle: false,
676
- }));
736
+ })));
677
737
  if (status === "completed") {
678
738
  const detected = detectWorkingBranchFromToolOutput(toolContent);
679
739
  if (detected.branch) {
@@ -946,6 +1006,35 @@ function writeOpencodeConfig(backend, apiKey, boot) {
946
1006
  }, null, 2));
947
1007
  return model;
948
1008
  }
1009
+ /** Abort in-flight tool/LLM work on the local OpenCode server (attach mode). */
1010
+ async function abortOpencodeSession(attachUrl, opencodeSessionId) {
1011
+ const base = attachUrl.replace(/\/$/, "");
1012
+ const url = `${base}/session/${encodeURIComponent(opencodeSessionId)}/abort`;
1013
+ try {
1014
+ const res = await fetch(url, {
1015
+ method: "POST",
1016
+ headers: {
1017
+ Accept: "application/json",
1018
+ "x-opencode-directory": process.cwd(),
1019
+ },
1020
+ });
1021
+ console.error(`ChimpHands OpenCode session abort: session=${opencodeSessionId} http=${res.status}`);
1022
+ }
1023
+ catch (err) {
1024
+ console.error(`ChimpHands OpenCode session abort failed: ${err instanceof Error ? err.message : String(err)}`);
1025
+ }
1026
+ }
1027
+ function parseInboundUserMessage(data) {
1028
+ if (!data || typeof data !== "object")
1029
+ return null;
1030
+ const msg = data;
1031
+ const content = typeof msg.content === "string" ? msg.content : "";
1032
+ const idRaw = (typeof msg.id === "string" && msg.id) ||
1033
+ (typeof msg.message_id === "string" && msg.message_id) ||
1034
+ (typeof msg.messageId === "string" && msg.messageId) ||
1035
+ "";
1036
+ return { id: idRaw || undefined, content };
1037
+ }
949
1038
  function buildOpencodeArgs(prompt, model, opencodeSessionId, attachUrl) {
950
1039
  // --auto: headless CI must approve tool permissions (1.18+ otherwise auto-rejects).
951
1040
  // --print-logs: surface server/client progress on stderr while waiting for first token.
@@ -1228,9 +1317,9 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
1228
1317
  if (!status || status === "pending" || status === "running")
1229
1318
  return;
1230
1319
  const toolContent = formatToolUseContent(ev.part);
1231
- callbacks.postEvent(ROLE_TOOL, toolContent, {
1320
+ callbacks.postEvent(ROLE_TOOL, toolContent, toolEventOptions(ev.part, {
1232
1321
  messageId: opencodeMessageId("oc_tool_", ev.part),
1233
- });
1322
+ }));
1234
1323
  if (status === "completed") {
1235
1324
  const detected = detectWorkingBranchFromToolOutput(toolContent);
1236
1325
  if (detected.branch) {
@@ -1409,6 +1498,7 @@ function connectInboundStream(backend, apiKey, sessionId, handlers) {
1409
1498
  catch {
1410
1499
  /* ignore malformed payload */
1411
1500
  }
1501
+ console.error(`ChimpHands inbound SSE cancel_turn for session ${sessionId}`);
1412
1502
  handlers.onCancelTurn?.();
1413
1503
  }
1414
1504
  else if (eventName === "user_file_edit") {
@@ -1432,11 +1522,11 @@ function connectInboundStream(backend, apiKey, sessionId, handlers) {
1432
1522
  }
1433
1523
  else if (eventName === "user_message" || eventName === "message") {
1434
1524
  try {
1435
- const msg = JSON.parse(data);
1436
- handlers.onUserMessage({
1437
- id: msg.id || msg.message_id,
1438
- content: msg.content || "",
1439
- });
1525
+ const msg = parseInboundUserMessage(JSON.parse(data));
1526
+ if (!msg)
1527
+ continue;
1528
+ console.error(`ChimpHands inbound SSE user_message for session ${sessionId}`);
1529
+ handlers.onUserMessage(msg);
1440
1530
  }
1441
1531
  catch {
1442
1532
  /* ignore */
@@ -1643,6 +1733,8 @@ export async function runChimphands(opts) {
1643
1733
  queue.push(content);
1644
1734
  lastUserActivity = Date.now();
1645
1735
  idle = false;
1736
+ const preview = content.length > 120 ? `${content.slice(0, 117)}...` : content;
1737
+ console.error(`ChimpHands queued user message (turnActive=${agentTurnInProgress}, depth=${queue.length}): ${JSON.stringify(preview)}`);
1646
1738
  };
1647
1739
  const pollPendingUserMessages = async () => {
1648
1740
  try {
@@ -1704,7 +1796,12 @@ export async function runChimphands(opts) {
1704
1796
  if (userId)
1705
1797
  childEnv.TESTCHIMP_USER_ID = userId;
1706
1798
  const handleCancelTurn = () => {
1799
+ console.error(`ChimpHands cancel_turn received for session ${sessionId}`);
1707
1800
  cancelTurnRequested = true;
1801
+ const sessionToAbort = opencodeSessionId?.trim();
1802
+ if (attachUrl && sessionToAbort) {
1803
+ void abortOpencodeSession(attachUrl, sessionToAbort);
1804
+ }
1708
1805
  const ch = activeOpencodeChild;
1709
1806
  if (ch && !ch.killed) {
1710
1807
  try {
@@ -1733,14 +1830,21 @@ export async function runChimphands(opts) {
1733
1830
  onUserFileEdit: handleUserFileEdit,
1734
1831
  shouldRun: () => sessionActive,
1735
1832
  });
1833
+ const inboundPollTimer = setInterval(() => {
1834
+ if (!sessionActive)
1835
+ return;
1836
+ void pollPendingUserMessages();
1837
+ }, 1500);
1736
1838
  if (runtimeId && attachUrl) {
1737
1839
  stopTunnel = startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, {
1738
1840
  onUserFileEdit: handleUserFileEdit,
1739
1841
  onCancelTurn: handleCancelTurn,
1842
+ onUserMessage: enqueueUserMessage,
1740
1843
  });
1741
1844
  }
1742
1845
  const shutdownRuntime = async () => {
1743
1846
  sessionActive = false;
1847
+ clearInterval(inboundPollTimer);
1744
1848
  stopInbound();
1745
1849
  stopTunnel();
1746
1850
  stopHeartbeat();
@@ -2154,9 +2258,22 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, han
2154
2258
  if (payload?.sessionId && payload.sessionId !== sessionId) {
2155
2259
  return;
2156
2260
  }
2261
+ console.error(`ChimpHands tunnel cancel_turn for session ${sessionId}`);
2157
2262
  handlers?.onCancelTurn?.();
2158
2263
  return;
2159
2264
  }
2265
+ if (frame.type === "inbound_event" && frame.event === "user_message" && frame.data) {
2266
+ const payload = frame.data;
2267
+ if (payload?.sessionId && payload.sessionId !== sessionId) {
2268
+ return;
2269
+ }
2270
+ const msg = parseInboundUserMessage(frame.data);
2271
+ if (msg) {
2272
+ console.error(`ChimpHands tunnel user_message for session ${sessionId}`);
2273
+ handlers?.onUserMessage?.(msg);
2274
+ }
2275
+ return;
2276
+ }
2160
2277
  if (frame.type === "inbound_event" && frame.event === "user_file_edit" && frame.data) {
2161
2278
  const edit = frame.data;
2162
2279
  if (edit.sessionId && edit.sessionId !== sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.61",
3
+ "version": "0.1.62",
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",