@groupchatai/claude-runner 0.4.5 → 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.
Files changed (2) hide show
  1. package/dist/index.js +159 -214
  2. 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
- const tagLen = ` [pid ${pid}] `.length;
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());
@@ -203,6 +202,16 @@ function formatStreamEvent(event, pid) {
203
202
  }
204
203
  case "tool_result":
205
204
  return null;
205
+ case "rate_limit_event": {
206
+ const info = event.rate_limit_info;
207
+ if (info && typeof info === "object" && !Array.isArray(info)) {
208
+ const payload = safeFormatRateLimitPayload(info);
209
+ const blocking = isRateLimitEventBlocking(event);
210
+ const icon = blocking ? "\u{1F6AB}" : "\u2139\uFE0F ";
211
+ return `${tag} ${C.dim}${icon} ${payload}${blocking ? "" : " (non-blocking)"}${C.reset}`;
212
+ }
213
+ return `${tag} ${C.dim}rate_limit_event (no info payload)${C.reset}`;
214
+ }
206
215
  case "result": {
207
216
  const lines = [];
208
217
  if (event.cost_usd !== void 0 || event.total_cost_usd !== void 0) {
@@ -267,22 +276,16 @@ function clampUserFacingDetail(text, maxChars) {
267
276
  function stripAnsi(text) {
268
277
  return text.replace(/\x1b\[[0-9;]*m/g, "");
269
278
  }
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;
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;
278
285
  }
279
286
  function formatAnthropicRateLimitPayload(o) {
280
287
  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
- }
288
+ const resetMs = normalizeEpochMs(o.resetsAt);
286
289
  const status = typeof o.status === "string" ? o.status : null;
287
290
  const overageReason = typeof o.overageDisabledReason === "string" ? o.overageDisabledReason : null;
288
291
  const parts = [`Rate limit (${rateLimitTypeRaw})`];
@@ -303,9 +306,8 @@ function safeFormatRateLimitPayload(o) {
303
306
  return formatAnthropicRateLimitPayload(o);
304
307
  } catch {
305
308
  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
+ const ms = normalizeEpochMs(o.resetsAt);
310
+ if (ms !== null) {
309
311
  return `Rate limit (${rt}) \u2014 resets at ${new Date(ms).toISOString()} UTC`;
310
312
  }
311
313
  return `Rate limit (${rt})`;
@@ -326,88 +328,40 @@ function parseClaudeNdjsonEvents(rawOutput) {
326
328
  }
327
329
  return events;
328
330
  }
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");
331
+ function isRateLimitEventBlocking(ev) {
332
+ const info = ev.rate_limit_info;
333
+ if (!info || typeof info !== "object" || Array.isArray(info)) return true;
334
+ return info.status !== "allowed";
344
335
  }
345
- var USAGE_LIMIT_RE = /you['\u2019]?ve hit your limit/i;
346
336
  function extractRateLimitRetryInfo(events) {
347
337
  for (const ev of events) {
348
338
  if (ev.type !== "rate_limit_event") continue;
339
+ if (!isRateLimitEventBlocking(ev)) continue;
349
340
  const info = ev.rate_limit_info;
350
341
  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;
342
+ const ms = normalizeEpochMs(info.resetsAt);
343
+ if (ms === null) continue;
354
344
  return { errorType: "rate_limit", retryAfterMs: ms };
355
345
  }
356
346
  return null;
357
347
  }
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
- }
348
+ function deriveClaudeFailureSummary(exitCode, stderr, events) {
378
349
  for (let i = events.length - 1; i >= 0; i--) {
379
350
  const ev = events[i];
380
351
  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
352
  if (ev.is_error === true && typeof ev.result === "string" && ev.result.trim()) {
387
353
  return clampUserFacingDetail(ev.result.trim(), 2e3);
388
354
  }
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
355
  if (typeof ev.error === "string" && ev.error.trim()) {
397
356
  return clampUserFacingDetail(ev.error.trim(), 2e3);
398
357
  }
399
- const nested = errorMessageFromUnknown(ev.error);
400
- if (nested) return clampUserFacingDetail(nested, 2e3);
401
358
  }
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);
359
+ const stderrText = stripAnsi(stderr).trim();
360
+ if (stderrText.length > 0) {
361
+ return clampUserFacingDetail(stderrText, 2e3);
408
362
  }
409
363
  for (const ev of events) {
410
- if (ev.type === "rate_limit_event") {
364
+ if (ev.type === "rate_limit_event" && isRateLimitEventBlocking(ev)) {
411
365
  const info = ev.rate_limit_info;
412
366
  if (info && typeof info === "object" && !Array.isArray(info)) {
413
367
  return clampUserFacingDetail(
@@ -417,23 +371,13 @@ function deriveClaudeFailureSummary(exitCode, rawOutput, stderr, events) {
417
371
  }
418
372
  }
419
373
  }
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.`;
374
+ return `Claude Code ended with exit code ${exitCode}`;
425
375
  }
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;
376
+ function claudeRunShouldReportAsError(exitCode, events) {
377
+ if (exitCode !== 0) return true;
378
+ for (let i = events.length - 1; i >= 0; i--) {
379
+ if (events[i].type === "result" && events[i].is_error === true) return true;
434
380
  }
435
- if (isClaudeUsageLimitText(options.resultSummaryText)) return true;
436
- if (isClaudeUsageLimitText(options.combinedText)) return true;
437
381
  return false;
438
382
  }
439
383
  function compactTaskErrorForApi(detail) {
@@ -490,31 +434,32 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
490
434
  const match = combined.match(GITHUB_PR_URL_RE);
491
435
  if (match) capturedPrUrl = match[match.length - 1];
492
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
+ }
493
455
  child.stdout?.on("data", (data) => {
494
456
  chunks.push(data);
495
- const text = data.toString("utf-8");
496
- lineBuf += text;
457
+ lineBuf += data.toString("utf-8");
497
458
  const lines = lineBuf.split("\n");
498
459
  lineBuf = lines.pop() ?? "";
499
460
  for (const line of lines) {
500
461
  const trimmed = line.trim();
501
- if (!trimmed) continue;
502
- try {
503
- const event = JSON.parse(trimmed);
504
- if (event.type === "system" && event.subtype === "init" && event.session_id) {
505
- capturedSessionId = event.session_id;
506
- }
507
- if (event.type === "result") lastResultJson = trimmed;
508
- checkEventForPrUrl(event);
509
- if (config.verbose) {
510
- const formatted = formatStreamEvent(event, pid);
511
- if (formatted) console.log(formatted);
512
- }
513
- } catch {
514
- if (config.verbose) {
515
- console.log(`${pidTag(pid)} ${C.dim}${trimmed}${C.reset}`);
516
- }
517
- }
462
+ if (trimmed) processLine(trimmed);
518
463
  }
519
464
  });
520
465
  child.stderr?.on("data", (data) => {
@@ -526,24 +471,8 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
526
471
  });
527
472
  child.on("error", (err) => reject(new Error(`Failed to spawn claude: ${err.message}`)));
528
473
  child.on("close", (code) => {
529
- if (lineBuf.trim()) {
530
- try {
531
- const event = JSON.parse(lineBuf.trim());
532
- if (event.type === "system" && event.subtype === "init" && event.session_id) {
533
- capturedSessionId = event.session_id;
534
- }
535
- if (event.type === "result") lastResultJson = lineBuf.trim();
536
- checkEventForPrUrl(event);
537
- if (config.verbose) {
538
- const formatted = formatStreamEvent(event, pid);
539
- if (formatted) console.log(formatted);
540
- }
541
- } catch {
542
- if (config.verbose) {
543
- console.log(`${pidTag(pid)} ${C.dim}${lineBuf.trim()}${C.reset}`);
544
- }
545
- }
546
- }
474
+ const remaining = lineBuf.trim();
475
+ if (remaining) processLine(remaining);
547
476
  const rawOutput = Buffer.concat(chunks).toString("utf-8");
548
477
  const stdout = config.verbose ? lastResultJson || rawOutput : rawOutput;
549
478
  const stderr = Buffer.concat(errChunks).toString("utf-8");
@@ -613,10 +542,9 @@ function extractPullRequestUrlFromText(text) {
613
542
  return void 0;
614
543
  }
615
544
  function extractPullRequestUrlFromOutput(stdout) {
616
- const event = findResultEvent(stdout);
617
- if (event) {
618
- const text = typeof event.result === "string" ? event.result : typeof event.text === "string" ? event.text : "";
619
- const found = extractPullRequestUrlFromText(text);
545
+ const resultText = extractResultText(stdout);
546
+ if (resultText !== stdout) {
547
+ const found = extractPullRequestUrlFromText(resultText);
620
548
  if (found) return found;
621
549
  }
622
550
  return extractPullRequestUrlFromText(stdout);
@@ -808,7 +736,7 @@ async function removeWorktree(workDir, info) {
808
736
  }
809
737
  return true;
810
738
  } catch (err) {
811
- console.error(` Failed to remove ${info.name}: ${err instanceof Error ? err.message : err}`);
739
+ console.error(` Failed to remove ${info.name}: ${errMsg(err)}`);
812
740
  return false;
813
741
  }
814
742
  }
@@ -892,23 +820,51 @@ Removing ${safe.length} safe worktree(s)\u2026`);
892
820
  }
893
821
  console.log();
894
822
  }
895
- function logClaudeRunFailureDiagnostics(log, stderr, rawOutput) {
896
- const se = stripAnsi(stderr).trimEnd();
897
- const so = stripAnsi(rawOutput);
823
+ function logClaudeRunFailureDiagnostics(log, opts) {
824
+ const se = stripAnsi(opts.stderr).trimEnd();
825
+ const so = stripAnsi(opts.rawOutput);
898
826
  log(`${C.dim}\u2500\u2500 Claude failure diagnostics (--verbose) \u2500\u2500${C.reset}`);
827
+ log(`${C.dim} exit code: ${opts.exitCode}${C.reset}`);
828
+ log(`${C.dim} session: ${opts.sessionId ?? "(none)"}${C.reset}`);
829
+ log(`${C.dim} derived error: ${opts.derivedError}${C.reset}`);
830
+ log(`${C.dim} api error: ${opts.apiError}${C.reset}`);
831
+ const rateLimitEvents = opts.streamEvents.filter((e) => e.type === "rate_limit_event");
832
+ if (rateLimitEvents.length > 0) {
833
+ log(`${C.dim} rate_limit_event(s): ${rateLimitEvents.length}${C.reset}`);
834
+ for (const rl of rateLimitEvents) {
835
+ const info = rl.rate_limit_info;
836
+ const blocking = isRateLimitEventBlocking(rl);
837
+ if (info && typeof info === "object" && !Array.isArray(info)) {
838
+ log(`${C.dim} blocking=${blocking} payload=${JSON.stringify(info)}${C.reset}`);
839
+ } else {
840
+ log(`${C.dim} blocking=${blocking} (no rate_limit_info)${C.reset}`);
841
+ }
842
+ }
843
+ }
844
+ const typeCounts = /* @__PURE__ */ new Map();
845
+ for (const ev of opts.streamEvents) {
846
+ const t = typeof ev.type === "string" ? ev.type : "?";
847
+ typeCounts.set(t, (typeCounts.get(t) ?? 0) + 1);
848
+ }
849
+ if (typeCounts.size > 0) {
850
+ const timeline = Array.from(typeCounts.entries()).map(([t, c]) => c > 1 ? `${t}\xD7${c}` : t).join(", ");
851
+ log(`${C.dim} events: ${opts.streamEvents.length} total (${timeline})${C.reset}`);
852
+ } else {
853
+ log(`${C.dim} events: 0 parsed${C.reset}`);
854
+ }
899
855
  if (se.length > 0) {
900
- log(`${C.dim}stderr:${C.reset}`);
856
+ log(`${C.dim} stderr:${C.reset}`);
901
857
  for (const ln of se.split("\n")) {
902
- log(`${C.dim} ${ln}${C.reset}`);
858
+ log(`${C.dim} ${ln}${C.reset}`);
903
859
  }
904
860
  } else {
905
- log(`${C.dim}(stderr empty)${C.reset}`);
861
+ log(`${C.dim} stderr: (empty)${C.reset}`);
906
862
  }
907
863
  const lines = so.length > 0 ? so.split("\n") : [];
908
864
  const tailLines = lines.length > 200 ? lines.slice(-200) : lines;
909
- log(`${C.dim}stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
865
+ log(`${C.dim} stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
910
866
  for (const ln of tailLines) {
911
- log(`${C.dim} ${ln}${C.reset}`);
867
+ log(`${C.dim} ${ln}${C.reset}`);
912
868
  }
913
869
  log(`${C.dim}\u2500\u2500 end diagnostics \u2500\u2500${C.reset}`);
914
870
  }
@@ -958,7 +914,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
958
914
  try {
959
915
  await client.startRun(run.id, startMsg);
960
916
  } catch (err) {
961
- const msg = err instanceof Error ? err.message : String(err);
917
+ const msg = errMsg(err);
962
918
  if (msg.includes("not pending") || msg.includes("not PENDING") || msg.includes("400")) {
963
919
  log(`\u23ED Run was already claimed, skipping.`);
964
920
  return;
@@ -967,11 +923,13 @@ async function processRun(client, run, config, worktreeDir, detail) {
967
923
  }
968
924
  log("\u25B6 Run started");
969
925
  const effectiveCwd = worktreeDir ?? config.workDir;
970
- let lastClaudeStderr = "";
971
- let lastClaudeStdout = "";
972
- let lastClaudeRawOutput = "";
973
- let lastClaudeExitCode = null;
974
- let lastClaudeSessionId;
926
+ let lastClaude = {
927
+ stderr: "",
928
+ stdout: "",
929
+ rawOutput: "",
930
+ exitCode: null,
931
+ sessionId: void 0
932
+ };
975
933
  try {
976
934
  if (!worktreeDir && runOptions.branch) {
977
935
  log(`\u{1F33F} Checking out branch: ${runOptions.branch}`);
@@ -1001,45 +959,23 @@ async function processRun(client, run, config, worktreeDir, detail) {
1001
959
  );
1002
960
  log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
1003
961
  let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
1004
- lastClaudeStderr = stderr;
1005
- lastClaudeStdout = stdout;
1006
- lastClaudeRawOutput = rawOutput;
1007
- lastClaudeExitCode = exitCode;
1008
- lastClaudeSessionId = sessionId;
962
+ lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
1009
963
  if (exitCode !== 0 && isFollowUp) {
1010
964
  log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
1011
965
  sessionCache.delete(run.taskId);
1012
966
  const retry = spawnClaudeCode(prompt, config, runOptions, void 0, effectiveCwd);
1013
967
  log(`\u{1F916} Claude Code spawned (pid ${retry.process.pid}) (fresh)`);
1014
- const retryResult = await retry.output;
1015
- stdout = retryResult.stdout;
1016
- rawOutput = retryResult.rawOutput;
1017
- stderr = retryResult.stderr;
1018
- exitCode = retryResult.exitCode;
1019
- sessionId = retryResult.sessionId;
1020
- streamPrUrl = retryResult.streamPrUrl;
1021
- lastClaudeStderr = stderr;
1022
- lastClaudeStdout = stdout;
1023
- lastClaudeRawOutput = rawOutput;
1024
- lastClaudeExitCode = exitCode;
1025
- lastClaudeSessionId = sessionId;
968
+ ({ stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await retry.output);
969
+ lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
1026
970
  }
1027
971
  if (sessionId) {
1028
972
  sessionCache.set(run.taskId, sessionId);
1029
973
  }
1030
974
  const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
1031
975
  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);
976
+ const isError = claudeRunShouldReportAsError(exitCode, streamEvents);
977
+ if (isError) {
978
+ const detail2 = deriveClaudeFailureSummary(exitCode, stderr, streamEvents);
1043
979
  const errorMsg = compactTaskErrorForApi(detail2);
1044
980
  if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
1045
981
  const stderrDbg = truncateClaudeDebugString(stripAnsi(stderr));
@@ -1050,7 +986,6 @@ ${rawOutput}`);
1050
986
  taskId: run.taskId,
1051
987
  taskTitle: run.taskTitle,
1052
988
  exitCode,
1053
- usageCap,
1054
989
  sessionId: sessionId ?? null,
1055
990
  streamPrUrl: streamPrUrl ?? null,
1056
991
  pullRequestUrl: pullRequestUrl ?? null,
@@ -1062,8 +997,7 @@ ${rawOutput}`);
1062
997
  stdout: stdoutDbg.text,
1063
998
  stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
1064
999
  rawOutput: rawDbg.text,
1065
- rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated },
1066
- resultTextFromExtract: resultSummaryText || null
1000
+ rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
1067
1001
  });
1068
1002
  }
1069
1003
  const retryInfo = extractRateLimitRetryInfo(streamEvents);
@@ -1087,7 +1021,15 @@ ${rawOutput}`);
1087
1021
  log(`${C.red} ${line}${C.reset}`);
1088
1022
  }
1089
1023
  if (config.verbose) {
1090
- logClaudeRunFailureDiagnostics(log, stderr, rawOutput);
1024
+ logClaudeRunFailureDiagnostics(log, {
1025
+ exitCode,
1026
+ sessionId,
1027
+ stderr,
1028
+ rawOutput,
1029
+ streamEvents,
1030
+ derivedError: detail2,
1031
+ apiError: errorMsg
1032
+ });
1091
1033
  }
1092
1034
  return;
1093
1035
  }
@@ -1097,11 +1039,11 @@ ${rawOutput}`);
1097
1039
  if (pullRequestUrl) logGreen(`\u{1F517} PR: ${pullRequestUrl}`);
1098
1040
  logGreen(`\u2705 Run completed`);
1099
1041
  } catch (err) {
1100
- const message = err instanceof Error ? err.message : String(err);
1042
+ const message = errMsg(err);
1101
1043
  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));
1044
+ const stderrDbg = truncateClaudeDebugString(stripAnsi(lastClaude.stderr));
1045
+ const stdoutDbg = truncateClaudeDebugString(stripAnsi(lastClaude.stdout));
1046
+ const rawDbg = truncateClaudeDebugString(stripAnsi(lastClaude.rawOutput));
1105
1047
  if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
1106
1048
  logClaudeRawFailureJson({
1107
1049
  runId: run.id,
@@ -1109,8 +1051,8 @@ ${rawOutput}`);
1109
1051
  taskTitle: run.taskTitle,
1110
1052
  thrownMessage: message,
1111
1053
  thrownStack: stack ?? null,
1112
- exitCode: lastClaudeExitCode,
1113
- sessionId: lastClaudeSessionId ?? null,
1054
+ exitCode: lastClaude.exitCode,
1055
+ sessionId: lastClaude.sessionId ?? null,
1114
1056
  note: "Thrown before or after Claude finished; fields may be empty.",
1115
1057
  stderr: stderrDbg.text,
1116
1058
  stderrMeta: { fullLength: stderrDbg.fullLength, truncated: stderrDbg.truncated },
@@ -1214,6 +1156,7 @@ function parseArgs() {
1214
1156
  workDir: process.cwd(),
1215
1157
  pollInterval: 3e4,
1216
1158
  maxConcurrent: 5,
1159
+ command: "run",
1217
1160
  poll: false,
1218
1161
  dryRun: false,
1219
1162
  once: false,
@@ -1263,20 +1206,23 @@ function parseArgs() {
1263
1206
  config.useWorktrees = false;
1264
1207
  break;
1265
1208
  case "cleanup":
1209
+ config.command = "cleanup";
1266
1210
  break;
1267
1211
  default:
1268
1212
  console.error(`Unknown argument: ${arg}`);
1269
1213
  process.exit(1);
1270
1214
  }
1271
1215
  }
1272
- if (!config.token) {
1273
- console.error("Error: No agent token found.");
1274
- console.error(" Add GCA_TOKEN=gca_... to .env.local, or pass --token gca_...");
1275
- process.exit(1);
1276
- }
1277
- if (!config.token.startsWith("gca_")) {
1278
- console.error(`Error: Token must start with gca_ (got "${config.token.slice(0, 8)}\u2026")`);
1279
- process.exit(1);
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
+ }
1280
1226
  }
1281
1227
  return config;
1282
1228
  }
@@ -1477,7 +1423,7 @@ async function runWithPolling(client, config, scheduler) {
1477
1423
  const pending = await client.listPendingRuns();
1478
1424
  handlePendingRuns(pending, scheduler, client, config);
1479
1425
  } catch (err) {
1480
- console.error(`Poll error: ${err instanceof Error ? err.message : err}`);
1426
+ console.error(`Poll error: ${errMsg(err)}`);
1481
1427
  }
1482
1428
  while (!scheduler.isEmpty()) await sleep(1e3);
1483
1429
  return;
@@ -1487,7 +1433,7 @@ async function runWithPolling(client, config, scheduler) {
1487
1433
  const pending = await client.listPendingRuns();
1488
1434
  handlePendingRuns(pending, scheduler, client, config);
1489
1435
  } catch (err) {
1490
- const msg = err instanceof Error ? err.message : String(err);
1436
+ const msg = errMsg(err);
1491
1437
  if (config.verbose || !msg.includes("fetch")) {
1492
1438
  console.error(`Poll error: ${msg}`);
1493
1439
  }
@@ -1495,29 +1441,18 @@ async function runWithPolling(client, config, scheduler) {
1495
1441
  await sleep(config.pollInterval);
1496
1442
  }
1497
1443
  }
1498
- function wantsVersionOnly(argv) {
1499
- return argv.some((a) => a === "--version" || a === "-v" || a === "-version");
1500
- }
1501
1444
  async function main() {
1502
- const argv = process.argv.slice(2);
1503
- if (wantsVersionOnly(argv)) {
1504
- showVersion();
1505
- }
1506
- if (process.argv.includes("cleanup")) {
1507
- loadEnvFile();
1508
- const workDir = process.cwd();
1509
- const workDirIdx = process.argv.indexOf("--work-dir");
1510
- const resolvedDir = workDirIdx >= 0 ? path.resolve(process.argv[workDirIdx + 1] ?? ".") : workDir;
1511
- await interactiveCleanup(resolvedDir);
1445
+ const config = parseArgs();
1446
+ if (config.command === "cleanup") {
1447
+ await interactiveCleanup(config.workDir);
1512
1448
  return;
1513
1449
  }
1514
- const config = parseArgs();
1515
1450
  const client = new GroupChatAgentClient(config.apiUrl, config.token);
1516
1451
  let me;
1517
1452
  try {
1518
1453
  me = await client.getMe();
1519
1454
  } catch (err) {
1520
- console.error("Failed to authenticate:", err instanceof Error ? err.message : err);
1455
+ console.error("Failed to authenticate:", errMsg(err));
1521
1456
  process.exit(1);
1522
1457
  }
1523
1458
  console.log(`
@@ -1540,10 +1475,20 @@ async function main() {
1540
1475
  await runWithWebSocket(client, config, scheduler);
1541
1476
  }
1542
1477
  if (!scheduler.isEmpty()) {
1543
- console.log(`\u23F3 Waiting for ${scheduler.activeTaskCount} in-flight task(s)\u2026`);
1544
- while (!scheduler.isEmpty()) {
1478
+ console.log(
1479
+ `\u23F3 Waiting for ${scheduler.activeTaskCount} in-flight task(s)\u2026 (Ctrl+C to force quit)`
1480
+ );
1481
+ let forceQuit = false;
1482
+ const forceHandler = () => {
1483
+ forceQuit = true;
1484
+ };
1485
+ process.once("SIGINT", forceHandler);
1486
+ process.once("SIGTERM", forceHandler);
1487
+ while (!scheduler.isEmpty() && !forceQuit) {
1545
1488
  await sleep(1e3);
1546
1489
  }
1490
+ process.removeListener("SIGINT", forceHandler);
1491
+ process.removeListener("SIGTERM", forceHandler);
1547
1492
  }
1548
1493
  console.log("\u{1F44B} Goodbye.");
1549
1494
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupchatai/claude-runner",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {