@groupchatai/claude-runner 0.4.0 → 0.4.2
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 +293 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { spawn } from "child_process";
|
|
5
|
-
import { readFileSync } from "fs";
|
|
4
|
+
import { spawn, execFileSync } from "child_process";
|
|
5
|
+
import { readFileSync, readdirSync, statSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
var API_URL = "https://groupchat.ai";
|
|
8
8
|
var CONVEX_URL = "https://fantastic-jay-464.convex.cloud";
|
|
@@ -116,7 +116,7 @@ Due: ${new Date(detail.task.dueDate).toLocaleDateString()}`);
|
|
|
116
116
|
"- NEVER run `gh pr merge`. Do NOT merge any PR.",
|
|
117
117
|
"- NEVER run `gh pr close`. Do NOT close any PR.",
|
|
118
118
|
"- NEVER run `git push --force` or `git push -f`. No force pushes.",
|
|
119
|
-
"- When you are done, provide a clear summary of what you accomplished
|
|
119
|
+
"- When you are done, provide a clear summary of what you accomplished."
|
|
120
120
|
].join("\n")
|
|
121
121
|
);
|
|
122
122
|
return parts.join("\n");
|
|
@@ -240,7 +240,7 @@ var ALLOWED_TOOLS = [
|
|
|
240
240
|
"TodoWrite",
|
|
241
241
|
"NotebookEdit"
|
|
242
242
|
];
|
|
243
|
-
function spawnClaudeCode(prompt, config, runOptions, resumeSessionId) {
|
|
243
|
+
function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverride) {
|
|
244
244
|
const format = config.verbose ? "stream-json" : "json";
|
|
245
245
|
const args = [];
|
|
246
246
|
if (resumeSessionId) {
|
|
@@ -262,9 +262,8 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId) {
|
|
|
262
262
|
args.push("--model", model);
|
|
263
263
|
}
|
|
264
264
|
const child = spawn("claude", args, {
|
|
265
|
-
cwd: config.workDir,
|
|
266
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
267
|
-
env: { ...process.env }
|
|
265
|
+
cwd: cwdOverride ?? config.workDir,
|
|
266
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
268
267
|
});
|
|
269
268
|
const pid = child.pid ?? 0;
|
|
270
269
|
const output = new Promise((resolve, reject) => {
|
|
@@ -446,7 +445,202 @@ function runShellCommand(cmd, args, cwd) {
|
|
|
446
445
|
}
|
|
447
446
|
var sessionCache = /* @__PURE__ */ new Map();
|
|
448
447
|
var runCounter = 0;
|
|
449
|
-
|
|
448
|
+
var WORKTREE_DIR = ".agent-worktrees";
|
|
449
|
+
var WORKTREE_PREFIX = "task-";
|
|
450
|
+
function worktreeNameForTask(taskId) {
|
|
451
|
+
return `${WORKTREE_PREFIX}${taskId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(-12)}`;
|
|
452
|
+
}
|
|
453
|
+
function execGit(args, cwd) {
|
|
454
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
455
|
+
}
|
|
456
|
+
function getDefaultBranch(repoDir) {
|
|
457
|
+
try {
|
|
458
|
+
const out = execGit(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], repoDir);
|
|
459
|
+
return out.replace("origin/", "");
|
|
460
|
+
} catch {
|
|
461
|
+
return "main";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function createWorktree(repoDir, taskId) {
|
|
465
|
+
const name = worktreeNameForTask(taskId);
|
|
466
|
+
const branchName = `agent/${name}-${Date.now()}`;
|
|
467
|
+
const worktreeBase = path.join(repoDir, WORKTREE_DIR);
|
|
468
|
+
const worktreePath = path.join(worktreeBase, name);
|
|
469
|
+
if (existsSync(worktreePath)) {
|
|
470
|
+
return worktreePath;
|
|
471
|
+
}
|
|
472
|
+
const baseBranch = getDefaultBranch(repoDir);
|
|
473
|
+
try {
|
|
474
|
+
execGit(["fetch", "origin", baseBranch, "--quiet"], repoDir);
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
execGit(["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], repoDir);
|
|
478
|
+
writeFileSync(path.join(worktreePath, ".agent-branch"), branchName, "utf-8");
|
|
479
|
+
return worktreePath;
|
|
480
|
+
}
|
|
481
|
+
async function listOurWorktrees(workDir) {
|
|
482
|
+
const worktreeDir = path.join(workDir, WORKTREE_DIR);
|
|
483
|
+
let entries;
|
|
484
|
+
try {
|
|
485
|
+
entries = readdirSync(worktreeDir).filter((e) => e.startsWith(WORKTREE_PREFIX));
|
|
486
|
+
} catch {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
const results = [];
|
|
490
|
+
for (const name of entries) {
|
|
491
|
+
const wtPath = path.join(worktreeDir, name);
|
|
492
|
+
let branch = `agent/${name}`;
|
|
493
|
+
try {
|
|
494
|
+
branch = readFileSync(path.join(wtPath, ".agent-branch"), "utf-8").trim();
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
let ageMs = 0;
|
|
498
|
+
let age = "unknown";
|
|
499
|
+
try {
|
|
500
|
+
const stat = statSync(wtPath);
|
|
501
|
+
ageMs = Date.now() - stat.mtimeMs;
|
|
502
|
+
if (ageMs < 6e4) age = "just now";
|
|
503
|
+
else if (ageMs < 36e5) age = `${Math.floor(ageMs / 6e4)}m ago`;
|
|
504
|
+
else if (ageMs < 864e5) age = `${Math.floor(ageMs / 36e5)}h ago`;
|
|
505
|
+
else age = `${Math.floor(ageMs / 864e5)}d ago`;
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
let isMerged = false;
|
|
509
|
+
try {
|
|
510
|
+
const merged = await runShellCommand(
|
|
511
|
+
"git",
|
|
512
|
+
["branch", "--merged", "main", "--list", branch],
|
|
513
|
+
workDir
|
|
514
|
+
);
|
|
515
|
+
isMerged = merged.trim().length > 0;
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
let hasUnpushed = false;
|
|
519
|
+
try {
|
|
520
|
+
const log = await runShellCommand(
|
|
521
|
+
"git",
|
|
522
|
+
["log", `origin/${branch}..${branch}`, "--oneline"],
|
|
523
|
+
wtPath
|
|
524
|
+
);
|
|
525
|
+
hasUnpushed = log.trim().length > 0;
|
|
526
|
+
} catch {
|
|
527
|
+
try {
|
|
528
|
+
const log = await runShellCommand(
|
|
529
|
+
"git",
|
|
530
|
+
["log", `origin/main..${branch}`, "--oneline"],
|
|
531
|
+
workDir
|
|
532
|
+
);
|
|
533
|
+
hasUnpushed = log.trim().length > 0;
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
results.push({
|
|
538
|
+
name,
|
|
539
|
+
path: wtPath,
|
|
540
|
+
branch,
|
|
541
|
+
age,
|
|
542
|
+
ageMs,
|
|
543
|
+
isMerged,
|
|
544
|
+
hasUnpushed: hasUnpushed && !isMerged,
|
|
545
|
+
safeToRemove: isMerged || !hasUnpushed && ageMs > 36e5
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return results;
|
|
549
|
+
}
|
|
550
|
+
async function removeWorktree(workDir, info) {
|
|
551
|
+
try {
|
|
552
|
+
await runShellCommand("git", ["worktree", "remove", info.path, "--force"], workDir);
|
|
553
|
+
try {
|
|
554
|
+
await runShellCommand("git", ["branch", "-D", info.branch], workDir);
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
return true;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
console.error(` Failed to remove ${info.name}: ${err instanceof Error ? err.message : err}`);
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function removeWorktreeSimple(repoDir, worktreePath) {
|
|
564
|
+
let branchName;
|
|
565
|
+
try {
|
|
566
|
+
branchName = readFileSync(path.join(worktreePath, ".agent-branch"), "utf-8").trim();
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
execGit(["worktree", "remove", worktreePath, "--force"], repoDir);
|
|
571
|
+
} catch {
|
|
572
|
+
try {
|
|
573
|
+
if (existsSync(worktreePath)) rmSync(worktreePath, { recursive: true, force: true });
|
|
574
|
+
execGit(["worktree", "prune"], repoDir);
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (branchName) {
|
|
579
|
+
try {
|
|
580
|
+
execGit(["branch", "-D", branchName], repoDir);
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function startupSweep(workDir) {
|
|
586
|
+
const worktrees = await listOurWorktrees(workDir);
|
|
587
|
+
if (worktrees.length === 0) return;
|
|
588
|
+
const safe = worktrees.filter((w) => w.safeToRemove);
|
|
589
|
+
const unsafe = worktrees.filter((w) => !w.safeToRemove);
|
|
590
|
+
if (safe.length > 0) {
|
|
591
|
+
console.log(`\u{1F9F9} Cleaning up ${safe.length} stale worktree(s)\u2026`);
|
|
592
|
+
for (const wt of safe) {
|
|
593
|
+
const label = wt.isMerged ? "merged" : "stale";
|
|
594
|
+
const ok = await removeWorktree(workDir, wt);
|
|
595
|
+
if (ok) console.log(` \u2705 Removed ${wt.name} (${label}, ${wt.age})`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (unsafe.length > 0) {
|
|
599
|
+
console.log(` \u26A0\uFE0F ${unsafe.length} worktree(s) with unmerged changes kept:`);
|
|
600
|
+
for (const wt of unsafe) {
|
|
601
|
+
console.log(` ${wt.name} (${wt.age})`);
|
|
602
|
+
}
|
|
603
|
+
console.log(` Run "npx @groupchatai/claude-runner cleanup" to manage them.
|
|
604
|
+
`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async function interactiveCleanup(workDir) {
|
|
608
|
+
const worktrees = await listOurWorktrees(workDir);
|
|
609
|
+
if (worktrees.length === 0) {
|
|
610
|
+
console.log("\n\u{1F33F} No agent worktrees found.\n");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
console.log(`
|
|
614
|
+
\u{1F33F} Found ${worktrees.length} worktree(s):
|
|
615
|
+
`);
|
|
616
|
+
for (const wt of worktrees) {
|
|
617
|
+
const icon = wt.safeToRemove ? "\u2705" : "\u26A0\uFE0F ";
|
|
618
|
+
const status = wt.isMerged ? "Branch merged" : wt.hasUnpushed ? "Branch NOT merged, has unpushed commits" : "Branch NOT merged";
|
|
619
|
+
const safety = wt.safeToRemove ? "Safe to remove" : "Has unmerged work";
|
|
620
|
+
console.log(` ${icon} ${wt.name} (${wt.branch})`);
|
|
621
|
+
console.log(` ${status} \u2022 Created ${wt.age} \u2022 ${safety}`);
|
|
622
|
+
}
|
|
623
|
+
const safe = worktrees.filter((w) => w.safeToRemove);
|
|
624
|
+
const unsafe = worktrees.filter((w) => !w.safeToRemove);
|
|
625
|
+
if (safe.length > 0) {
|
|
626
|
+
console.log(`
|
|
627
|
+
Removing ${safe.length} safe worktree(s)\u2026`);
|
|
628
|
+
for (const wt of safe) {
|
|
629
|
+
const ok = await removeWorktree(workDir, wt);
|
|
630
|
+
if (ok) console.log(` \u2705 Removed ${wt.name}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (unsafe.length > 0) {
|
|
634
|
+
console.log(`
|
|
635
|
+
\u26A0\uFE0F ${unsafe.length} worktree(s) with unmerged work:`);
|
|
636
|
+
for (const wt of unsafe) {
|
|
637
|
+
console.log(` Skipping ${wt.name} \u2014 has unmerged changes`);
|
|
638
|
+
console.log(` To force remove: git worktree remove "${wt.path}" --force`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
console.log();
|
|
642
|
+
}
|
|
643
|
+
async function processRun(client, run, config, worktreeDir) {
|
|
450
644
|
const runNum = ++runCounter;
|
|
451
645
|
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
452
646
|
const log = (msg) => console.log(`${runTag} ${msg}`);
|
|
@@ -480,11 +674,12 @@ async function processRun(client, run, config) {
|
|
|
480
674
|
throw err;
|
|
481
675
|
}
|
|
482
676
|
log("\u25B6 Run started");
|
|
677
|
+
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
483
678
|
try {
|
|
484
|
-
if (runOptions.branch) {
|
|
679
|
+
if (!worktreeDir && runOptions.branch) {
|
|
485
680
|
log(`\u{1F33F} Checking out branch: ${runOptions.branch}`);
|
|
486
681
|
const git = spawn("git", ["checkout", runOptions.branch], {
|
|
487
|
-
cwd:
|
|
682
|
+
cwd: effectiveCwd,
|
|
488
683
|
stdio: "pipe"
|
|
489
684
|
});
|
|
490
685
|
await new Promise((resolve) => git.on("close", () => resolve()));
|
|
@@ -504,14 +699,15 @@ async function processRun(client, run, config) {
|
|
|
504
699
|
effectivePrompt,
|
|
505
700
|
config,
|
|
506
701
|
runOptions,
|
|
507
|
-
resumeSession
|
|
702
|
+
resumeSession,
|
|
703
|
+
effectiveCwd
|
|
508
704
|
);
|
|
509
705
|
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
510
706
|
let { stdout, rawOutput, exitCode, sessionId, streamPrUrl } = await output;
|
|
511
707
|
if (exitCode !== 0 && isFollowUp) {
|
|
512
708
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
513
709
|
sessionCache.delete(run.taskId);
|
|
514
|
-
const retry = spawnClaudeCode(prompt, config, runOptions);
|
|
710
|
+
const retry = spawnClaudeCode(prompt, config, runOptions, void 0, effectiveCwd);
|
|
515
711
|
log(`\u{1F916} Claude Code spawned (pid ${retry.process.pid}) (fresh)`);
|
|
516
712
|
const retryResult = await retry.output;
|
|
517
713
|
stdout = retryResult.stdout;
|
|
@@ -523,7 +719,7 @@ async function processRun(client, run, config) {
|
|
|
523
719
|
if (sessionId) {
|
|
524
720
|
sessionCache.set(run.taskId, sessionId);
|
|
525
721
|
}
|
|
526
|
-
const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(
|
|
722
|
+
const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
|
|
527
723
|
if (exitCode !== 0) {
|
|
528
724
|
const errorMsg = `Claude Code exited with code ${exitCode}:
|
|
529
725
|
\`\`\`
|
|
@@ -576,6 +772,34 @@ function loadEnvFile() {
|
|
|
576
772
|
}
|
|
577
773
|
}
|
|
578
774
|
}
|
|
775
|
+
function showHelp() {
|
|
776
|
+
console.log(`
|
|
777
|
+
Usage: npx @groupchatai/claude-runner [command] [options]
|
|
778
|
+
|
|
779
|
+
Commands:
|
|
780
|
+
(default) Start the agent runner
|
|
781
|
+
cleanup Interactively review and remove stale worktrees
|
|
782
|
+
|
|
783
|
+
Options:
|
|
784
|
+
--work-dir <path> Repo directory for Claude Code to work in (default: cwd)
|
|
785
|
+
--poll Use HTTP polling fallback (default: websocket, see below)
|
|
786
|
+
--poll-interval <ms> Polling interval in milliseconds (default: 30000, only with --poll)
|
|
787
|
+
--max-concurrent <n> Max concurrent tasks (default: 5)
|
|
788
|
+
--model <model> Claude model to use (passed to claude CLI)
|
|
789
|
+
--dry-run Poll and log runs without executing Claude Code
|
|
790
|
+
--once Process one batch of pending runs and exit (implies --poll)
|
|
791
|
+
--verbose Stream Claude Code activity with pid
|
|
792
|
+
--token <token> Agent token (or set GCA_TOKEN env var)
|
|
793
|
+
--no-worktree Disable git worktree isolation for runs
|
|
794
|
+
-h, --help Show this help message
|
|
795
|
+
|
|
796
|
+
Environment variables:
|
|
797
|
+
GCA_TOKEN Agent token (gca_...)
|
|
798
|
+
GCA_API_URL API URL override
|
|
799
|
+
GCA_CONVEX_URL Convex URL override
|
|
800
|
+
`);
|
|
801
|
+
process.exit(0);
|
|
802
|
+
}
|
|
579
803
|
function parseArgs() {
|
|
580
804
|
loadEnvFile();
|
|
581
805
|
const args = process.argv.slice(2);
|
|
@@ -584,12 +808,13 @@ function parseArgs() {
|
|
|
584
808
|
apiUrl: process.env.GCA_API_URL ?? API_URL,
|
|
585
809
|
convexUrl: process.env.GCA_CONVEX_URL ?? CONVEX_URL,
|
|
586
810
|
workDir: process.cwd(),
|
|
587
|
-
pollInterval:
|
|
588
|
-
maxConcurrent:
|
|
811
|
+
pollInterval: 3e4,
|
|
812
|
+
maxConcurrent: 5,
|
|
589
813
|
poll: false,
|
|
590
814
|
dryRun: false,
|
|
591
815
|
once: false,
|
|
592
|
-
verbose: false
|
|
816
|
+
verbose: false,
|
|
817
|
+
useWorktrees: true
|
|
593
818
|
};
|
|
594
819
|
for (let i = 0; i < args.length; i++) {
|
|
595
820
|
const arg = args[i];
|
|
@@ -598,7 +823,7 @@ function parseArgs() {
|
|
|
598
823
|
config.workDir = path.resolve(args[++i] ?? ".");
|
|
599
824
|
break;
|
|
600
825
|
case "--poll-interval":
|
|
601
|
-
config.pollInterval = parseInt(args[++i] ?? "
|
|
826
|
+
config.pollInterval = parseInt(args[++i] ?? "30000", 10);
|
|
602
827
|
break;
|
|
603
828
|
case "--max-concurrent":
|
|
604
829
|
config.maxConcurrent = parseInt(args[++i] ?? "1", 10);
|
|
@@ -621,6 +846,15 @@ function parseArgs() {
|
|
|
621
846
|
case "--token":
|
|
622
847
|
config.token = args[++i] ?? "";
|
|
623
848
|
break;
|
|
849
|
+
case "--help":
|
|
850
|
+
case "-h":
|
|
851
|
+
showHelp();
|
|
852
|
+
break;
|
|
853
|
+
case "--no-worktree":
|
|
854
|
+
config.useWorktrees = false;
|
|
855
|
+
break;
|
|
856
|
+
case "cleanup":
|
|
857
|
+
break;
|
|
624
858
|
default:
|
|
625
859
|
console.error(`Unknown argument: ${arg}`);
|
|
626
860
|
process.exit(1);
|
|
@@ -709,10 +943,21 @@ function handlePendingRuns(runs, scheduler, client, config) {
|
|
|
709
943
|
}
|
|
710
944
|
}
|
|
711
945
|
async function processRunWithDrain(client, run, scheduler, config) {
|
|
946
|
+
let worktreeDir;
|
|
947
|
+
if (config.useWorktrees) {
|
|
948
|
+
try {
|
|
949
|
+
worktreeDir = createWorktree(config.workDir, run.taskId);
|
|
950
|
+
console.log(
|
|
951
|
+
` \u{1F333} Worktree created from main \u2192 ${path.relative(config.workDir, worktreeDir)}`
|
|
952
|
+
);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
console.error(` \u26A0 Failed to create worktree, running in-place:`, err);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
712
957
|
let current = run;
|
|
713
958
|
while (current) {
|
|
714
959
|
try {
|
|
715
|
-
await processRun(client, current, config);
|
|
960
|
+
await processRun(client, current, config, worktreeDir);
|
|
716
961
|
} catch (err) {
|
|
717
962
|
console.error(`Unhandled error processing run ${current.id}:`, err);
|
|
718
963
|
}
|
|
@@ -721,6 +966,15 @@ async function processRunWithDrain(client, run, scheduler, config) {
|
|
|
721
966
|
console.log(` \u23ED Processing queued follow-up\u2026`);
|
|
722
967
|
}
|
|
723
968
|
}
|
|
969
|
+
if (worktreeDir) {
|
|
970
|
+
sessionCache.delete(run.taskId);
|
|
971
|
+
try {
|
|
972
|
+
removeWorktreeSimple(config.workDir, worktreeDir);
|
|
973
|
+
console.log(` \u{1F9F9} Worktree cleaned up`);
|
|
974
|
+
} catch (err) {
|
|
975
|
+
console.error(` \u26A0 Failed to clean up worktree:`, err);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
724
978
|
}
|
|
725
979
|
async function runWithWebSocket(client, config, scheduler) {
|
|
726
980
|
let ConvexClient;
|
|
@@ -730,6 +984,7 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
730
984
|
({ anyApi } = await import("convex/server"));
|
|
731
985
|
} catch {
|
|
732
986
|
console.warn("\u26A0 convex package not found \u2014 falling back to HTTP polling.");
|
|
987
|
+
console.warn(" WebSocket mode is recommended for instant task pickup and zero idle cost.");
|
|
733
988
|
console.warn(" Install convex for WebSocket mode: npm i convex\n");
|
|
734
989
|
await runWithPolling(client, config, scheduler);
|
|
735
990
|
return;
|
|
@@ -755,8 +1010,14 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
755
1010
|
});
|
|
756
1011
|
}
|
|
757
1012
|
async function runWithPolling(client, config, scheduler) {
|
|
758
|
-
console.log(`\u{1F4E1} Polling every ${config.pollInterval}
|
|
759
|
-
|
|
1013
|
+
console.log(`\u{1F4E1} Polling every ${config.pollInterval / 1e3}s \u2014 listening for tasks\u2026`);
|
|
1014
|
+
console.log(
|
|
1015
|
+
`${C.dim} Tip: WebSocket mode (default) picks up tasks instantly with zero idle cost.${C.reset}`
|
|
1016
|
+
);
|
|
1017
|
+
console.log(
|
|
1018
|
+
`${C.dim} Remove --poll unless your network blocks WebSocket connections.${C.reset}
|
|
1019
|
+
`
|
|
1020
|
+
);
|
|
760
1021
|
let running = true;
|
|
761
1022
|
const shutdown = () => {
|
|
762
1023
|
console.log("\n\u{1F6D1} Shutting down\u2026");
|
|
@@ -788,6 +1049,14 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
788
1049
|
}
|
|
789
1050
|
}
|
|
790
1051
|
async function main() {
|
|
1052
|
+
if (process.argv.includes("cleanup")) {
|
|
1053
|
+
loadEnvFile();
|
|
1054
|
+
const workDir = process.cwd();
|
|
1055
|
+
const workDirIdx = process.argv.indexOf("--work-dir");
|
|
1056
|
+
const resolvedDir = workDirIdx >= 0 ? path.resolve(process.argv[workDirIdx + 1] ?? ".") : workDir;
|
|
1057
|
+
await interactiveCleanup(resolvedDir);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
791
1060
|
const config = parseArgs();
|
|
792
1061
|
const client = new GroupChatAgentClient(config.apiUrl, config.token);
|
|
793
1062
|
let me;
|
|
@@ -805,7 +1074,11 @@ async function main() {
|
|
|
805
1074
|
if (config.apiUrl !== API_URL) console.log(` API: ${config.apiUrl}`);
|
|
806
1075
|
if (config.model) console.log(` Model: ${config.model}`);
|
|
807
1076
|
if (config.dryRun) console.log(` Mode: DRY RUN`);
|
|
1077
|
+
if (!config.useWorktrees) console.log(` Worktrees: disabled`);
|
|
808
1078
|
console.log();
|
|
1079
|
+
if (config.useWorktrees) {
|
|
1080
|
+
await startupSweep(config.workDir);
|
|
1081
|
+
}
|
|
809
1082
|
const scheduler = new TaskScheduler();
|
|
810
1083
|
if (config.poll || config.once) {
|
|
811
1084
|
await runWithPolling(client, config, scheduler);
|