@groupchatai/claude-runner 0.2.3 → 0.4.0
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 +211 -75
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -240,11 +240,15 @@ var ALLOWED_TOOLS = [
|
|
|
240
240
|
"TodoWrite",
|
|
241
241
|
"NotebookEdit"
|
|
242
242
|
];
|
|
243
|
-
function spawnClaudeCode(prompt, config, runOptions) {
|
|
243
|
+
function spawnClaudeCode(prompt, config, runOptions, resumeSessionId) {
|
|
244
244
|
const format = config.verbose ? "stream-json" : "json";
|
|
245
|
-
const args = [
|
|
246
|
-
|
|
247
|
-
prompt
|
|
245
|
+
const args = [];
|
|
246
|
+
if (resumeSessionId) {
|
|
247
|
+
args.push("--resume", resumeSessionId, "-p", prompt);
|
|
248
|
+
} else {
|
|
249
|
+
args.push("-p", prompt);
|
|
250
|
+
}
|
|
251
|
+
args.push(
|
|
248
252
|
"--output-format",
|
|
249
253
|
format,
|
|
250
254
|
"--verbose",
|
|
@@ -252,7 +256,7 @@ function spawnClaudeCode(prompt, config, runOptions) {
|
|
|
252
256
|
"default",
|
|
253
257
|
"--allowedTools",
|
|
254
258
|
...ALLOWED_TOOLS
|
|
255
|
-
|
|
259
|
+
);
|
|
256
260
|
const model = runOptions?.model ?? config.model;
|
|
257
261
|
if (model) {
|
|
258
262
|
args.push("--model", model);
|
|
@@ -263,61 +267,97 @@ function spawnClaudeCode(prompt, config, runOptions) {
|
|
|
263
267
|
env: { ...process.env }
|
|
264
268
|
});
|
|
265
269
|
const pid = child.pid ?? 0;
|
|
266
|
-
const output = new Promise(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
270
|
+
const output = new Promise((resolve, reject) => {
|
|
271
|
+
const chunks = [];
|
|
272
|
+
const errChunks = [];
|
|
273
|
+
let lineBuf = "";
|
|
274
|
+
let lastResultJson = "";
|
|
275
|
+
let capturedSessionId;
|
|
276
|
+
let capturedPrUrl;
|
|
277
|
+
function checkEventForPrUrl(event) {
|
|
278
|
+
if (capturedPrUrl) return;
|
|
279
|
+
const texts = [];
|
|
280
|
+
if (event.message?.content) {
|
|
281
|
+
for (const block of event.message.content) {
|
|
282
|
+
if (block.text) texts.push(block.text);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (event.content_block?.text) texts.push(event.content_block.text);
|
|
286
|
+
if (typeof event.result === "string") texts.push(event.result);
|
|
287
|
+
const combined = texts.join("\n");
|
|
288
|
+
const match = combined.match(GITHUB_PR_URL_RE);
|
|
289
|
+
if (match) capturedPrUrl = match[match.length - 1];
|
|
290
|
+
}
|
|
291
|
+
child.stdout?.on("data", (data) => {
|
|
292
|
+
chunks.push(data);
|
|
293
|
+
const text = data.toString("utf-8");
|
|
294
|
+
lineBuf += text;
|
|
295
|
+
const lines = lineBuf.split("\n");
|
|
296
|
+
lineBuf = lines.pop() ?? "";
|
|
297
|
+
for (const line of lines) {
|
|
298
|
+
const trimmed = line.trim();
|
|
299
|
+
if (!trimmed) continue;
|
|
300
|
+
try {
|
|
301
|
+
const event = JSON.parse(trimmed);
|
|
302
|
+
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
303
|
+
capturedSessionId = event.session_id;
|
|
304
|
+
}
|
|
305
|
+
if (event.type === "result") lastResultJson = trimmed;
|
|
306
|
+
checkEventForPrUrl(event);
|
|
307
|
+
if (config.verbose) {
|
|
284
308
|
const formatted = formatStreamEvent(event, pid);
|
|
285
309
|
if (formatted) console.log(formatted);
|
|
286
|
-
}
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
if (config.verbose) {
|
|
287
313
|
console.log(`${pidTag(pid)} ${C.dim}${trimmed}${C.reset}`);
|
|
288
314
|
}
|
|
289
315
|
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
child.stderr?.on("data", (data) => {
|
|
319
|
+
errChunks.push(data);
|
|
320
|
+
if (config.verbose) {
|
|
321
|
+
const text = data.toString("utf-8").trimEnd();
|
|
322
|
+
if (text) console.error(`${pidTag(pid)} ${C.dim}${text}${C.reset}`);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
child.on("error", (err) => reject(new Error(`Failed to spawn claude: ${err.message}`)));
|
|
326
|
+
child.on("close", (code) => {
|
|
327
|
+
if (lineBuf.trim()) {
|
|
328
|
+
try {
|
|
329
|
+
const event = JSON.parse(lineBuf.trim());
|
|
330
|
+
if (event.type === "system" && event.subtype === "init" && event.session_id) {
|
|
331
|
+
capturedSessionId = event.session_id;
|
|
332
|
+
}
|
|
333
|
+
if (event.type === "result") lastResultJson = lineBuf.trim();
|
|
334
|
+
checkEventForPrUrl(event);
|
|
335
|
+
if (config.verbose) {
|
|
304
336
|
const formatted = formatStreamEvent(event, pid);
|
|
305
337
|
if (formatted) console.log(formatted);
|
|
306
|
-
}
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
if (config.verbose) {
|
|
307
341
|
console.log(`${pidTag(pid)} ${C.dim}${lineBuf.trim()}${C.reset}`);
|
|
308
342
|
}
|
|
309
343
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
344
|
+
}
|
|
345
|
+
const rawOutput = Buffer.concat(chunks).toString("utf-8");
|
|
346
|
+
const stdout = config.verbose ? lastResultJson || rawOutput : rawOutput;
|
|
347
|
+
const stderr = Buffer.concat(errChunks).toString("utf-8");
|
|
348
|
+
if (code !== 0 && stdout.trim().length === 0) {
|
|
349
|
+
reject(new Error(`claude exited with code ${code}: ${stderr}`));
|
|
350
|
+
} else {
|
|
351
|
+
resolve({
|
|
352
|
+
stdout,
|
|
353
|
+
rawOutput,
|
|
354
|
+
exitCode: code ?? 0,
|
|
355
|
+
sessionId: capturedSessionId,
|
|
356
|
+
streamPrUrl: capturedPrUrl
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
321
361
|
return { process: child, output };
|
|
322
362
|
}
|
|
323
363
|
function findResultEvent(stdout) {
|
|
@@ -404,8 +444,11 @@ function runShellCommand(cmd, args, cwd) {
|
|
|
404
444
|
});
|
|
405
445
|
});
|
|
406
446
|
}
|
|
447
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
448
|
+
var runCounter = 0;
|
|
407
449
|
async function processRun(client, run, config) {
|
|
408
|
-
const
|
|
450
|
+
const runNum = ++runCounter;
|
|
451
|
+
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
409
452
|
const log = (msg) => console.log(`${runTag} ${msg}`);
|
|
410
453
|
const logGreen = (msg) => console.log(`${runTag} ${C.green}${msg}${C.reset}`);
|
|
411
454
|
const detail = await client.getRunDetail(run.id);
|
|
@@ -446,14 +489,41 @@ async function processRun(client, run, config) {
|
|
|
446
489
|
});
|
|
447
490
|
await new Promise((resolve) => git.on("close", () => resolve()));
|
|
448
491
|
}
|
|
492
|
+
const cachedSessionId = sessionCache.get(run.taskId);
|
|
493
|
+
const isFollowUp = cachedSessionId !== void 0;
|
|
494
|
+
const effectivePrompt = isFollowUp ? detail.prompt : prompt;
|
|
495
|
+
const resumeSession = isFollowUp ? cachedSessionId : void 0;
|
|
449
496
|
if (config.verbose) {
|
|
450
|
-
|
|
497
|
+
if (isFollowUp) {
|
|
498
|
+
log(`\u{1F504} Resuming session ${cachedSessionId.slice(0, 12)}\u2026`);
|
|
499
|
+
}
|
|
500
|
+
log(`\u{1F4DD} Prompt (${effectivePrompt.length} chars)`);
|
|
451
501
|
if (effectiveModel) log(`\u{1F9E0} Model: ${effectiveModel}`);
|
|
452
502
|
}
|
|
453
|
-
const { process: child, output } = spawnClaudeCode(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
503
|
+
const { process: child, output } = spawnClaudeCode(
|
|
504
|
+
effectivePrompt,
|
|
505
|
+
config,
|
|
506
|
+
runOptions,
|
|
507
|
+
resumeSession
|
|
508
|
+
);
|
|
509
|
+
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
510
|
+
let { stdout, rawOutput, exitCode, sessionId, streamPrUrl } = await output;
|
|
511
|
+
if (exitCode !== 0 && isFollowUp) {
|
|
512
|
+
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
513
|
+
sessionCache.delete(run.taskId);
|
|
514
|
+
const retry = spawnClaudeCode(prompt, config, runOptions);
|
|
515
|
+
log(`\u{1F916} Claude Code spawned (pid ${retry.process.pid}) (fresh)`);
|
|
516
|
+
const retryResult = await retry.output;
|
|
517
|
+
stdout = retryResult.stdout;
|
|
518
|
+
rawOutput = retryResult.rawOutput;
|
|
519
|
+
exitCode = retryResult.exitCode;
|
|
520
|
+
sessionId = retryResult.sessionId;
|
|
521
|
+
streamPrUrl = retryResult.streamPrUrl;
|
|
522
|
+
}
|
|
523
|
+
if (sessionId) {
|
|
524
|
+
sessionCache.set(run.taskId, sessionId);
|
|
525
|
+
}
|
|
526
|
+
const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(config.workDir) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
|
|
457
527
|
if (exitCode !== 0) {
|
|
458
528
|
const errorMsg = `Claude Code exited with code ${exitCode}:
|
|
459
529
|
\`\`\`
|
|
@@ -570,23 +640,89 @@ function parseArgs() {
|
|
|
570
640
|
async function sleep(ms) {
|
|
571
641
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
572
642
|
}
|
|
573
|
-
|
|
643
|
+
var TaskScheduler = class {
|
|
644
|
+
slots = /* @__PURE__ */ new Map();
|
|
645
|
+
processing = /* @__PURE__ */ new Set();
|
|
646
|
+
get activeTaskCount() {
|
|
647
|
+
return this.slots.size;
|
|
648
|
+
}
|
|
649
|
+
isRunActive(runId) {
|
|
650
|
+
return this.processing.has(runId);
|
|
651
|
+
}
|
|
652
|
+
hasActiveTask(taskId) {
|
|
653
|
+
return this.slots.has(taskId);
|
|
654
|
+
}
|
|
655
|
+
tryStart(run, maxConcurrent) {
|
|
656
|
+
if (this.processing.has(run.id)) return "at_limit";
|
|
657
|
+
const existingSlot = this.slots.get(run.taskId);
|
|
658
|
+
if (existingSlot) {
|
|
659
|
+
if (!existingSlot.queue.some((q) => q.id === run.id)) {
|
|
660
|
+
existingSlot.queue.push(run);
|
|
661
|
+
}
|
|
662
|
+
return "queued";
|
|
663
|
+
}
|
|
664
|
+
if (this.slots.size >= maxConcurrent) {
|
|
665
|
+
return "at_limit";
|
|
666
|
+
}
|
|
667
|
+
this.slots.set(run.taskId, { activeRunId: run.id, queue: [] });
|
|
668
|
+
this.processing.add(run.id);
|
|
669
|
+
return "start";
|
|
670
|
+
}
|
|
671
|
+
complete(run) {
|
|
672
|
+
this.processing.delete(run.id);
|
|
673
|
+
const slot = this.slots.get(run.taskId);
|
|
674
|
+
if (!slot) return void 0;
|
|
675
|
+
const next = slot.queue.shift();
|
|
676
|
+
if (next) {
|
|
677
|
+
slot.activeRunId = next.id;
|
|
678
|
+
this.processing.add(next.id);
|
|
679
|
+
return next;
|
|
680
|
+
}
|
|
681
|
+
this.slots.delete(run.taskId);
|
|
682
|
+
return void 0;
|
|
683
|
+
}
|
|
684
|
+
isEmpty() {
|
|
685
|
+
return this.slots.size === 0;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
function handlePendingRuns(runs, scheduler, client, config) {
|
|
574
689
|
if (runs.length > 0 && config.verbose) {
|
|
575
690
|
console.log(`\u{1F4EC} ${runs.length} pending run(s)`);
|
|
576
691
|
}
|
|
577
692
|
for (const run of runs) {
|
|
578
|
-
|
|
579
|
-
if (
|
|
693
|
+
const result = scheduler.tryStart(run, config.maxConcurrent);
|
|
694
|
+
if (result === "at_limit") {
|
|
580
695
|
if (config.verbose) {
|
|
581
|
-
console.log(`\u23F8 At concurrency limit (${config.maxConcurrent}), waiting\u2026`);
|
|
696
|
+
console.log(`\u23F8 At concurrency limit (${config.maxConcurrent} tasks), waiting\u2026`);
|
|
582
697
|
}
|
|
583
698
|
break;
|
|
584
699
|
}
|
|
585
|
-
|
|
586
|
-
|
|
700
|
+
if (result === "queued") {
|
|
701
|
+
if (config.verbose) {
|
|
702
|
+
console.log(` ${C.dim}Queued follow-up for active task${C.reset}`);
|
|
703
|
+
}
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
processRunWithDrain(client, run, scheduler, config).catch(
|
|
707
|
+
(err) => console.error(`Unhandled error processing run ${run.id}:`, err)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function processRunWithDrain(client, run, scheduler, config) {
|
|
712
|
+
let current = run;
|
|
713
|
+
while (current) {
|
|
714
|
+
try {
|
|
715
|
+
await processRun(client, current, config);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error(`Unhandled error processing run ${current.id}:`, err);
|
|
718
|
+
}
|
|
719
|
+
current = scheduler.complete(current);
|
|
720
|
+
if (current) {
|
|
721
|
+
console.log(` \u23ED Processing queued follow-up\u2026`);
|
|
722
|
+
}
|
|
587
723
|
}
|
|
588
724
|
}
|
|
589
|
-
async function runWithWebSocket(client, config,
|
|
725
|
+
async function runWithWebSocket(client, config, scheduler) {
|
|
590
726
|
let ConvexClient;
|
|
591
727
|
let anyApi;
|
|
592
728
|
try {
|
|
@@ -595,7 +731,7 @@ async function runWithWebSocket(client, config, activeRuns) {
|
|
|
595
731
|
} catch {
|
|
596
732
|
console.warn("\u26A0 convex package not found \u2014 falling back to HTTP polling.");
|
|
597
733
|
console.warn(" Install convex for WebSocket mode: npm i convex\n");
|
|
598
|
-
await runWithPolling(client, config,
|
|
734
|
+
await runWithPolling(client, config, scheduler);
|
|
599
735
|
return;
|
|
600
736
|
}
|
|
601
737
|
const convex = new ConvexClient(config.convexUrl);
|
|
@@ -605,7 +741,7 @@ async function runWithWebSocket(client, config, activeRuns) {
|
|
|
605
741
|
{ token: config.token },
|
|
606
742
|
(runs) => {
|
|
607
743
|
if (!runs || runs.length === 0) return;
|
|
608
|
-
handlePendingRuns(runs,
|
|
744
|
+
handlePendingRuns(runs, scheduler, client, config);
|
|
609
745
|
}
|
|
610
746
|
);
|
|
611
747
|
await new Promise((resolve) => {
|
|
@@ -618,7 +754,7 @@ async function runWithWebSocket(client, config, activeRuns) {
|
|
|
618
754
|
process.on("SIGTERM", shutdown);
|
|
619
755
|
});
|
|
620
756
|
}
|
|
621
|
-
async function runWithPolling(client, config,
|
|
757
|
+
async function runWithPolling(client, config, scheduler) {
|
|
622
758
|
console.log(`\u{1F4E1} Polling every ${config.pollInterval}ms \u2014 listening for tasks\u2026
|
|
623
759
|
`);
|
|
624
760
|
let running = true;
|
|
@@ -631,17 +767,17 @@ async function runWithPolling(client, config, activeRuns) {
|
|
|
631
767
|
if (config.once) {
|
|
632
768
|
try {
|
|
633
769
|
const pending = await client.listPendingRuns();
|
|
634
|
-
handlePendingRuns(pending,
|
|
770
|
+
handlePendingRuns(pending, scheduler, client, config);
|
|
635
771
|
} catch (err) {
|
|
636
772
|
console.error(`Poll error: ${err instanceof Error ? err.message : err}`);
|
|
637
773
|
}
|
|
638
|
-
while (
|
|
774
|
+
while (!scheduler.isEmpty()) await sleep(1e3);
|
|
639
775
|
return;
|
|
640
776
|
}
|
|
641
777
|
while (running) {
|
|
642
778
|
try {
|
|
643
779
|
const pending = await client.listPendingRuns();
|
|
644
|
-
handlePendingRuns(pending,
|
|
780
|
+
handlePendingRuns(pending, scheduler, client, config);
|
|
645
781
|
} catch (err) {
|
|
646
782
|
const msg = err instanceof Error ? err.message : String(err);
|
|
647
783
|
if (config.verbose || !msg.includes("fetch")) {
|
|
@@ -670,15 +806,15 @@ async function main() {
|
|
|
670
806
|
if (config.model) console.log(` Model: ${config.model}`);
|
|
671
807
|
if (config.dryRun) console.log(` Mode: DRY RUN`);
|
|
672
808
|
console.log();
|
|
673
|
-
const
|
|
809
|
+
const scheduler = new TaskScheduler();
|
|
674
810
|
if (config.poll || config.once) {
|
|
675
|
-
await runWithPolling(client, config,
|
|
811
|
+
await runWithPolling(client, config, scheduler);
|
|
676
812
|
} else {
|
|
677
|
-
await runWithWebSocket(client, config,
|
|
813
|
+
await runWithWebSocket(client, config, scheduler);
|
|
678
814
|
}
|
|
679
|
-
if (
|
|
680
|
-
console.log(`\u23F3 Waiting for ${
|
|
681
|
-
while (
|
|
815
|
+
if (!scheduler.isEmpty()) {
|
|
816
|
+
console.log(`\u23F3 Waiting for ${scheduler.activeTaskCount} in-flight task(s)\u2026`);
|
|
817
|
+
while (!scheduler.isEmpty()) {
|
|
682
818
|
await sleep(1e3);
|
|
683
819
|
}
|
|
684
820
|
}
|