@groupchatai/claude-runner 0.4.5 → 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.
Files changed (2) hide show
  1. package/dist/index.js +87 -106
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -203,6 +203,16 @@ function formatStreamEvent(event, pid) {
203
203
  }
204
204
  case "tool_result":
205
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
+ }
206
216
  case "result": {
207
217
  const lines = [];
208
218
  if (event.cost_usd !== void 0 || event.total_cost_usd !== void 0) {
@@ -267,15 +277,6 @@ function clampUserFacingDetail(text, maxChars) {
267
277
  function stripAnsi(text) {
268
278
  return text.replace(/\x1b\[[0-9;]*m/g, "");
269
279
  }
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
280
  function formatAnthropicRateLimitPayload(o) {
280
281
  const rateLimitTypeRaw = typeof o.rateLimitType === "string" ? o.rateLimitType.replace(/_/g, " ") : "unknown";
281
282
  const rawReset = o.resetsAt;
@@ -326,26 +327,16 @@ function parseClaudeNdjsonEvents(rawOutput) {
326
327
  }
327
328
  return events;
328
329
  }
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");
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");
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
342
  const raw = info.resetsAt;
@@ -355,59 +346,23 @@ function extractRateLimitRetryInfo(events) {
355
346
  }
356
347
  return null;
357
348
  }
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
- }
349
+ function deriveClaudeFailureSummary(exitCode, stderr, events) {
378
350
  for (let i = events.length - 1; i >= 0; i--) {
379
351
  const ev = events[i];
380
352
  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
353
  if (ev.is_error === true && typeof ev.result === "string" && ev.result.trim()) {
387
354
  return clampUserFacingDetail(ev.result.trim(), 2e3);
388
355
  }
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
356
  if (typeof ev.error === "string" && ev.error.trim()) {
397
357
  return clampUserFacingDetail(ev.error.trim(), 2e3);
398
358
  }
399
- const nested = errorMessageFromUnknown(ev.error);
400
- if (nested) return clampUserFacingDetail(nested, 2e3);
401
359
  }
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);
360
+ const stderrText = stripAnsi(stderr).trim();
361
+ if (stderrText.length > 0) {
362
+ return clampUserFacingDetail(stderrText, 2e3);
408
363
  }
409
364
  for (const ev of events) {
410
- if (ev.type === "rate_limit_event") {
365
+ if (ev.type === "rate_limit_event" && isRateLimitEventBlocking(ev)) {
411
366
  const info = ev.rate_limit_info;
412
367
  if (info && typeof info === "object" && !Array.isArray(info)) {
413
368
  return clampUserFacingDetail(
@@ -417,23 +372,13 @@ function deriveClaudeFailureSummary(exitCode, rawOutput, stderr, events) {
417
372
  }
418
373
  }
419
374
  }
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.`;
375
+ return `Claude Code ended with exit code ${exitCode}`;
425
376
  }
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;
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;
434
381
  }
435
- if (isClaudeUsageLimitText(options.resultSummaryText)) return true;
436
- if (isClaudeUsageLimitText(options.combinedText)) return true;
437
382
  return false;
438
383
  }
439
384
  function compactTaskErrorForApi(detail) {
@@ -892,23 +837,51 @@ Removing ${safe.length} safe worktree(s)\u2026`);
892
837
  }
893
838
  console.log();
894
839
  }
895
- function logClaudeRunFailureDiagnostics(log, stderr, rawOutput) {
896
- const se = stripAnsi(stderr).trimEnd();
897
- const so = stripAnsi(rawOutput);
840
+ function logClaudeRunFailureDiagnostics(log, opts) {
841
+ const se = stripAnsi(opts.stderr).trimEnd();
842
+ const so = stripAnsi(opts.rawOutput);
898
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
+ }
899
872
  if (se.length > 0) {
900
- log(`${C.dim}stderr:${C.reset}`);
873
+ log(`${C.dim} stderr:${C.reset}`);
901
874
  for (const ln of se.split("\n")) {
902
- log(`${C.dim} ${ln}${C.reset}`);
875
+ log(`${C.dim} ${ln}${C.reset}`);
903
876
  }
904
877
  } else {
905
- log(`${C.dim}(stderr empty)${C.reset}`);
878
+ log(`${C.dim} stderr: (empty)${C.reset}`);
906
879
  }
907
880
  const lines = so.length > 0 ? so.split("\n") : [];
908
881
  const tailLines = lines.length > 200 ? lines.slice(-200) : lines;
909
- log(`${C.dim}stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
882
+ log(`${C.dim} stdout: ${lines.length} line(s), showing last ${tailLines.length}${C.reset}`);
910
883
  for (const ln of tailLines) {
911
- log(`${C.dim} ${ln}${C.reset}`);
884
+ log(`${C.dim} ${ln}${C.reset}`);
912
885
  }
913
886
  log(`${C.dim}\u2500\u2500 end diagnostics \u2500\u2500${C.reset}`);
914
887
  }
@@ -1029,17 +1002,9 @@ async function processRun(client, run, config, worktreeDir, detail) {
1029
1002
  }
1030
1003
  const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
1031
1004
  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);
1005
+ const isError = claudeRunShouldReportAsError(exitCode, streamEvents);
1006
+ if (isError) {
1007
+ const detail2 = deriveClaudeFailureSummary(exitCode, stderr, streamEvents);
1043
1008
  const errorMsg = compactTaskErrorForApi(detail2);
1044
1009
  if (process.env.GCA_DEBUG_CLAUDE_RAW === "1") {
1045
1010
  const stderrDbg = truncateClaudeDebugString(stripAnsi(stderr));
@@ -1050,7 +1015,6 @@ ${rawOutput}`);
1050
1015
  taskId: run.taskId,
1051
1016
  taskTitle: run.taskTitle,
1052
1017
  exitCode,
1053
- usageCap,
1054
1018
  sessionId: sessionId ?? null,
1055
1019
  streamPrUrl: streamPrUrl ?? null,
1056
1020
  pullRequestUrl: pullRequestUrl ?? null,
@@ -1062,8 +1026,7 @@ ${rawOutput}`);
1062
1026
  stdout: stdoutDbg.text,
1063
1027
  stdoutMeta: { fullLength: stdoutDbg.fullLength, truncated: stdoutDbg.truncated },
1064
1028
  rawOutput: rawDbg.text,
1065
- rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated },
1066
- resultTextFromExtract: resultSummaryText || null
1029
+ rawOutputMeta: { fullLength: rawDbg.fullLength, truncated: rawDbg.truncated }
1067
1030
  });
1068
1031
  }
1069
1032
  const retryInfo = extractRateLimitRetryInfo(streamEvents);
@@ -1087,7 +1050,15 @@ ${rawOutput}`);
1087
1050
  log(`${C.red} ${line}${C.reset}`);
1088
1051
  }
1089
1052
  if (config.verbose) {
1090
- logClaudeRunFailureDiagnostics(log, stderr, rawOutput);
1053
+ logClaudeRunFailureDiagnostics(log, {
1054
+ exitCode,
1055
+ sessionId,
1056
+ stderr,
1057
+ rawOutput,
1058
+ streamEvents,
1059
+ derivedError: detail2,
1060
+ apiError: errorMsg
1061
+ });
1091
1062
  }
1092
1063
  return;
1093
1064
  }
@@ -1540,10 +1511,20 @@ async function main() {
1540
1511
  await runWithWebSocket(client, config, scheduler);
1541
1512
  }
1542
1513
  if (!scheduler.isEmpty()) {
1543
- console.log(`\u23F3 Waiting for ${scheduler.activeTaskCount} in-flight task(s)\u2026`);
1544
- while (!scheduler.isEmpty()) {
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) {
1545
1524
  await sleep(1e3);
1546
1525
  }
1526
+ process.removeListener("SIGINT", forceHandler);
1527
+ process.removeListener("SIGTERM", forceHandler);
1547
1528
  }
1548
1529
  console.log("\u{1F44B} Goodbye.");
1549
1530
  }
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.6",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {