@groupchatai/claude-runner 0.4.6 → 0.4.7
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/index.js +75 -111
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -154,8 +154,7 @@ function pidTag(pid) {
|
|
|
154
154
|
return ` ${C.pid}[pid ${pid}]${C.reset}`;
|
|
155
155
|
}
|
|
156
156
|
function padForTag(pid) {
|
|
157
|
-
|
|
158
|
-
return " ".repeat(tagLen);
|
|
157
|
+
return " ".repeat(8 + String(pid).length);
|
|
159
158
|
}
|
|
160
159
|
function wrapLines(tag, pad, text, color) {
|
|
161
160
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
@@ -277,13 +276,16 @@ function clampUserFacingDetail(text, maxChars) {
|
|
|
277
276
|
function stripAnsi(text) {
|
|
278
277
|
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
279
278
|
}
|
|
279
|
+
function errMsg(err) {
|
|
280
|
+
return err instanceof Error ? err.message : String(err);
|
|
281
|
+
}
|
|
282
|
+
function normalizeEpochMs(raw) {
|
|
283
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
284
|
+
return raw > 1e10 ? raw : raw * 1e3;
|
|
285
|
+
}
|
|
280
286
|
function formatAnthropicRateLimitPayload(o) {
|
|
281
287
|
const rateLimitTypeRaw = typeof o.rateLimitType === "string" ? o.rateLimitType.replace(/_/g, " ") : "unknown";
|
|
282
|
-
const
|
|
283
|
-
let resetMs = null;
|
|
284
|
-
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
285
|
-
resetMs = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
286
|
-
}
|
|
288
|
+
const resetMs = normalizeEpochMs(o.resetsAt);
|
|
287
289
|
const status = typeof o.status === "string" ? o.status : null;
|
|
288
290
|
const overageReason = typeof o.overageDisabledReason === "string" ? o.overageDisabledReason : null;
|
|
289
291
|
const parts = [`Rate limit (${rateLimitTypeRaw})`];
|
|
@@ -304,9 +306,8 @@ function safeFormatRateLimitPayload(o) {
|
|
|
304
306
|
return formatAnthropicRateLimitPayload(o);
|
|
305
307
|
} catch {
|
|
306
308
|
const rt = typeof o.rateLimitType === "string" ? o.rateLimitType : "unknown";
|
|
307
|
-
const
|
|
308
|
-
if (
|
|
309
|
-
const ms = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
309
|
+
const ms = normalizeEpochMs(o.resetsAt);
|
|
310
|
+
if (ms !== null) {
|
|
310
311
|
return `Rate limit (${rt}) \u2014 resets at ${new Date(ms).toISOString()} UTC`;
|
|
311
312
|
}
|
|
312
313
|
return `Rate limit (${rt})`;
|
|
@@ -330,8 +331,7 @@ function parseClaudeNdjsonEvents(rawOutput) {
|
|
|
330
331
|
function isRateLimitEventBlocking(ev) {
|
|
331
332
|
const info = ev.rate_limit_info;
|
|
332
333
|
if (!info || typeof info !== "object" || Array.isArray(info)) return true;
|
|
333
|
-
|
|
334
|
-
return !(typeof status === "string" && status === "allowed");
|
|
334
|
+
return info.status !== "allowed";
|
|
335
335
|
}
|
|
336
336
|
function extractRateLimitRetryInfo(events) {
|
|
337
337
|
for (const ev of events) {
|
|
@@ -339,9 +339,8 @@ function extractRateLimitRetryInfo(events) {
|
|
|
339
339
|
if (!isRateLimitEventBlocking(ev)) continue;
|
|
340
340
|
const info = ev.rate_limit_info;
|
|
341
341
|
if (!info || typeof info !== "object" || Array.isArray(info)) continue;
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
const ms = raw > 1e10 ? raw : raw * 1e3;
|
|
342
|
+
const ms = normalizeEpochMs(info.resetsAt);
|
|
343
|
+
if (ms === null) continue;
|
|
345
344
|
return { errorType: "rate_limit", retryAfterMs: ms };
|
|
346
345
|
}
|
|
347
346
|
return null;
|
|
@@ -435,31 +434,32 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
|
|
|
435
434
|
const match = combined.match(GITHUB_PR_URL_RE);
|
|
436
435
|
if (match) capturedPrUrl = match[match.length - 1];
|
|
437
436
|
}
|
|
437
|
+
function processLine(trimmed) {
|
|
438
|
+
try {
|
|
439
|
+
const event = JSON.parse(trimmed);
|
|
440
|
+
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
441
|
+
capturedSessionId = event.session_id;
|
|
442
|
+
}
|
|
443
|
+
if (event.type === "result") lastResultJson = trimmed;
|
|
444
|
+
checkEventForPrUrl(event);
|
|
445
|
+
if (config.verbose) {
|
|
446
|
+
const formatted = formatStreamEvent(event, pid);
|
|
447
|
+
if (formatted) console.log(formatted);
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
if (config.verbose) {
|
|
451
|
+
console.log(`${pidTag(pid)} ${C.dim}${trimmed}${C.reset}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
438
455
|
child.stdout?.on("data", (data) => {
|
|
439
456
|
chunks.push(data);
|
|
440
|
-
|
|
441
|
-
lineBuf += text;
|
|
457
|
+
lineBuf += data.toString("utf-8");
|
|
442
458
|
const lines = lineBuf.split("\n");
|
|
443
459
|
lineBuf = lines.pop() ?? "";
|
|
444
460
|
for (const line of lines) {
|
|
445
461
|
const trimmed = line.trim();
|
|
446
|
-
if (
|
|
447
|
-
try {
|
|
448
|
-
const event = JSON.parse(trimmed);
|
|
449
|
-
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
450
|
-
capturedSessionId = event.session_id;
|
|
451
|
-
}
|
|
452
|
-
if (event.type === "result") lastResultJson = trimmed;
|
|
453
|
-
checkEventForPrUrl(event);
|
|
454
|
-
if (config.verbose) {
|
|
455
|
-
const formatted = formatStreamEvent(event, pid);
|
|
456
|
-
if (formatted) console.log(formatted);
|
|
457
|
-
}
|
|
458
|
-
} catch {
|
|
459
|
-
if (config.verbose) {
|
|
460
|
-
console.log(`${pidTag(pid)} ${C.dim}${trimmed}${C.reset}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
462
|
+
if (trimmed) processLine(trimmed);
|
|
463
463
|
}
|
|
464
464
|
});
|
|
465
465
|
child.stderr?.on("data", (data) => {
|
|
@@ -471,24 +471,8 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
|
|
|
471
471
|
});
|
|
472
472
|
child.on("error", (err) => reject(new Error(`Failed to spawn claude: ${err.message}`)));
|
|
473
473
|
child.on("close", (code) => {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const event = JSON.parse(lineBuf.trim());
|
|
477
|
-
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
478
|
-
capturedSessionId = event.session_id;
|
|
479
|
-
}
|
|
480
|
-
if (event.type === "result") lastResultJson = lineBuf.trim();
|
|
481
|
-
checkEventForPrUrl(event);
|
|
482
|
-
if (config.verbose) {
|
|
483
|
-
const formatted = formatStreamEvent(event, pid);
|
|
484
|
-
if (formatted) console.log(formatted);
|
|
485
|
-
}
|
|
486
|
-
} catch {
|
|
487
|
-
if (config.verbose) {
|
|
488
|
-
console.log(`${pidTag(pid)} ${C.dim}${lineBuf.trim()}${C.reset}`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
474
|
+
const remaining = lineBuf.trim();
|
|
475
|
+
if (remaining) processLine(remaining);
|
|
492
476
|
const rawOutput = Buffer.concat(chunks).toString("utf-8");
|
|
493
477
|
const stdout = config.verbose ? lastResultJson || rawOutput : rawOutput;
|
|
494
478
|
const stderr = Buffer.concat(errChunks).toString("utf-8");
|
|
@@ -558,10 +542,9 @@ function extractPullRequestUrlFromText(text) {
|
|
|
558
542
|
return void 0;
|
|
559
543
|
}
|
|
560
544
|
function extractPullRequestUrlFromOutput(stdout) {
|
|
561
|
-
const
|
|
562
|
-
if (
|
|
563
|
-
const
|
|
564
|
-
const found = extractPullRequestUrlFromText(text);
|
|
545
|
+
const resultText = extractResultText(stdout);
|
|
546
|
+
if (resultText !== stdout) {
|
|
547
|
+
const found = extractPullRequestUrlFromText(resultText);
|
|
565
548
|
if (found) return found;
|
|
566
549
|
}
|
|
567
550
|
return extractPullRequestUrlFromText(stdout);
|
|
@@ -753,7 +736,7 @@ async function removeWorktree(workDir, info) {
|
|
|
753
736
|
}
|
|
754
737
|
return true;
|
|
755
738
|
} catch (err) {
|
|
756
|
-
console.error(` Failed to remove ${info.name}: ${err
|
|
739
|
+
console.error(` Failed to remove ${info.name}: ${errMsg(err)}`);
|
|
757
740
|
return false;
|
|
758
741
|
}
|
|
759
742
|
}
|
|
@@ -931,7 +914,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
931
914
|
try {
|
|
932
915
|
await client.startRun(run.id, startMsg);
|
|
933
916
|
} catch (err) {
|
|
934
|
-
const msg =
|
|
917
|
+
const msg = errMsg(err);
|
|
935
918
|
if (msg.includes("not pending") || msg.includes("not PENDING") || msg.includes("400")) {
|
|
936
919
|
log(`\u23ED Run was already claimed, skipping.`);
|
|
937
920
|
return;
|
|
@@ -940,11 +923,13 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
940
923
|
}
|
|
941
924
|
log("\u25B6 Run started");
|
|
942
925
|
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
943
|
-
let
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
926
|
+
let lastClaude = {
|
|
927
|
+
stderr: "",
|
|
928
|
+
stdout: "",
|
|
929
|
+
rawOutput: "",
|
|
930
|
+
exitCode: null,
|
|
931
|
+
sessionId: void 0
|
|
932
|
+
};
|
|
948
933
|
try {
|
|
949
934
|
if (!worktreeDir && runOptions.branch) {
|
|
950
935
|
log(`\u{1F33F} Checking out branch: ${runOptions.branch}`);
|
|
@@ -974,28 +959,14 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
974
959
|
);
|
|
975
960
|
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
976
961
|
let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
|
|
977
|
-
|
|
978
|
-
lastClaudeStdout = stdout;
|
|
979
|
-
lastClaudeRawOutput = rawOutput;
|
|
980
|
-
lastClaudeExitCode = exitCode;
|
|
981
|
-
lastClaudeSessionId = sessionId;
|
|
962
|
+
lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
|
|
982
963
|
if (exitCode !== 0 && isFollowUp) {
|
|
983
964
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
984
965
|
sessionCache.delete(run.taskId);
|
|
985
966
|
const retry = spawnClaudeCode(prompt, config, runOptions, void 0, effectiveCwd);
|
|
986
967
|
log(`\u{1F916} Claude Code spawned (pid ${retry.process.pid}) (fresh)`);
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
rawOutput = retryResult.rawOutput;
|
|
990
|
-
stderr = retryResult.stderr;
|
|
991
|
-
exitCode = retryResult.exitCode;
|
|
992
|
-
sessionId = retryResult.sessionId;
|
|
993
|
-
streamPrUrl = retryResult.streamPrUrl;
|
|
994
|
-
lastClaudeStderr = stderr;
|
|
995
|
-
lastClaudeStdout = stdout;
|
|
996
|
-
lastClaudeRawOutput = rawOutput;
|
|
997
|
-
lastClaudeExitCode = exitCode;
|
|
998
|
-
lastClaudeSessionId = sessionId;
|
|
968
|
+
({ stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await retry.output);
|
|
969
|
+
lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
|
|
999
970
|
}
|
|
1000
971
|
if (sessionId) {
|
|
1001
972
|
sessionCache.set(run.taskId, sessionId);
|
|
@@ -1068,11 +1039,11 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
1068
1039
|
if (pullRequestUrl) logGreen(`\u{1F517} PR: ${pullRequestUrl}`);
|
|
1069
1040
|
logGreen(`\u2705 Run completed`);
|
|
1070
1041
|
} catch (err) {
|
|
1071
|
-
const message =
|
|
1042
|
+
const message = errMsg(err);
|
|
1072
1043
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
1073
|
-
const stderrDbg = truncateClaudeDebugString(stripAnsi(
|
|
1074
|
-
const stdoutDbg = truncateClaudeDebugString(stripAnsi(
|
|
1075
|
-
const rawDbg = truncateClaudeDebugString(stripAnsi(
|
|
1044
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(lastClaude.stderr));
|
|
1045
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(lastClaude.stdout));
|
|
1046
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(lastClaude.rawOutput));
|
|
1076
1047
|
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1077
1048
|
logClaudeRawFailureJson({
|
|
1078
1049
|
runId: run.id,
|
|
@@ -1080,8 +1051,8 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
1080
1051
|
taskTitle: run.taskTitle,
|
|
1081
1052
|
thrownMessage: message,
|
|
1082
1053
|
thrownStack: stack ?? null,
|
|
1083
|
-
exitCode:
|
|
1084
|
-
sessionId:
|
|
1054
|
+
exitCode: lastClaude.exitCode,
|
|
1055
|
+
sessionId: lastClaude.sessionId ?? null,
|
|
1085
1056
|
note: "Thrown before or after Claude finished; fields may be empty.",
|
|
1086
1057
|
stderr: stderrDbg.text,
|
|
1087
1058
|
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
@@ -1185,6 +1156,7 @@ function parseArgs() {
|
|
|
1185
1156
|
workDir: process.cwd(),
|
|
1186
1157
|
pollInterval: 3e4,
|
|
1187
1158
|
maxConcurrent: 5,
|
|
1159
|
+
command: "run",
|
|
1188
1160
|
poll: false,
|
|
1189
1161
|
dryRun: false,
|
|
1190
1162
|
once: false,
|
|
@@ -1234,20 +1206,23 @@ function parseArgs() {
|
|
|
1234
1206
|
config.useWorktrees = false;
|
|
1235
1207
|
break;
|
|
1236
1208
|
case "cleanup":
|
|
1209
|
+
config.command = "cleanup";
|
|
1237
1210
|
break;
|
|
1238
1211
|
default:
|
|
1239
1212
|
console.error(`Unknown argument: ${arg}`);
|
|
1240
1213
|
process.exit(1);
|
|
1241
1214
|
}
|
|
1242
1215
|
}
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1216
|
+
if (config.command !== "cleanup") {
|
|
1217
|
+
if (!config.token) {
|
|
1218
|
+
console.error("Error: No agent token found.");
|
|
1219
|
+
console.error(" Add GCA_TOKEN=gca_... to .env.local, or pass --token gca_...");
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
if (!config.token.startsWith("gca_")) {
|
|
1223
|
+
console.error(`Error: Token must start with gca_ (got "${config.token.slice(0, 8)}\u2026")`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1251
1226
|
}
|
|
1252
1227
|
return config;
|
|
1253
1228
|
}
|
|
@@ -1448,7 +1423,7 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1448
1423
|
const pending = await client.listPendingRuns();
|
|
1449
1424
|
handlePendingRuns(pending, scheduler, client, config);
|
|
1450
1425
|
} catch (err) {
|
|
1451
|
-
console.error(`Poll error: ${err
|
|
1426
|
+
console.error(`Poll error: ${errMsg(err)}`);
|
|
1452
1427
|
}
|
|
1453
1428
|
while (!scheduler.isEmpty()) await sleep(1e3);
|
|
1454
1429
|
return;
|
|
@@ -1458,7 +1433,7 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1458
1433
|
const pending = await client.listPendingRuns();
|
|
1459
1434
|
handlePendingRuns(pending, scheduler, client, config);
|
|
1460
1435
|
} catch (err) {
|
|
1461
|
-
const msg =
|
|
1436
|
+
const msg = errMsg(err);
|
|
1462
1437
|
if (config.verbose || !msg.includes("fetch")) {
|
|
1463
1438
|
console.error(`Poll error: ${msg}`);
|
|
1464
1439
|
}
|
|
@@ -1466,29 +1441,18 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1466
1441
|
await sleep(config.pollInterval);
|
|
1467
1442
|
}
|
|
1468
1443
|
}
|
|
1469
|
-
function wantsVersionOnly(argv) {
|
|
1470
|
-
return argv.some((a) => a === "--version" || a === "-v" || a === "-version");
|
|
1471
|
-
}
|
|
1472
1444
|
async function main() {
|
|
1473
|
-
const
|
|
1474
|
-
if (
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1477
|
-
if (process.argv.includes("cleanup")) {
|
|
1478
|
-
loadEnvFile();
|
|
1479
|
-
const workDir = process.cwd();
|
|
1480
|
-
const workDirIdx = process.argv.indexOf("--work-dir");
|
|
1481
|
-
const resolvedDir = workDirIdx >= 0 ? path.resolve(process.argv[workDirIdx + 1] ?? ".") : workDir;
|
|
1482
|
-
await interactiveCleanup(resolvedDir);
|
|
1445
|
+
const config = parseArgs();
|
|
1446
|
+
if (config.command === "cleanup") {
|
|
1447
|
+
await interactiveCleanup(config.workDir);
|
|
1483
1448
|
return;
|
|
1484
1449
|
}
|
|
1485
|
-
const config = parseArgs();
|
|
1486
1450
|
const client = new GroupChatAgentClient(config.apiUrl, config.token);
|
|
1487
1451
|
let me;
|
|
1488
1452
|
try {
|
|
1489
1453
|
me = await client.getMe();
|
|
1490
1454
|
} catch (err) {
|
|
1491
|
-
console.error("Failed to authenticate:", err
|
|
1455
|
+
console.error("Failed to authenticate:", errMsg(err));
|
|
1492
1456
|
process.exit(1);
|
|
1493
1457
|
}
|
|
1494
1458
|
console.log(`
|