@testchimp/cli 0.1.61 → 0.1.63
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 +2 -0
- package/dist/chimphands/run.js +156 -14
- package/package.json +1 -1
package/dist/chimphands/run.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ type RunOptions = {
|
|
|
19
19
|
runtimeId?: string;
|
|
20
20
|
};
|
|
21
21
|
export declare function runChimphands(opts: RunOptions): Promise<void>;
|
|
22
|
+
/** OpenCode workspace-scoped APIs require directory routing (header + query). */
|
|
23
|
+
export declare function appendOpencodeDirectoryRouting(path: string, query: string | undefined, headers: Record<string, string>, directory: string): string;
|
|
22
24
|
/** Runtime-aware entry: register + attach to local OpenCode server (Phase 1+). */
|
|
23
25
|
export declare function serveChimphands(opts: RunOptions & {
|
|
24
26
|
attachUrl: string;
|
package/dist/chimphands/run.js
CHANGED
|
@@ -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
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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();
|
|
@@ -2018,9 +2122,32 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
2018
2122
|
console.error(`ChimpHands commit-before-idle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2019
2123
|
}
|
|
2020
2124
|
}
|
|
2125
|
+
function opencodeProxyNeedsDirectory(path) {
|
|
2126
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
2127
|
+
return (p.startsWith("/vcs") ||
|
|
2128
|
+
p.startsWith("/file") ||
|
|
2129
|
+
p.startsWith("/path") ||
|
|
2130
|
+
p.startsWith("/session") ||
|
|
2131
|
+
p.startsWith("/instance") ||
|
|
2132
|
+
p.startsWith("/event") ||
|
|
2133
|
+
p.startsWith("/global/event"));
|
|
2134
|
+
}
|
|
2135
|
+
/** OpenCode workspace-scoped APIs require directory routing (header + query). */
|
|
2136
|
+
export function appendOpencodeDirectoryRouting(path, query, headers, directory) {
|
|
2137
|
+
if (!opencodeProxyNeedsDirectory(path)) {
|
|
2138
|
+
return query || "";
|
|
2139
|
+
}
|
|
2140
|
+
headers["x-opencode-directory"] = directory;
|
|
2141
|
+
if (query?.includes("directory=")) {
|
|
2142
|
+
return query;
|
|
2143
|
+
}
|
|
2144
|
+
const dirParam = `directory=${encodeURIComponent(directory)}`;
|
|
2145
|
+
return query ? `${query}&${dirParam}` : dirParam;
|
|
2146
|
+
}
|
|
2021
2147
|
function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, handlers) {
|
|
2022
2148
|
let stopped = false;
|
|
2023
2149
|
const base = attachUrl.replace(/\/$/, "");
|
|
2150
|
+
const workDirectory = process.cwd();
|
|
2024
2151
|
let ws = null;
|
|
2025
2152
|
let reconnectTimer = null;
|
|
2026
2153
|
let backoffMs = 1000;
|
|
@@ -2036,8 +2163,10 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, han
|
|
|
2036
2163
|
if (!req.requestId || socket.readyState !== 1)
|
|
2037
2164
|
return;
|
|
2038
2165
|
const requestId = req.requestId;
|
|
2039
|
-
const
|
|
2166
|
+
const pathPart = req.path || "/";
|
|
2040
2167
|
const headers = { ...(req.headers || {}) };
|
|
2168
|
+
const query = appendOpencodeDirectoryRouting(pathPart, req.query, headers, workDirectory);
|
|
2169
|
+
const target = base + pathPart + (query ? `?${query}` : "");
|
|
2041
2170
|
const init = { method: req.method || "GET", headers };
|
|
2042
2171
|
if (req.bodyBase64) {
|
|
2043
2172
|
init.body = Buffer.from(req.bodyBase64, "base64");
|
|
@@ -2154,9 +2283,22 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, han
|
|
|
2154
2283
|
if (payload?.sessionId && payload.sessionId !== sessionId) {
|
|
2155
2284
|
return;
|
|
2156
2285
|
}
|
|
2286
|
+
console.error(`ChimpHands tunnel cancel_turn for session ${sessionId}`);
|
|
2157
2287
|
handlers?.onCancelTurn?.();
|
|
2158
2288
|
return;
|
|
2159
2289
|
}
|
|
2290
|
+
if (frame.type === "inbound_event" && frame.event === "user_message" && frame.data) {
|
|
2291
|
+
const payload = frame.data;
|
|
2292
|
+
if (payload?.sessionId && payload.sessionId !== sessionId) {
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
const msg = parseInboundUserMessage(frame.data);
|
|
2296
|
+
if (msg) {
|
|
2297
|
+
console.error(`ChimpHands tunnel user_message for session ${sessionId}`);
|
|
2298
|
+
handlers?.onUserMessage?.(msg);
|
|
2299
|
+
}
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2160
2302
|
if (frame.type === "inbound_event" && frame.event === "user_file_edit" && frame.data) {
|
|
2161
2303
|
const edit = frame.data;
|
|
2162
2304
|
if (edit.sessionId && edit.sessionId !== sessionId) {
|
package/package.json
CHANGED