@testchimp/cli 0.1.58 → 0.1.59
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 +132 -2
- 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);
|
|
@@ -1498,6 +1594,15 @@ export async function runChimphands(opts) {
|
|
|
1498
1594
|
let sessionActive = true;
|
|
1499
1595
|
let lastUserActivity = Date.now();
|
|
1500
1596
|
let exitCode;
|
|
1597
|
+
let cancelTurnRequested = false;
|
|
1598
|
+
let activeOpencodeChild = null;
|
|
1599
|
+
let agentTurnInProgress = false;
|
|
1600
|
+
const turnControl = {
|
|
1601
|
+
getCancelRequested: () => cancelTurnRequested,
|
|
1602
|
+
onActiveChild: (child) => {
|
|
1603
|
+
activeOpencodeChild = child;
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1501
1606
|
const enqueueUserMessage = (msg) => {
|
|
1502
1607
|
const id = msg.id?.trim();
|
|
1503
1608
|
if (id) {
|
|
@@ -1569,6 +1674,21 @@ export async function runChimphands(opts) {
|
|
|
1569
1674
|
onIdle: () => {
|
|
1570
1675
|
idle = true;
|
|
1571
1676
|
},
|
|
1677
|
+
onCancelTurn: () => {
|
|
1678
|
+
cancelTurnRequested = true;
|
|
1679
|
+
const ch = activeOpencodeChild;
|
|
1680
|
+
if (ch && !ch.killed) {
|
|
1681
|
+
try {
|
|
1682
|
+
ch.kill("SIGTERM");
|
|
1683
|
+
}
|
|
1684
|
+
catch {
|
|
1685
|
+
/* ignore */
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
},
|
|
1689
|
+
onUserFileEdit: (edit) => {
|
|
1690
|
+
void applyUserFileEdit(backend, apiKey, sessionId, edit, () => agentTurnInProgress);
|
|
1691
|
+
},
|
|
1572
1692
|
shouldRun: () => sessionActive,
|
|
1573
1693
|
});
|
|
1574
1694
|
const shutdownRuntime = async () => {
|
|
@@ -1641,6 +1761,8 @@ export async function runChimphands(opts) {
|
|
|
1641
1761
|
tick();
|
|
1642
1762
|
});
|
|
1643
1763
|
while (prompt) {
|
|
1764
|
+
cancelTurnRequested = false;
|
|
1765
|
+
agentTurnInProgress = true;
|
|
1644
1766
|
let useOpencodeSessionId = opencodeSessionId;
|
|
1645
1767
|
let isNewOpencodeSession = !useOpencodeSessionId;
|
|
1646
1768
|
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl);
|
|
@@ -1658,6 +1780,7 @@ export async function runChimphands(opts) {
|
|
|
1658
1780
|
},
|
|
1659
1781
|
onWorkingBranch: noteWorkingBranch,
|
|
1660
1782
|
postEvent: turnPostEvent,
|
|
1783
|
+
...turnControl,
|
|
1661
1784
|
}, attachUrl);
|
|
1662
1785
|
if (result.code !== 0 &&
|
|
1663
1786
|
useOpencodeSessionId &&
|
|
@@ -1676,8 +1799,10 @@ export async function runChimphands(opts) {
|
|
|
1676
1799
|
},
|
|
1677
1800
|
onWorkingBranch: noteWorkingBranch,
|
|
1678
1801
|
postEvent: turnPostEvent,
|
|
1802
|
+
...turnControl,
|
|
1679
1803
|
}, attachUrl);
|
|
1680
1804
|
}
|
|
1805
|
+
agentTurnInProgress = false;
|
|
1681
1806
|
await poster.flush();
|
|
1682
1807
|
if (result.opencodeSessionId) {
|
|
1683
1808
|
opencodeSessionId = result.opencodeSessionId;
|
|
@@ -1700,7 +1825,7 @@ export async function runChimphands(opts) {
|
|
|
1700
1825
|
console.error(`ChimpHands turn-end reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1701
1826
|
}
|
|
1702
1827
|
}
|
|
1703
|
-
if (result.code !== 0) {
|
|
1828
|
+
if (result.code !== 0 && !result.cancelled) {
|
|
1704
1829
|
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
1705
1830
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
1706
1831
|
try {
|
|
@@ -1724,7 +1849,12 @@ export async function runChimphands(opts) {
|
|
|
1724
1849
|
exitCode = result.code || 1;
|
|
1725
1850
|
break;
|
|
1726
1851
|
}
|
|
1727
|
-
|
|
1852
|
+
if (result.cancelled) {
|
|
1853
|
+
postEvent(ROLE_STATUS, "Turn stopped", { status: STATUS_WAITING_USER });
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
postEvent(ROLE_STATUS, "Waiting for user input", { status: STATUS_WAITING_USER });
|
|
1857
|
+
}
|
|
1728
1858
|
lastUserActivity = Date.now();
|
|
1729
1859
|
idle = false;
|
|
1730
1860
|
prompt = (await waitForNextPrompt()) || "";
|
package/package.json
CHANGED