@groupchatai/claude-runner 0.4.3 → 0.4.6
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 +391 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
import { spawn, execFileSync } from "child_process";
|
|
5
5
|
import { readFileSync, readdirSync, statSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
7
8
|
var API_URL = "https://groupchat.ai";
|
|
8
9
|
var CONVEX_URL = "https://fantastic-jay-464.convex.cloud";
|
|
10
|
+
function sanitizeAgentJsonPayload(payload) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
13
|
+
if (value === void 0 || value === null) continue;
|
|
14
|
+
if (typeof value === "number" && !Number.isFinite(value)) continue;
|
|
15
|
+
out[key] = value;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
9
19
|
var GroupChatAgentClient = class {
|
|
10
20
|
baseUrl;
|
|
11
21
|
token;
|
|
@@ -15,13 +25,14 @@ var GroupChatAgentClient = class {
|
|
|
15
25
|
}
|
|
16
26
|
async request(method, endpoint, body) {
|
|
17
27
|
const url = `${this.baseUrl}${endpoint}`;
|
|
28
|
+
const jsonBody = body ? JSON.stringify(sanitizeAgentJsonPayload(body)) : void 0;
|
|
18
29
|
const res = await fetch(url, {
|
|
19
30
|
method,
|
|
20
31
|
headers: {
|
|
21
32
|
Authorization: `Bearer ${this.token}`,
|
|
22
33
|
"Content-Type": "application/json"
|
|
23
34
|
},
|
|
24
|
-
body:
|
|
35
|
+
body: jsonBody,
|
|
25
36
|
redirect: "follow"
|
|
26
37
|
});
|
|
27
38
|
if (res.status === 401 && res.redirected) {
|
|
@@ -31,7 +42,7 @@ var GroupChatAgentClient = class {
|
|
|
31
42
|
Authorization: `Bearer ${this.token}`,
|
|
32
43
|
"Content-Type": "application/json"
|
|
33
44
|
},
|
|
34
|
-
body:
|
|
45
|
+
body: jsonBody
|
|
35
46
|
});
|
|
36
47
|
if (!retry.ok) {
|
|
37
48
|
const text = await retry.text().catch(() => "");
|
|
@@ -104,8 +115,16 @@ ${comment.body}`
|
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
if (detail.task.dueDate) {
|
|
118
|
+
let dueStr = new Date(detail.task.dueDate).toLocaleDateString();
|
|
119
|
+
if (detail.task.dueTime != null) {
|
|
120
|
+
const h = Math.floor(detail.task.dueTime / 60);
|
|
121
|
+
const m = detail.task.dueTime % 60;
|
|
122
|
+
const period = h >= 12 ? "PM" : "AM";
|
|
123
|
+
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
124
|
+
dueStr += ` at ${h12}:${String(m).padStart(2, "0")} ${period}`;
|
|
125
|
+
}
|
|
107
126
|
parts.push(`
|
|
108
|
-
Due: ${
|
|
127
|
+
Due: ${dueStr}`);
|
|
109
128
|
}
|
|
110
129
|
parts.push(
|
|
111
130
|
[
|
|
@@ -184,6 +203,16 @@ function formatStreamEvent(event, pid) {
|
|
|
184
203
|
}
|
|
185
204
|
case "tool_result":
|
|
186
205
|
return null;
|
|
206
|
+
case "rate_limit_event": {
|
|
207
|
+
const info = event.rate_limit_info;
|
|
208
|
+
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
209
|
+
const payload = safeFormatRateLimitPayload(info);
|
|
210
|
+
const blocking = isRateLimitEventBlocking(event);
|
|
211
|
+
const icon = blocking ? "\u{1F6AB}" : "\u2139\uFE0F ";
|
|
212
|
+
return `${tag} ${C.dim}${icon} ${payload}${blocking ? "" : " (non-blocking)"}${C.reset}`;
|
|
213
|
+
}
|
|
214
|
+
return `${tag} ${C.dim}rate_limit_event (no info payload)${C.reset}`;
|
|
215
|
+
}
|
|
187
216
|
case "result": {
|
|
188
217
|
const lines = [];
|
|
189
218
|
if (event.cost_usd !== void 0 || event.total_cost_usd !== void 0) {
|
|
@@ -240,6 +269,125 @@ var ALLOWED_TOOLS = [
|
|
|
240
269
|
"TodoWrite",
|
|
241
270
|
"NotebookEdit"
|
|
242
271
|
];
|
|
272
|
+
function clampUserFacingDetail(text, maxChars) {
|
|
273
|
+
const t = text.trim();
|
|
274
|
+
if (t.length <= maxChars) return t;
|
|
275
|
+
return `${t.slice(0, maxChars - 1)}\u2026`;
|
|
276
|
+
}
|
|
277
|
+
function stripAnsi(text) {
|
|
278
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
279
|
+
}
|
|
280
|
+
function formatAnthropicRateLimitPayload(o) {
|
|
281
|
+
const rateLimitTypeRaw = typeof o.rateLimitType === "string" ? o.rateLimitType.replace(/_/g, " ") : "unknown";
|
|
282
|
+
const rawReset = o.resetsAt;
|
|
283
|
+
let resetMs = null;
|
|
284
|
+
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
285
|
+
resetMs = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
286
|
+
}
|
|
287
|
+
const status = typeof o.status === "string" ? o.status : null;
|
|
288
|
+
const overageReason = typeof o.overageDisabledReason === "string" ? o.overageDisabledReason : null;
|
|
289
|
+
const parts = [`Rate limit (${rateLimitTypeRaw})`];
|
|
290
|
+
if (status) parts.push(`status ${status}`);
|
|
291
|
+
if (resetMs !== null) {
|
|
292
|
+
const when = new Date(resetMs);
|
|
293
|
+
const local = when.toLocaleString(void 0, {
|
|
294
|
+
dateStyle: "medium",
|
|
295
|
+
timeStyle: "short"
|
|
296
|
+
});
|
|
297
|
+
parts.push(`resets at ${local} (${when.toISOString()} UTC)`);
|
|
298
|
+
}
|
|
299
|
+
if (overageReason) parts.push(`overage: ${overageReason}`);
|
|
300
|
+
return parts.join(" \u2014 ");
|
|
301
|
+
}
|
|
302
|
+
function safeFormatRateLimitPayload(o) {
|
|
303
|
+
try {
|
|
304
|
+
return formatAnthropicRateLimitPayload(o);
|
|
305
|
+
} catch {
|
|
306
|
+
const rt = typeof o.rateLimitType === "string" ? o.rateLimitType : "unknown";
|
|
307
|
+
const rawReset = o.resetsAt;
|
|
308
|
+
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
309
|
+
const ms = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
310
|
+
return `Rate limit (${rt}) \u2014 resets at ${new Date(ms).toISOString()} UTC`;
|
|
311
|
+
}
|
|
312
|
+
return `Rate limit (${rt})`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function parseClaudeNdjsonEvents(rawOutput) {
|
|
316
|
+
const events = [];
|
|
317
|
+
for (const line of stripAnsi(rawOutput).split("\n")) {
|
|
318
|
+
const t = line.trim();
|
|
319
|
+
if (!t.startsWith("{")) continue;
|
|
320
|
+
try {
|
|
321
|
+
const ev = JSON.parse(t);
|
|
322
|
+
if (ev && typeof ev === "object" && typeof ev.type === "string") {
|
|
323
|
+
events.push(ev);
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return events;
|
|
329
|
+
}
|
|
330
|
+
function isRateLimitEventBlocking(ev) {
|
|
331
|
+
const info = ev.rate_limit_info;
|
|
332
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) return true;
|
|
333
|
+
const status = info.status;
|
|
334
|
+
return !(typeof status === "string" && status === "allowed");
|
|
335
|
+
}
|
|
336
|
+
function extractRateLimitRetryInfo(events) {
|
|
337
|
+
for (const ev of events) {
|
|
338
|
+
if (ev.type !== "rate_limit_event") continue;
|
|
339
|
+
if (!isRateLimitEventBlocking(ev)) continue;
|
|
340
|
+
const info = ev.rate_limit_info;
|
|
341
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) continue;
|
|
342
|
+
const raw = info.resetsAt;
|
|
343
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) continue;
|
|
344
|
+
const ms = raw > 1e10 ? raw : raw * 1e3;
|
|
345
|
+
return { errorType: "rate_limit", retryAfterMs: ms };
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
function deriveClaudeFailureSummary(exitCode, stderr, events) {
|
|
350
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
351
|
+
const ev = events[i];
|
|
352
|
+
if (ev.type !== "result") continue;
|
|
353
|
+
if (ev.is_error === true && typeof ev.result === "string" && ev.result.trim()) {
|
|
354
|
+
return clampUserFacingDetail(ev.result.trim(), 2e3);
|
|
355
|
+
}
|
|
356
|
+
if (typeof ev.error === "string" && ev.error.trim()) {
|
|
357
|
+
return clampUserFacingDetail(ev.error.trim(), 2e3);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const stderrText = stripAnsi(stderr).trim();
|
|
361
|
+
if (stderrText.length > 0) {
|
|
362
|
+
return clampUserFacingDetail(stderrText, 2e3);
|
|
363
|
+
}
|
|
364
|
+
for (const ev of events) {
|
|
365
|
+
if (ev.type === "rate_limit_event" && isRateLimitEventBlocking(ev)) {
|
|
366
|
+
const info = ev.rate_limit_info;
|
|
367
|
+
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
368
|
+
return clampUserFacingDetail(
|
|
369
|
+
safeFormatRateLimitPayload(info),
|
|
370
|
+
2e3
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return `Claude Code ended with exit code ${exitCode}`;
|
|
376
|
+
}
|
|
377
|
+
function claudeRunShouldReportAsError(exitCode, events) {
|
|
378
|
+
if (exitCode !== 0) return true;
|
|
379
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
380
|
+
if (events[i].type === "result" && events[i].is_error === true) return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
function compactTaskErrorForApi(detail) {
|
|
385
|
+
const s = detail.trim();
|
|
386
|
+
if (/^rate limit \(/i.test(s)) {
|
|
387
|
+
return s.split(/\s*—\s*/)[0]?.trim() ?? s;
|
|
388
|
+
}
|
|
389
|
+
return s;
|
|
390
|
+
}
|
|
243
391
|
function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverride) {
|
|
244
392
|
const format = config.verbose ? "stream-json" : "json";
|
|
245
393
|
const args = [];
|
|
@@ -350,6 +498,7 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
|
|
|
350
498
|
resolve({
|
|
351
499
|
stdout,
|
|
352
500
|
rawOutput,
|
|
501
|
+
stderr,
|
|
353
502
|
exitCode: code ?? 0,
|
|
354
503
|
sessionId: capturedSessionId,
|
|
355
504
|
streamPrUrl: capturedPrUrl
|
|
@@ -445,6 +594,12 @@ function runShellCommand(cmd, args, cwd) {
|
|
|
445
594
|
}
|
|
446
595
|
var sessionCache = /* @__PURE__ */ new Map();
|
|
447
596
|
var runCounter = 0;
|
|
597
|
+
var claudeRunnerSignalTeardownDone = false;
|
|
598
|
+
function tryBeginClaudeRunnerSignalTeardown() {
|
|
599
|
+
if (claudeRunnerSignalTeardownDone) return false;
|
|
600
|
+
claudeRunnerSignalTeardownDone = true;
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
448
603
|
var WORKTREE_DIR = ".agent-worktrees";
|
|
449
604
|
var WORKTREE_PREFIX = "task-";
|
|
450
605
|
function worktreeNameForTask(taskId) {
|
|
@@ -682,6 +837,75 @@ Removing ${safe.length} safe worktree(s)\u2026`);
|
|
|
682
837
|
}
|
|
683
838
|
console.log();
|
|
684
839
|
}
|
|
840
|
+
function logClaudeRunFailureDiagnostics(log, opts) {
|
|
841
|
+
const se = stripAnsi(opts.stderr).trimEnd();
|
|
842
|
+
const so = stripAnsi(opts.rawOutput);
|
|
843
|
+
log(`${C.dim}\u2500\u2500 Claude failure diagnostics (--verbose) \u2500\u2500${C.reset}`);
|
|
844
|
+
log(`${C.dim} exit code: ${opts.exitCode}${C.reset}`);
|
|
845
|
+
log(`${C.dim} session: ${opts.sessionId ?? "(none)"}${C.reset}`);
|
|
846
|
+
log(`${C.dim} derived error: ${opts.derivedError}${C.reset}`);
|
|
847
|
+
log(`${C.dim} api error: ${opts.apiError}${C.reset}`);
|
|
848
|
+
const rateLimitEvents = opts.streamEvents.filter((e) => e.type === "rate_limit_event");
|
|
849
|
+
if (rateLimitEvents.length > 0) {
|
|
850
|
+
log(`${C.dim} rate_limit_event(s): ${rateLimitEvents.length}${C.reset}`);
|
|
851
|
+
for (const rl of rateLimitEvents) {
|
|
852
|
+
const info = rl.rate_limit_info;
|
|
853
|
+
const blocking = isRateLimitEventBlocking(rl);
|
|
854
|
+
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
855
|
+
log(`${C.dim} blocking=${blocking} payload=${JSON.stringify(info)}${C.reset}`);
|
|
856
|
+
} else {
|
|
857
|
+
log(`${C.dim} blocking=${blocking} (no rate_limit_info)${C.reset}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const typeCounts = /* @__PURE__ */ new Map();
|
|
862
|
+
for (const ev of opts.streamEvents) {
|
|
863
|
+
const t = typeof ev.type === "string" ? ev.type : "?";
|
|
864
|
+
typeCounts.set(t, (typeCounts.get(t) ?? 0) + 1);
|
|
865
|
+
}
|
|
866
|
+
if (typeCounts.size > 0) {
|
|
867
|
+
const timeline = Array.from(typeCounts.entries()).map(([t, c]) => c > 1 ? `${t}\xD7${c}` : t).join(", ");
|
|
868
|
+
log(`${C.dim} events: ${opts.streamEvents.length} total (${timeline})${C.reset}`);
|
|
869
|
+
} else {
|
|
870
|
+
log(`${C.dim} events: 0 parsed${C.reset}`);
|
|
871
|
+
}
|
|
872
|
+
if (se.length > 0) {
|
|
873
|
+
log(`${C.dim} stderr:${C.reset}`);
|
|
874
|
+
for (const ln of se.split("\n")) {
|
|
875
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
log(`${C.dim} stderr: (empty)${C.reset}`);
|
|
879
|
+
}
|
|
880
|
+
const lines = so.length > 0 ? so.split("\n") : [];
|
|
881
|
+
const tailLines = lines.length > 200 ? lines.slice(-200) : lines;
|
|
882
|
+
log(`${C.dim} stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
|
|
883
|
+
for (const ln of tailLines) {
|
|
884
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
885
|
+
}
|
|
886
|
+
log(`${C.dim}\u2500\u2500 end diagnostics \u2500\u2500${C.reset}`);
|
|
887
|
+
}
|
|
888
|
+
var CLAUDE_DEBUG_JSON_MAX_CHARS = 1e6;
|
|
889
|
+
function logClaudeRawFailureJson(payload) {
|
|
890
|
+
const stamped = {
|
|
891
|
+
_tag: "claude-runner-claude-raw-failure",
|
|
892
|
+
_capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
893
|
+
...payload
|
|
894
|
+
};
|
|
895
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
896
|
+
}
|
|
897
|
+
function truncateClaudeDebugString(text) {
|
|
898
|
+
const fullLength = text.length;
|
|
899
|
+
if (fullLength <= CLAUDE_DEBUG_JSON_MAX_CHARS) {
|
|
900
|
+
return { text, fullLength, truncated: false };
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
text: `${text.slice(0, CLAUDE_DEBUG_JSON_MAX_CHARS)}
|
|
904
|
+
\u2026 [truncated; copy from logs or raise CLAUDE_DEBUG_JSON_MAX_CHARS]`,
|
|
905
|
+
fullLength,
|
|
906
|
+
truncated: true
|
|
907
|
+
};
|
|
908
|
+
}
|
|
685
909
|
async function processRun(client, run, config, worktreeDir, detail) {
|
|
686
910
|
const runNum = ++runCounter;
|
|
687
911
|
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
@@ -716,6 +940,11 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
716
940
|
}
|
|
717
941
|
log("\u25B6 Run started");
|
|
718
942
|
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
943
|
+
let lastClaudeStderr = "";
|
|
944
|
+
let lastClaudeStdout = "";
|
|
945
|
+
let lastClaudeRawOutput = "";
|
|
946
|
+
let lastClaudeExitCode = null;
|
|
947
|
+
let lastClaudeSessionId;
|
|
719
948
|
try {
|
|
720
949
|
if (!worktreeDir && runOptions.branch) {
|
|
721
950
|
log(`\u{1F33F} Checking out branch: ${runOptions.branch}`);
|
|
@@ -744,7 +973,12 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
744
973
|
effectiveCwd
|
|
745
974
|
);
|
|
746
975
|
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
747
|
-
let { stdout, rawOutput, exitCode, sessionId, streamPrUrl } = await output;
|
|
976
|
+
let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
|
|
977
|
+
lastClaudeStderr = stderr;
|
|
978
|
+
lastClaudeStdout = stdout;
|
|
979
|
+
lastClaudeRawOutput = rawOutput;
|
|
980
|
+
lastClaudeExitCode = exitCode;
|
|
981
|
+
lastClaudeSessionId = sessionId;
|
|
748
982
|
if (exitCode !== 0 && isFollowUp) {
|
|
749
983
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
750
984
|
sessionCache.delete(run.taskId);
|
|
@@ -753,21 +987,79 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
753
987
|
const retryResult = await retry.output;
|
|
754
988
|
stdout = retryResult.stdout;
|
|
755
989
|
rawOutput = retryResult.rawOutput;
|
|
990
|
+
stderr = retryResult.stderr;
|
|
756
991
|
exitCode = retryResult.exitCode;
|
|
757
992
|
sessionId = retryResult.sessionId;
|
|
758
993
|
streamPrUrl = retryResult.streamPrUrl;
|
|
994
|
+
lastClaudeStderr = stderr;
|
|
995
|
+
lastClaudeStdout = stdout;
|
|
996
|
+
lastClaudeRawOutput = rawOutput;
|
|
997
|
+
lastClaudeExitCode = exitCode;
|
|
998
|
+
lastClaudeSessionId = sessionId;
|
|
759
999
|
}
|
|
760
1000
|
if (sessionId) {
|
|
761
1001
|
sessionCache.set(run.taskId, sessionId);
|
|
762
1002
|
}
|
|
763
1003
|
const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1004
|
+
const streamEvents = parseClaudeNdjsonEvents(rawOutput);
|
|
1005
|
+
const isError = claudeRunShouldReportAsError(exitCode, streamEvents);
|
|
1006
|
+
if (isError) {
|
|
1007
|
+
const detail2 = deriveClaudeFailureSummary(exitCode, stderr, streamEvents);
|
|
1008
|
+
const errorMsg = compactTaskErrorForApi(detail2);
|
|
1009
|
+
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1010
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(stderr));
|
|
1011
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(stdout));
|
|
1012
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(rawOutput));
|
|
1013
|
+
logClaudeRawFailureJson({
|
|
1014
|
+
runId: run.id,
|
|
1015
|
+
taskId: run.taskId,
|
|
1016
|
+
taskTitle: run.taskTitle,
|
|
1017
|
+
exitCode,
|
|
1018
|
+
sessionId: sessionId ?? null,
|
|
1019
|
+
streamPrUrl: streamPrUrl ?? null,
|
|
1020
|
+
pullRequestUrl: pullRequestUrl ?? null,
|
|
1021
|
+
formattedDetail: detail2,
|
|
1022
|
+
taskErrorMessageSentToApi: errorMsg,
|
|
1023
|
+
parsedEventTypes: streamEvents.map((e) => e.type),
|
|
1024
|
+
stderr: stderrDbg.text,
|
|
1025
|
+
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
1026
|
+
stdout: stdoutDbg.text,
|
|
1027
|
+
stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
|
|
1028
|
+
rawOutput: rawDbg.text,
|
|
1029
|
+
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
const retryInfo = extractRateLimitRetryInfo(streamEvents);
|
|
1033
|
+
await client.errorRun(run.id, errorMsg, {
|
|
1034
|
+
pullRequestUrl,
|
|
1035
|
+
errorType: retryInfo?.errorType,
|
|
1036
|
+
retryAfter: retryInfo?.retryAfterMs
|
|
1037
|
+
});
|
|
1038
|
+
if (retryInfo) {
|
|
1039
|
+
const retryAt = new Date(retryInfo.retryAfterMs).toLocaleString(void 0, {
|
|
1040
|
+
dateStyle: "medium",
|
|
1041
|
+
timeStyle: "short"
|
|
1042
|
+
});
|
|
1043
|
+
log(`${C.red}\u23F8 Rate limited \u2014 server will retry at ${retryAt}${C.reset}`);
|
|
1044
|
+
} else {
|
|
1045
|
+
log(
|
|
1046
|
+
`${C.red}\u274C Run errored${exitCode !== 0 ? ` (exit code ${exitCode})` : " (usage limit)"}${C.reset}`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
for (const line of detail2.split("\n")) {
|
|
1050
|
+
log(`${C.red} ${line}${C.reset}`);
|
|
1051
|
+
}
|
|
1052
|
+
if (config.verbose) {
|
|
1053
|
+
logClaudeRunFailureDiagnostics(log, {
|
|
1054
|
+
exitCode,
|
|
1055
|
+
sessionId,
|
|
1056
|
+
stderr,
|
|
1057
|
+
rawOutput,
|
|
1058
|
+
streamEvents,
|
|
1059
|
+
derivedError: detail2,
|
|
1060
|
+
apiError: errorMsg
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
771
1063
|
return;
|
|
772
1064
|
}
|
|
773
1065
|
const resultText = extractResultText(stdout);
|
|
@@ -777,6 +1069,28 @@ ${stdout.slice(0, 2e3)}
|
|
|
777
1069
|
logGreen(`\u2705 Run completed`);
|
|
778
1070
|
} catch (err) {
|
|
779
1071
|
const message = err instanceof Error ? err.message : String(err);
|
|
1072
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
1073
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(lastClaudeStderr));
|
|
1074
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(lastClaudeStdout));
|
|
1075
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(lastClaudeRawOutput));
|
|
1076
|
+
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1077
|
+
logClaudeRawFailureJson({
|
|
1078
|
+
runId: run.id,
|
|
1079
|
+
taskId: run.taskId,
|
|
1080
|
+
taskTitle: run.taskTitle,
|
|
1081
|
+
thrownMessage: message,
|
|
1082
|
+
thrownStack: stack ?? null,
|
|
1083
|
+
exitCode: lastClaudeExitCode,
|
|
1084
|
+
sessionId: lastClaudeSessionId ?? null,
|
|
1085
|
+
note: "Thrown before or after Claude finished; fields may be empty.",
|
|
1086
|
+
stderr: stderrDbg.text,
|
|
1087
|
+
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
1088
|
+
stdout: stdoutDbg.text,
|
|
1089
|
+
stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
|
|
1090
|
+
rawOutput: rawDbg.text,
|
|
1091
|
+
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
780
1094
|
const errorBody = `Claude Code runner error:
|
|
781
1095
|
\`\`\`
|
|
782
1096
|
${message.slice(0, 2e3)}
|
|
@@ -813,6 +1127,25 @@ function loadEnvFile() {
|
|
|
813
1127
|
}
|
|
814
1128
|
}
|
|
815
1129
|
}
|
|
1130
|
+
function getRunnerPackageInfo() {
|
|
1131
|
+
try {
|
|
1132
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1133
|
+
const pkgPath = path.join(here, "..", "package.json");
|
|
1134
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
1135
|
+
const pkg = JSON.parse(raw);
|
|
1136
|
+
return {
|
|
1137
|
+
name: pkg.name ?? "@groupchatai/claude-runner",
|
|
1138
|
+
version: pkg.version ?? "unknown"
|
|
1139
|
+
};
|
|
1140
|
+
} catch {
|
|
1141
|
+
return { name: "@groupchatai/claude-runner", version: "unknown" };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function showVersion() {
|
|
1145
|
+
const { name, version } = getRunnerPackageInfo();
|
|
1146
|
+
console.log(`${name} ${version}`);
|
|
1147
|
+
process.exit(0);
|
|
1148
|
+
}
|
|
816
1149
|
function showHelp() {
|
|
817
1150
|
console.log(`
|
|
818
1151
|
Usage: npx @groupchatai/claude-runner [command] [options]
|
|
@@ -833,6 +1166,7 @@ Options:
|
|
|
833
1166
|
--token <token> Agent token (or set GCA_TOKEN env var)
|
|
834
1167
|
--no-worktree Disable git worktree isolation for runs
|
|
835
1168
|
-h, --help Show this help message
|
|
1169
|
+
-v, -version, --version Print version and exit
|
|
836
1170
|
|
|
837
1171
|
Environment variables:
|
|
838
1172
|
GCA_TOKEN Agent token (gca_...)
|
|
@@ -891,6 +1225,11 @@ function parseArgs() {
|
|
|
891
1225
|
case "-h":
|
|
892
1226
|
showHelp();
|
|
893
1227
|
break;
|
|
1228
|
+
case "--version":
|
|
1229
|
+
case "-v":
|
|
1230
|
+
case "-version":
|
|
1231
|
+
showVersion();
|
|
1232
|
+
break;
|
|
894
1233
|
case "--no-worktree":
|
|
895
1234
|
config.useWorktrees = false;
|
|
896
1235
|
break;
|
|
@@ -1067,13 +1406,22 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
1067
1406
|
}
|
|
1068
1407
|
);
|
|
1069
1408
|
await new Promise((resolve) => {
|
|
1070
|
-
|
|
1409
|
+
function shutdown() {
|
|
1410
|
+
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
1411
|
+
process.removeListener("SIGINT", shutdown);
|
|
1412
|
+
process.removeListener("SIGTERM", shutdown);
|
|
1071
1413
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|
|
1072
|
-
void
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1414
|
+
void (async () => {
|
|
1415
|
+
try {
|
|
1416
|
+
await convex.close();
|
|
1417
|
+
} catch {
|
|
1418
|
+
} finally {
|
|
1419
|
+
resolve();
|
|
1420
|
+
}
|
|
1421
|
+
})();
|
|
1422
|
+
}
|
|
1423
|
+
process.once("SIGINT", shutdown);
|
|
1424
|
+
process.once("SIGTERM", shutdown);
|
|
1077
1425
|
});
|
|
1078
1426
|
}
|
|
1079
1427
|
async function runWithPolling(client, config, scheduler) {
|
|
@@ -1086,12 +1434,15 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1086
1434
|
`
|
|
1087
1435
|
);
|
|
1088
1436
|
let running = true;
|
|
1089
|
-
|
|
1437
|
+
function shutdownPoll() {
|
|
1438
|
+
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
1439
|
+
process.removeListener("SIGINT", shutdownPoll);
|
|
1440
|
+
process.removeListener("SIGTERM", shutdownPoll);
|
|
1090
1441
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|
|
1091
1442
|
running = false;
|
|
1092
|
-
}
|
|
1093
|
-
process.
|
|
1094
|
-
process.
|
|
1443
|
+
}
|
|
1444
|
+
process.once("SIGINT", shutdownPoll);
|
|
1445
|
+
process.once("SIGTERM", shutdownPoll);
|
|
1095
1446
|
if (config.once) {
|
|
1096
1447
|
try {
|
|
1097
1448
|
const pending = await client.listPendingRuns();
|
|
@@ -1115,7 +1466,14 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1115
1466
|
await sleep(config.pollInterval);
|
|
1116
1467
|
}
|
|
1117
1468
|
}
|
|
1469
|
+
function wantsVersionOnly(argv) {
|
|
1470
|
+
return argv.some((a) => a === "--version" || a === "-v" || a === "-version");
|
|
1471
|
+
}
|
|
1118
1472
|
async function main() {
|
|
1473
|
+
const argv = process.argv.slice(2);
|
|
1474
|
+
if (wantsVersionOnly(argv)) {
|
|
1475
|
+
showVersion();
|
|
1476
|
+
}
|
|
1119
1477
|
if (process.argv.includes("cleanup")) {
|
|
1120
1478
|
loadEnvFile();
|
|
1121
1479
|
const workDir = process.cwd();
|
|
@@ -1153,10 +1511,20 @@ async function main() {
|
|
|
1153
1511
|
await runWithWebSocket(client, config, scheduler);
|
|
1154
1512
|
}
|
|
1155
1513
|
if (!scheduler.isEmpty()) {
|
|
1156
|
-
console.log(
|
|
1157
|
-
|
|
1514
|
+
console.log(
|
|
1515
|
+
`\u23F3 Waiting for ${scheduler.activeTaskCount} in-flight task(s)\u2026 (Ctrl+C to force quit)`
|
|
1516
|
+
);
|
|
1517
|
+
let forceQuit = false;
|
|
1518
|
+
const forceHandler = () => {
|
|
1519
|
+
forceQuit = true;
|
|
1520
|
+
};
|
|
1521
|
+
process.once("SIGINT", forceHandler);
|
|
1522
|
+
process.once("SIGTERM", forceHandler);
|
|
1523
|
+
while (!scheduler.isEmpty() && !forceQuit) {
|
|
1158
1524
|
await sleep(1e3);
|
|
1159
1525
|
}
|
|
1526
|
+
process.removeListener("SIGINT", forceHandler);
|
|
1527
|
+
process.removeListener("SIGTERM", forceHandler);
|
|
1160
1528
|
}
|
|
1161
1529
|
console.log("\u{1F44B} Goodbye.");
|
|
1162
1530
|
}
|