@testchimp/cli 0.1.58 → 0.1.60
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.js +159 -6
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { mkdirSync, openSync, writeFileSync } from "node:fs";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
8
10
|
import http from "node:http";
|
|
9
11
|
import https from "node:https";
|
|
10
12
|
import { URL } from "node:url";
|
|
@@ -1059,6 +1061,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1059
1061
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1060
1062
|
env: childEnv,
|
|
1061
1063
|
});
|
|
1064
|
+
callbacks.onActiveChild?.(child);
|
|
1062
1065
|
try {
|
|
1063
1066
|
child.stdin?.end();
|
|
1064
1067
|
}
|
|
@@ -1232,6 +1235,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1232
1235
|
}
|
|
1233
1236
|
});
|
|
1234
1237
|
child.on("close", (code) => {
|
|
1238
|
+
callbacks.onActiveChild?.(null);
|
|
1235
1239
|
if (progressTicker) {
|
|
1236
1240
|
clearInterval(progressTicker);
|
|
1237
1241
|
progressTicker = null;
|
|
@@ -1242,6 +1246,10 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1242
1246
|
const stderrFatal = extractOpencodeFatalError(err);
|
|
1243
1247
|
if (stderrFatal)
|
|
1244
1248
|
fatalError = stderrFatal;
|
|
1249
|
+
if (callbacks.getCancelRequested?.()) {
|
|
1250
|
+
resolve({ code: 0, err: "", opencodeSessionId: activeSessionId, cancelled: true });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1245
1253
|
if (fatalError) {
|
|
1246
1254
|
resolve({ code: 1, err: fatalError, opencodeSessionId: activeSessionId });
|
|
1247
1255
|
return;
|
|
@@ -1258,6 +1266,63 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1258
1266
|
});
|
|
1259
1267
|
});
|
|
1260
1268
|
}
|
|
1269
|
+
function normalizeWorktreeRelativePath(filePath) {
|
|
1270
|
+
let p = String(filePath || "").trim().replace(/\\/g, "/");
|
|
1271
|
+
while (p.startsWith("/"))
|
|
1272
|
+
p = p.slice(1);
|
|
1273
|
+
if (!p || p.includes("\0")) {
|
|
1274
|
+
throw new Error("invalid path");
|
|
1275
|
+
}
|
|
1276
|
+
const segments = [];
|
|
1277
|
+
for (const part of p.split("/")) {
|
|
1278
|
+
if (!part || part === ".")
|
|
1279
|
+
continue;
|
|
1280
|
+
if (part === "..") {
|
|
1281
|
+
if (!segments.length)
|
|
1282
|
+
throw new Error("invalid path");
|
|
1283
|
+
segments.pop();
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
segments.push(part);
|
|
1287
|
+
}
|
|
1288
|
+
if (!segments.length)
|
|
1289
|
+
throw new Error("invalid path");
|
|
1290
|
+
return segments.join("/");
|
|
1291
|
+
}
|
|
1292
|
+
async function ackWorktreeFileWrite(backend, apiKey, sessionId, requestId, ok, errorMessage) {
|
|
1293
|
+
const body = {
|
|
1294
|
+
sessionId,
|
|
1295
|
+
requestId,
|
|
1296
|
+
ok,
|
|
1297
|
+
};
|
|
1298
|
+
if (errorMessage)
|
|
1299
|
+
body.errorMessage = errorMessage.slice(0, 2000);
|
|
1300
|
+
await postJson(backend, apiKey, "/api/chimphands/ack_worktree_file_write", body).catch((err) => {
|
|
1301
|
+
console.error(`ChimpHands ack_worktree_file_write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
async function applyUserFileEdit(backend, apiKey, sessionId, edit, turnActive) {
|
|
1305
|
+
if (turnActive()) {
|
|
1306
|
+
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, false, "agent turn in progress");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
const relative = normalizeWorktreeRelativePath(edit.path);
|
|
1311
|
+
const root = process.cwd();
|
|
1312
|
+
const full = path.resolve(root, relative);
|
|
1313
|
+
const rootResolved = path.resolve(root);
|
|
1314
|
+
if (full !== rootResolved && !full.startsWith(rootResolved + path.sep)) {
|
|
1315
|
+
throw new Error("path outside worktree");
|
|
1316
|
+
}
|
|
1317
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
1318
|
+
await fs.writeFile(full, edit.content, "utf8");
|
|
1319
|
+
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, true);
|
|
1320
|
+
}
|
|
1321
|
+
catch (err) {
|
|
1322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1323
|
+
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, false, msg);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1261
1326
|
function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
1262
1327
|
const url = new URL(`${backend}/api/chimphands/sessions/${encodeURIComponent(sessionId)}/inbound`);
|
|
1263
1328
|
const lib = url.protocol === "https:" ? https : http;
|
|
@@ -1305,6 +1370,37 @@ function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
|
1305
1370
|
if (eventName === "idle") {
|
|
1306
1371
|
handlers.onIdle();
|
|
1307
1372
|
}
|
|
1373
|
+
else if (eventName === "cancel_turn") {
|
|
1374
|
+
try {
|
|
1375
|
+
const payload = JSON.parse(data);
|
|
1376
|
+
if (payload.sessionId && payload.sessionId !== sessionId) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
/* ignore malformed payload */
|
|
1382
|
+
}
|
|
1383
|
+
handlers.onCancelTurn?.();
|
|
1384
|
+
}
|
|
1385
|
+
else if (eventName === "user_file_edit") {
|
|
1386
|
+
try {
|
|
1387
|
+
const edit = JSON.parse(data);
|
|
1388
|
+
if (edit.sessionId && edit.sessionId !== sessionId) {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const requestId = edit.requestId || edit.request_id;
|
|
1392
|
+
if (requestId && edit.path) {
|
|
1393
|
+
handlers.onUserFileEdit?.({
|
|
1394
|
+
requestId,
|
|
1395
|
+
path: edit.path,
|
|
1396
|
+
content: edit.content ?? "",
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
/* ignore */
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1308
1404
|
else if (eventName === "user_message" || eventName === "message") {
|
|
1309
1405
|
try {
|
|
1310
1406
|
const msg = JSON.parse(data);
|
|
@@ -1450,9 +1546,7 @@ export async function runChimphands(opts) {
|
|
|
1450
1546
|
syncLiveSse(attached);
|
|
1451
1547
|
})
|
|
1452
1548
|
: () => { };
|
|
1453
|
-
|
|
1454
|
-
? startTunnelWorker(backend, apiKey, runtimeId, attachUrl)
|
|
1455
|
-
: () => { };
|
|
1549
|
+
let stopTunnel = () => { };
|
|
1456
1550
|
const userId = bootStr(boot, "chimphands_service_account_user_id", "chimphandsServiceAccountUserId");
|
|
1457
1551
|
if (userId) {
|
|
1458
1552
|
process.env.TESTCHIMP_USER_ID = userId;
|
|
@@ -1498,6 +1592,15 @@ export async function runChimphands(opts) {
|
|
|
1498
1592
|
let sessionActive = true;
|
|
1499
1593
|
let lastUserActivity = Date.now();
|
|
1500
1594
|
let exitCode;
|
|
1595
|
+
let cancelTurnRequested = false;
|
|
1596
|
+
let activeOpencodeChild = null;
|
|
1597
|
+
let agentTurnInProgress = false;
|
|
1598
|
+
const turnControl = {
|
|
1599
|
+
getCancelRequested: () => cancelTurnRequested,
|
|
1600
|
+
onActiveChild: (child) => {
|
|
1601
|
+
activeOpencodeChild = child;
|
|
1602
|
+
},
|
|
1603
|
+
};
|
|
1501
1604
|
const enqueueUserMessage = (msg) => {
|
|
1502
1605
|
const id = msg.id?.trim();
|
|
1503
1606
|
if (id) {
|
|
@@ -1529,6 +1632,13 @@ export async function runChimphands(opts) {
|
|
|
1529
1632
|
// Polling is best-effort when inbound SSE misses an event.
|
|
1530
1633
|
}
|
|
1531
1634
|
};
|
|
1635
|
+
const seenWorktreeWriteRequestIds = new Set();
|
|
1636
|
+
const handleUserFileEdit = (edit) => {
|
|
1637
|
+
if (seenWorktreeWriteRequestIds.has(edit.requestId))
|
|
1638
|
+
return;
|
|
1639
|
+
seenWorktreeWriteRequestIds.add(edit.requestId);
|
|
1640
|
+
void applyUserFileEdit(backend, apiKey, sessionId, edit, () => agentTurnInProgress);
|
|
1641
|
+
};
|
|
1532
1642
|
const postEvent = (role, content, opts) => {
|
|
1533
1643
|
const bodyOpts = { ...opts };
|
|
1534
1644
|
if (opencodeSessionId && !bodyOpts.opencodeSessionId) {
|
|
@@ -1569,8 +1679,26 @@ export async function runChimphands(opts) {
|
|
|
1569
1679
|
onIdle: () => {
|
|
1570
1680
|
idle = true;
|
|
1571
1681
|
},
|
|
1682
|
+
onCancelTurn: () => {
|
|
1683
|
+
cancelTurnRequested = true;
|
|
1684
|
+
const ch = activeOpencodeChild;
|
|
1685
|
+
if (ch && !ch.killed) {
|
|
1686
|
+
try {
|
|
1687
|
+
ch.kill("SIGTERM");
|
|
1688
|
+
}
|
|
1689
|
+
catch {
|
|
1690
|
+
/* ignore */
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
onUserFileEdit: handleUserFileEdit,
|
|
1572
1695
|
shouldRun: () => sessionActive,
|
|
1573
1696
|
});
|
|
1697
|
+
if (runtimeId && attachUrl) {
|
|
1698
|
+
stopTunnel = startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, {
|
|
1699
|
+
onUserFileEdit: handleUserFileEdit,
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1574
1702
|
const shutdownRuntime = async () => {
|
|
1575
1703
|
sessionActive = false;
|
|
1576
1704
|
stopInbound();
|
|
@@ -1641,6 +1769,8 @@ export async function runChimphands(opts) {
|
|
|
1641
1769
|
tick();
|
|
1642
1770
|
});
|
|
1643
1771
|
while (prompt) {
|
|
1772
|
+
cancelTurnRequested = false;
|
|
1773
|
+
agentTurnInProgress = true;
|
|
1644
1774
|
let useOpencodeSessionId = opencodeSessionId;
|
|
1645
1775
|
let isNewOpencodeSession = !useOpencodeSessionId;
|
|
1646
1776
|
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl);
|
|
@@ -1658,6 +1788,7 @@ export async function runChimphands(opts) {
|
|
|
1658
1788
|
},
|
|
1659
1789
|
onWorkingBranch: noteWorkingBranch,
|
|
1660
1790
|
postEvent: turnPostEvent,
|
|
1791
|
+
...turnControl,
|
|
1661
1792
|
}, attachUrl);
|
|
1662
1793
|
if (result.code !== 0 &&
|
|
1663
1794
|
useOpencodeSessionId &&
|
|
@@ -1676,8 +1807,10 @@ export async function runChimphands(opts) {
|
|
|
1676
1807
|
},
|
|
1677
1808
|
onWorkingBranch: noteWorkingBranch,
|
|
1678
1809
|
postEvent: turnPostEvent,
|
|
1810
|
+
...turnControl,
|
|
1679
1811
|
}, attachUrl);
|
|
1680
1812
|
}
|
|
1813
|
+
agentTurnInProgress = false;
|
|
1681
1814
|
await poster.flush();
|
|
1682
1815
|
if (result.opencodeSessionId) {
|
|
1683
1816
|
opencodeSessionId = result.opencodeSessionId;
|
|
@@ -1700,7 +1833,7 @@ export async function runChimphands(opts) {
|
|
|
1700
1833
|
console.error(`ChimpHands turn-end reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1701
1834
|
}
|
|
1702
1835
|
}
|
|
1703
|
-
if (result.code !== 0) {
|
|
1836
|
+
if (result.code !== 0 && !result.cancelled) {
|
|
1704
1837
|
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
1705
1838
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
1706
1839
|
try {
|
|
@@ -1724,7 +1857,12 @@ export async function runChimphands(opts) {
|
|
|
1724
1857
|
exitCode = result.code || 1;
|
|
1725
1858
|
break;
|
|
1726
1859
|
}
|
|
1727
|
-
|
|
1860
|
+
if (result.cancelled) {
|
|
1861
|
+
postEvent(ROLE_STATUS, "Turn stopped", { status: STATUS_WAITING_USER });
|
|
1862
|
+
}
|
|
1863
|
+
else {
|
|
1864
|
+
postEvent(ROLE_STATUS, "Waiting for user input", { status: STATUS_WAITING_USER });
|
|
1865
|
+
}
|
|
1728
1866
|
lastUserActivity = Date.now();
|
|
1729
1867
|
idle = false;
|
|
1730
1868
|
prompt = (await waitForNextPrompt()) || "";
|
|
@@ -1840,7 +1978,7 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
1840
1978
|
console.error(`ChimpHands commit-before-idle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1841
1979
|
}
|
|
1842
1980
|
}
|
|
1843
|
-
function startTunnelWorker(backend, apiKey, runtimeId, attachUrl) {
|
|
1981
|
+
function startTunnelWorker(backend, apiKey, runtimeId, attachUrl, sessionId, handlers) {
|
|
1844
1982
|
let stopped = false;
|
|
1845
1983
|
const base = attachUrl.replace(/\/$/, "");
|
|
1846
1984
|
let ws = null;
|
|
@@ -1971,6 +2109,21 @@ function startTunnelWorker(backend, apiKey, runtimeId, attachUrl) {
|
|
|
1971
2109
|
socket.send(JSON.stringify({ type: "pong" }));
|
|
1972
2110
|
return;
|
|
1973
2111
|
}
|
|
2112
|
+
if (frame.type === "inbound_event" && frame.event === "user_file_edit" && frame.data) {
|
|
2113
|
+
const edit = frame.data;
|
|
2114
|
+
if (edit.sessionId && edit.sessionId !== sessionId) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const requestId = edit.requestId || edit.request_id;
|
|
2118
|
+
if (requestId && edit.path) {
|
|
2119
|
+
handlers?.onUserFileEdit?.({
|
|
2120
|
+
requestId,
|
|
2121
|
+
path: edit.path,
|
|
2122
|
+
content: edit.content ?? "",
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
1974
2127
|
if (frame.type === "http_request" || frame.requestId) {
|
|
1975
2128
|
void handleHttpRequest(socket, frame);
|
|
1976
2129
|
}
|
package/package.json
CHANGED