@groupchatai/claude-runner 0.4.3 → 0.4.5
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 +408 -21
- 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
|
[
|
|
@@ -240,6 +259,190 @@ var ALLOWED_TOOLS = [
|
|
|
240
259
|
"TodoWrite",
|
|
241
260
|
"NotebookEdit"
|
|
242
261
|
];
|
|
262
|
+
function clampUserFacingDetail(text, maxChars) {
|
|
263
|
+
const t = text.trim();
|
|
264
|
+
if (t.length <= maxChars) return t;
|
|
265
|
+
return `${t.slice(0, maxChars - 1)}\u2026`;
|
|
266
|
+
}
|
|
267
|
+
function stripAnsi(text) {
|
|
268
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
269
|
+
}
|
|
270
|
+
function errorMessageFromUnknown(err) {
|
|
271
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
272
|
+
if (err && typeof err === "object") {
|
|
273
|
+
const o = err;
|
|
274
|
+
if (typeof o.message === "string" && o.message.trim()) return o.message.trim();
|
|
275
|
+
if (typeof o.error === "string" && o.error.trim()) return o.error.trim();
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
function formatAnthropicRateLimitPayload(o) {
|
|
280
|
+
const rateLimitTypeRaw = typeof o.rateLimitType === "string" ? o.rateLimitType.replace(/_/g, " ") : "unknown";
|
|
281
|
+
const rawReset = o.resetsAt;
|
|
282
|
+
let resetMs = null;
|
|
283
|
+
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
284
|
+
resetMs = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
285
|
+
}
|
|
286
|
+
const status = typeof o.status === "string" ? o.status : null;
|
|
287
|
+
const overageReason = typeof o.overageDisabledReason === "string" ? o.overageDisabledReason : null;
|
|
288
|
+
const parts = [`Rate limit (${rateLimitTypeRaw})`];
|
|
289
|
+
if (status) parts.push(`status ${status}`);
|
|
290
|
+
if (resetMs !== null) {
|
|
291
|
+
const when = new Date(resetMs);
|
|
292
|
+
const local = when.toLocaleString(void 0, {
|
|
293
|
+
dateStyle: "medium",
|
|
294
|
+
timeStyle: "short"
|
|
295
|
+
});
|
|
296
|
+
parts.push(`resets at ${local} (${when.toISOString()} UTC)`);
|
|
297
|
+
}
|
|
298
|
+
if (overageReason) parts.push(`overage: ${overageReason}`);
|
|
299
|
+
return parts.join(" \u2014 ");
|
|
300
|
+
}
|
|
301
|
+
function safeFormatRateLimitPayload(o) {
|
|
302
|
+
try {
|
|
303
|
+
return formatAnthropicRateLimitPayload(o);
|
|
304
|
+
} catch {
|
|
305
|
+
const rt = typeof o.rateLimitType === "string" ? o.rateLimitType : "unknown";
|
|
306
|
+
const rawReset = o.resetsAt;
|
|
307
|
+
if (typeof rawReset === "number" && Number.isFinite(rawReset)) {
|
|
308
|
+
const ms = rawReset > 1e10 ? rawReset : rawReset * 1e3;
|
|
309
|
+
return `Rate limit (${rt}) \u2014 resets at ${new Date(ms).toISOString()} UTC`;
|
|
310
|
+
}
|
|
311
|
+
return `Rate limit (${rt})`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function parseClaudeNdjsonEvents(rawOutput) {
|
|
315
|
+
const events = [];
|
|
316
|
+
for (const line of stripAnsi(rawOutput).split("\n")) {
|
|
317
|
+
const t = line.trim();
|
|
318
|
+
if (!t.startsWith("{")) continue;
|
|
319
|
+
try {
|
|
320
|
+
const ev = JSON.parse(t);
|
|
321
|
+
if (ev && typeof ev === "object" && typeof ev.type === "string") {
|
|
322
|
+
events.push(ev);
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return events;
|
|
328
|
+
}
|
|
329
|
+
function extractAssistantUserVisibleText(event) {
|
|
330
|
+
const msg = event.message;
|
|
331
|
+
if (!msg || typeof msg !== "object" || Array.isArray(msg)) return null;
|
|
332
|
+
const content = msg.content;
|
|
333
|
+
if (!Array.isArray(content)) return null;
|
|
334
|
+
const parts = [];
|
|
335
|
+
for (const block of content) {
|
|
336
|
+
if (!block || typeof block !== "object") continue;
|
|
337
|
+
const b = block;
|
|
338
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
339
|
+
parts.push(b.text.trim());
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (parts.length === 0) return null;
|
|
343
|
+
return parts.join("\n");
|
|
344
|
+
}
|
|
345
|
+
var USAGE_LIMIT_RE = /you['\u2019]?ve hit your limit/i;
|
|
346
|
+
function extractRateLimitRetryInfo(events) {
|
|
347
|
+
for (const ev of events) {
|
|
348
|
+
if (ev.type !== "rate_limit_event") continue;
|
|
349
|
+
const info = ev.rate_limit_info;
|
|
350
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) continue;
|
|
351
|
+
const raw = info.resetsAt;
|
|
352
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) continue;
|
|
353
|
+
const ms = raw > 1e10 ? raw : raw * 1e3;
|
|
354
|
+
return { errorType: "rate_limit", retryAfterMs: ms };
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
function isClaudeUsageLimitText(text) {
|
|
359
|
+
return USAGE_LIMIT_RE.test(text);
|
|
360
|
+
}
|
|
361
|
+
function extractPlainTextCliErrorFromOutput(rawOutput) {
|
|
362
|
+
const limitMatch = rawOutput.match(new RegExp(USAGE_LIMIT_RE.source + "[^\\n\\r]*", "gi"));
|
|
363
|
+
if (limitMatch?.[0]) return limitMatch[0].trim();
|
|
364
|
+
for (const line of rawOutput.split("\n")) {
|
|
365
|
+
const t = line.trim();
|
|
366
|
+
if (!t || t.startsWith("{")) continue;
|
|
367
|
+
if (/^API error:/i.test(t) || /^Error:/i.test(t) || /^Anthropic/i.test(t)) {
|
|
368
|
+
return t.length <= 2e3 ? t : `${t.slice(0, 1999)}\u2026`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
function deriveClaudeFailureSummary(exitCode, rawOutput, stderr, events) {
|
|
374
|
+
const stderrText = stripAnsi(stderr).trim();
|
|
375
|
+
if (stderrText.length > 0) {
|
|
376
|
+
return clampUserFacingDetail(stderrText, 2e3);
|
|
377
|
+
}
|
|
378
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
379
|
+
const ev = events[i];
|
|
380
|
+
if (ev.type !== "result") continue;
|
|
381
|
+
if (typeof ev.error === "string" && ev.error.trim()) {
|
|
382
|
+
return clampUserFacingDetail(ev.error.trim(), 2e3);
|
|
383
|
+
}
|
|
384
|
+
const nested = errorMessageFromUnknown(ev.error);
|
|
385
|
+
if (nested) return clampUserFacingDetail(nested, 2e3);
|
|
386
|
+
if (ev.is_error === true && typeof ev.result === "string" && ev.result.trim()) {
|
|
387
|
+
return clampUserFacingDetail(ev.result.trim(), 2e3);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
391
|
+
const ev = events[i];
|
|
392
|
+
if (ev.type !== "error") continue;
|
|
393
|
+
if (typeof ev.message === "string" && ev.message.trim()) {
|
|
394
|
+
return clampUserFacingDetail(ev.message.trim(), 2e3);
|
|
395
|
+
}
|
|
396
|
+
if (typeof ev.error === "string" && ev.error.trim()) {
|
|
397
|
+
return clampUserFacingDetail(ev.error.trim(), 2e3);
|
|
398
|
+
}
|
|
399
|
+
const nested = errorMessageFromUnknown(ev.error);
|
|
400
|
+
if (nested) return clampUserFacingDetail(nested, 2e3);
|
|
401
|
+
}
|
|
402
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
403
|
+
const ev = events[i];
|
|
404
|
+
if (ev.type !== "assistant") continue;
|
|
405
|
+
if (ev.error === void 0 || ev.error === null || ev.error === "") continue;
|
|
406
|
+
const assistantText = extractAssistantUserVisibleText(ev);
|
|
407
|
+
if (assistantText) return clampUserFacingDetail(assistantText, 2e3);
|
|
408
|
+
}
|
|
409
|
+
for (const ev of events) {
|
|
410
|
+
if (ev.type === "rate_limit_event") {
|
|
411
|
+
const info = ev.rate_limit_info;
|
|
412
|
+
if (info && typeof info === "object" && !Array.isArray(info)) {
|
|
413
|
+
return clampUserFacingDetail(
|
|
414
|
+
safeFormatRateLimitPayload(info),
|
|
415
|
+
2e3
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const combined = stripAnsi(`${stderr}
|
|
421
|
+
${rawOutput}`);
|
|
422
|
+
const plain = extractPlainTextCliErrorFromOutput(combined);
|
|
423
|
+
if (plain) return clampUserFacingDetail(plain, 2e3);
|
|
424
|
+
return `Claude Code ended with exit code ${exitCode}. No detailed error was returned; if this persists, run the runner with verbose logging or try again later.`;
|
|
425
|
+
}
|
|
426
|
+
function claudeRunShouldReportAsError(options) {
|
|
427
|
+
if (options.exitCode !== 0) return true;
|
|
428
|
+
for (const ev of options.events) {
|
|
429
|
+
if (ev.type === "rate_limit_event") return true;
|
|
430
|
+
if (ev.type === "assistant" && ev.error !== void 0 && ev.error !== null && ev.error !== "") {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (ev.type === "result" && ev.is_error === true) return true;
|
|
434
|
+
}
|
|
435
|
+
if (isClaudeUsageLimitText(options.resultSummaryText)) return true;
|
|
436
|
+
if (isClaudeUsageLimitText(options.combinedText)) return true;
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
function compactTaskErrorForApi(detail) {
|
|
440
|
+
const s = detail.trim();
|
|
441
|
+
if (/^rate limit \(/i.test(s)) {
|
|
442
|
+
return s.split(/\s*—\s*/)[0]?.trim() ?? s;
|
|
443
|
+
}
|
|
444
|
+
return s;
|
|
445
|
+
}
|
|
243
446
|
function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverride) {
|
|
244
447
|
const format = config.verbose ? "stream-json" : "json";
|
|
245
448
|
const args = [];
|
|
@@ -350,6 +553,7 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
|
|
|
350
553
|
resolve({
|
|
351
554
|
stdout,
|
|
352
555
|
rawOutput,
|
|
556
|
+
stderr,
|
|
353
557
|
exitCode: code ?? 0,
|
|
354
558
|
sessionId: capturedSessionId,
|
|
355
559
|
streamPrUrl: capturedPrUrl
|
|
@@ -445,6 +649,12 @@ function runShellCommand(cmd, args, cwd) {
|
|
|
445
649
|
}
|
|
446
650
|
var sessionCache = /* @__PURE__ */ new Map();
|
|
447
651
|
var runCounter = 0;
|
|
652
|
+
var claudeRunnerSignalTeardownDone = false;
|
|
653
|
+
function tryBeginClaudeRunnerSignalTeardown() {
|
|
654
|
+
if (claudeRunnerSignalTeardownDone) return false;
|
|
655
|
+
claudeRunnerSignalTeardownDone = true;
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
448
658
|
var WORKTREE_DIR = ".agent-worktrees";
|
|
449
659
|
var WORKTREE_PREFIX = "task-";
|
|
450
660
|
function worktreeNameForTask(taskId) {
|
|
@@ -682,6 +892,47 @@ Removing ${safe.length} safe worktree(s)\u2026`);
|
|
|
682
892
|
}
|
|
683
893
|
console.log();
|
|
684
894
|
}
|
|
895
|
+
function logClaudeRunFailureDiagnostics(log, stderr, rawOutput) {
|
|
896
|
+
const se = stripAnsi(stderr).trimEnd();
|
|
897
|
+
const so = stripAnsi(rawOutput);
|
|
898
|
+
log(`${C.dim}\u2500\u2500 Claude failure diagnostics (--verbose) \u2500\u2500${C.reset}`);
|
|
899
|
+
if (se.length > 0) {
|
|
900
|
+
log(`${C.dim}stderr:${C.reset}`);
|
|
901
|
+
for (const ln of se.split("\n")) {
|
|
902
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
log(`${C.dim}(stderr empty)${C.reset}`);
|
|
906
|
+
}
|
|
907
|
+
const lines = so.length > 0 ? so.split("\n") : [];
|
|
908
|
+
const tailLines = lines.length > 200 ? lines.slice(-200) : lines;
|
|
909
|
+
log(`${C.dim}stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
|
|
910
|
+
for (const ln of tailLines) {
|
|
911
|
+
log(`${C.dim} ${ln}${C.reset}`);
|
|
912
|
+
}
|
|
913
|
+
log(`${C.dim}\u2500\u2500 end diagnostics \u2500\u2500${C.reset}`);
|
|
914
|
+
}
|
|
915
|
+
var CLAUDE_DEBUG_JSON_MAX_CHARS = 1e6;
|
|
916
|
+
function logClaudeRawFailureJson(payload) {
|
|
917
|
+
const stamped = {
|
|
918
|
+
_tag: "claude-runner-claude-raw-failure",
|
|
919
|
+
_capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
920
|
+
...payload
|
|
921
|
+
};
|
|
922
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
923
|
+
}
|
|
924
|
+
function truncateClaudeDebugString(text) {
|
|
925
|
+
const fullLength = text.length;
|
|
926
|
+
if (fullLength <= CLAUDE_DEBUG_JSON_MAX_CHARS) {
|
|
927
|
+
return { text, fullLength, truncated: false };
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
text: `${text.slice(0, CLAUDE_DEBUG_JSON_MAX_CHARS)}
|
|
931
|
+
\u2026 [truncated; copy from logs or raise CLAUDE_DEBUG_JSON_MAX_CHARS]`,
|
|
932
|
+
fullLength,
|
|
933
|
+
truncated: true
|
|
934
|
+
};
|
|
935
|
+
}
|
|
685
936
|
async function processRun(client, run, config, worktreeDir, detail) {
|
|
686
937
|
const runNum = ++runCounter;
|
|
687
938
|
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
@@ -716,6 +967,11 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
716
967
|
}
|
|
717
968
|
log("\u25B6 Run started");
|
|
718
969
|
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
970
|
+
let lastClaudeStderr = "";
|
|
971
|
+
let lastClaudeStdout = "";
|
|
972
|
+
let lastClaudeRawOutput = "";
|
|
973
|
+
let lastClaudeExitCode = null;
|
|
974
|
+
let lastClaudeSessionId;
|
|
719
975
|
try {
|
|
720
976
|
if (!worktreeDir && runOptions.branch) {
|
|
721
977
|
log(`\u{1F33F} Checking out branch: ${runOptions.branch}`);
|
|
@@ -744,7 +1000,12 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
744
1000
|
effectiveCwd
|
|
745
1001
|
);
|
|
746
1002
|
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
747
|
-
let { stdout, rawOutput, exitCode, sessionId, streamPrUrl } = await output;
|
|
1003
|
+
let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
|
|
1004
|
+
lastClaudeStderr = stderr;
|
|
1005
|
+
lastClaudeStdout = stdout;
|
|
1006
|
+
lastClaudeRawOutput = rawOutput;
|
|
1007
|
+
lastClaudeExitCode = exitCode;
|
|
1008
|
+
lastClaudeSessionId = sessionId;
|
|
748
1009
|
if (exitCode !== 0 && isFollowUp) {
|
|
749
1010
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
750
1011
|
sessionCache.delete(run.taskId);
|
|
@@ -753,21 +1014,81 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
753
1014
|
const retryResult = await retry.output;
|
|
754
1015
|
stdout = retryResult.stdout;
|
|
755
1016
|
rawOutput = retryResult.rawOutput;
|
|
1017
|
+
stderr = retryResult.stderr;
|
|
756
1018
|
exitCode = retryResult.exitCode;
|
|
757
1019
|
sessionId = retryResult.sessionId;
|
|
758
1020
|
streamPrUrl = retryResult.streamPrUrl;
|
|
1021
|
+
lastClaudeStderr = stderr;
|
|
1022
|
+
lastClaudeStdout = stdout;
|
|
1023
|
+
lastClaudeRawOutput = rawOutput;
|
|
1024
|
+
lastClaudeExitCode = exitCode;
|
|
1025
|
+
lastClaudeSessionId = sessionId;
|
|
759
1026
|
}
|
|
760
1027
|
if (sessionId) {
|
|
761
1028
|
sessionCache.set(run.taskId, sessionId);
|
|
762
1029
|
}
|
|
763
1030
|
const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1031
|
+
const streamEvents = parseClaudeNdjsonEvents(rawOutput);
|
|
1032
|
+
const combinedForLimit = stripAnsi(`${stderr}
|
|
1033
|
+
${rawOutput}`);
|
|
1034
|
+
const resultSummaryText = extractResultText(stdout);
|
|
1035
|
+
const usageCap = claudeRunShouldReportAsError({
|
|
1036
|
+
exitCode,
|
|
1037
|
+
events: streamEvents,
|
|
1038
|
+
combinedText: combinedForLimit,
|
|
1039
|
+
resultSummaryText
|
|
1040
|
+
});
|
|
1041
|
+
if (exitCode !== 0 || usageCap) {
|
|
1042
|
+
const detail2 = deriveClaudeFailureSummary(exitCode, rawOutput, stderr, streamEvents);
|
|
1043
|
+
const errorMsg = compactTaskErrorForApi(detail2);
|
|
1044
|
+
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1045
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(stderr));
|
|
1046
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(stdout));
|
|
1047
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(rawOutput));
|
|
1048
|
+
logClaudeRawFailureJson({
|
|
1049
|
+
runId: run.id,
|
|
1050
|
+
taskId: run.taskId,
|
|
1051
|
+
taskTitle: run.taskTitle,
|
|
1052
|
+
exitCode,
|
|
1053
|
+
usageCap,
|
|
1054
|
+
sessionId: sessionId ?? null,
|
|
1055
|
+
streamPrUrl: streamPrUrl ?? null,
|
|
1056
|
+
pullRequestUrl: pullRequestUrl ?? null,
|
|
1057
|
+
formattedDetail: detail2,
|
|
1058
|
+
taskErrorMessageSentToApi: errorMsg,
|
|
1059
|
+
parsedEventTypes: streamEvents.map((e) => e.type),
|
|
1060
|
+
stderr: stderrDbg.text,
|
|
1061
|
+
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
1062
|
+
stdout: stdoutDbg.text,
|
|
1063
|
+
stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
|
|
1064
|
+
rawOutput: rawDbg.text,
|
|
1065
|
+
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated },
|
|
1066
|
+
resultTextFromExtract: resultSummaryText || null
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const retryInfo = extractRateLimitRetryInfo(streamEvents);
|
|
1070
|
+
await client.errorRun(run.id, errorMsg, {
|
|
1071
|
+
pullRequestUrl,
|
|
1072
|
+
errorType: retryInfo?.errorType,
|
|
1073
|
+
retryAfter: retryInfo?.retryAfterMs
|
|
1074
|
+
});
|
|
1075
|
+
if (retryInfo) {
|
|
1076
|
+
const retryAt = new Date(retryInfo.retryAfterMs).toLocaleString(void 0, {
|
|
1077
|
+
dateStyle: "medium",
|
|
1078
|
+
timeStyle: "short"
|
|
1079
|
+
});
|
|
1080
|
+
log(`${C.red}\u23F8 Rate limited \u2014 server will retry at ${retryAt}${C.reset}`);
|
|
1081
|
+
} else {
|
|
1082
|
+
log(
|
|
1083
|
+
`${C.red}\u274C Run errored${exitCode !== 0 ? ` (exit code ${exitCode})` : " (usage limit)"}${C.reset}`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
for (const line of detail2.split("\n")) {
|
|
1087
|
+
log(`${C.red} ${line}${C.reset}`);
|
|
1088
|
+
}
|
|
1089
|
+
if (config.verbose) {
|
|
1090
|
+
logClaudeRunFailureDiagnostics(log, stderr, rawOutput);
|
|
1091
|
+
}
|
|
771
1092
|
return;
|
|
772
1093
|
}
|
|
773
1094
|
const resultText = extractResultText(stdout);
|
|
@@ -777,6 +1098,28 @@ ${stdout.slice(0, 2e3)}
|
|
|
777
1098
|
logGreen(`\u2705 Run completed`);
|
|
778
1099
|
} catch (err) {
|
|
779
1100
|
const message = err instanceof Error ? err.message : String(err);
|
|
1101
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
1102
|
+
const stderrDbg = truncateClaudeDebugString(stripAnsi(lastClaudeStderr));
|
|
1103
|
+
const stdoutDbg = truncateClaudeDebugString(stripAnsi(lastClaudeStdout));
|
|
1104
|
+
const rawDbg = truncateClaudeDebugString(stripAnsi(lastClaudeRawOutput));
|
|
1105
|
+
if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
|
|
1106
|
+
logClaudeRawFailureJson({
|
|
1107
|
+
runId: run.id,
|
|
1108
|
+
taskId: run.taskId,
|
|
1109
|
+
taskTitle: run.taskTitle,
|
|
1110
|
+
thrownMessage: message,
|
|
1111
|
+
thrownStack: stack ?? null,
|
|
1112
|
+
exitCode: lastClaudeExitCode,
|
|
1113
|
+
sessionId: lastClaudeSessionId ?? null,
|
|
1114
|
+
note: "Thrown before or after Claude finished; fields may be empty.",
|
|
1115
|
+
stderr: stderrDbg.text,
|
|
1116
|
+
stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
|
|
1117
|
+
stdout: stdoutDbg.text,
|
|
1118
|
+
stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
|
|
1119
|
+
rawOutput: rawDbg.text,
|
|
1120
|
+
rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
780
1123
|
const errorBody = `Claude Code runner error:
|
|
781
1124
|
\`\`\`
|
|
782
1125
|
${message.slice(0, 2e3)}
|
|
@@ -813,6 +1156,25 @@ function loadEnvFile() {
|
|
|
813
1156
|
}
|
|
814
1157
|
}
|
|
815
1158
|
}
|
|
1159
|
+
function getRunnerPackageInfo() {
|
|
1160
|
+
try {
|
|
1161
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1162
|
+
const pkgPath = path.join(here, "..", "package.json");
|
|
1163
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
1164
|
+
const pkg = JSON.parse(raw);
|
|
1165
|
+
return {
|
|
1166
|
+
name: pkg.name ?? "@groupchatai/claude-runner",
|
|
1167
|
+
version: pkg.version ?? "unknown"
|
|
1168
|
+
};
|
|
1169
|
+
} catch {
|
|
1170
|
+
return { name: "@groupchatai/claude-runner", version: "unknown" };
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function showVersion() {
|
|
1174
|
+
const { name, version } = getRunnerPackageInfo();
|
|
1175
|
+
console.log(`${name} ${version}`);
|
|
1176
|
+
process.exit(0);
|
|
1177
|
+
}
|
|
816
1178
|
function showHelp() {
|
|
817
1179
|
console.log(`
|
|
818
1180
|
Usage: npx @groupchatai/claude-runner [command] [options]
|
|
@@ -833,6 +1195,7 @@ Options:
|
|
|
833
1195
|
--token <token> Agent token (or set GCA_TOKEN env var)
|
|
834
1196
|
--no-worktree Disable git worktree isolation for runs
|
|
835
1197
|
-h, --help Show this help message
|
|
1198
|
+
-v, -version, --version Print version and exit
|
|
836
1199
|
|
|
837
1200
|
Environment variables:
|
|
838
1201
|
GCA_TOKEN Agent token (gca_...)
|
|
@@ -891,6 +1254,11 @@ function parseArgs() {
|
|
|
891
1254
|
case "-h":
|
|
892
1255
|
showHelp();
|
|
893
1256
|
break;
|
|
1257
|
+
case "--version":
|
|
1258
|
+
case "-v":
|
|
1259
|
+
case "-version":
|
|
1260
|
+
showVersion();
|
|
1261
|
+
break;
|
|
894
1262
|
case "--no-worktree":
|
|
895
1263
|
config.useWorktrees = false;
|
|
896
1264
|
break;
|
|
@@ -1067,13 +1435,22 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
1067
1435
|
}
|
|
1068
1436
|
);
|
|
1069
1437
|
await new Promise((resolve) => {
|
|
1070
|
-
|
|
1438
|
+
function shutdown() {
|
|
1439
|
+
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
1440
|
+
process.removeListener("SIGINT", shutdown);
|
|
1441
|
+
process.removeListener("SIGTERM", shutdown);
|
|
1071
1442
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|
|
1072
|
-
void
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1443
|
+
void (async () => {
|
|
1444
|
+
try {
|
|
1445
|
+
await convex.close();
|
|
1446
|
+
} catch {
|
|
1447
|
+
} finally {
|
|
1448
|
+
resolve();
|
|
1449
|
+
}
|
|
1450
|
+
})();
|
|
1451
|
+
}
|
|
1452
|
+
process.once("SIGINT", shutdown);
|
|
1453
|
+
process.once("SIGTERM", shutdown);
|
|
1077
1454
|
});
|
|
1078
1455
|
}
|
|
1079
1456
|
async function runWithPolling(client, config, scheduler) {
|
|
@@ -1086,12 +1463,15 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1086
1463
|
`
|
|
1087
1464
|
);
|
|
1088
1465
|
let running = true;
|
|
1089
|
-
|
|
1466
|
+
function shutdownPoll() {
|
|
1467
|
+
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
1468
|
+
process.removeListener("SIGINT", shutdownPoll);
|
|
1469
|
+
process.removeListener("SIGTERM", shutdownPoll);
|
|
1090
1470
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|
|
1091
1471
|
running = false;
|
|
1092
|
-
}
|
|
1093
|
-
process.
|
|
1094
|
-
process.
|
|
1472
|
+
}
|
|
1473
|
+
process.once("SIGINT", shutdownPoll);
|
|
1474
|
+
process.once("SIGTERM", shutdownPoll);
|
|
1095
1475
|
if (config.once) {
|
|
1096
1476
|
try {
|
|
1097
1477
|
const pending = await client.listPendingRuns();
|
|
@@ -1115,7 +1495,14 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1115
1495
|
await sleep(config.pollInterval);
|
|
1116
1496
|
}
|
|
1117
1497
|
}
|
|
1498
|
+
function wantsVersionOnly(argv) {
|
|
1499
|
+
return argv.some((a) => a === "--version" || a === "-v" || a === "-version");
|
|
1500
|
+
}
|
|
1118
1501
|
async function main() {
|
|
1502
|
+
const argv = process.argv.slice(2);
|
|
1503
|
+
if (wantsVersionOnly(argv)) {
|
|
1504
|
+
showVersion();
|
|
1505
|
+
}
|
|
1119
1506
|
if (process.argv.includes("cleanup")) {
|
|
1120
1507
|
loadEnvFile();
|
|
1121
1508
|
const workDir = process.cwd();
|