@groupchatai/claude-runner 0.4.6 → 0.4.7

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