@groupchatai/claude-runner 0.4.7 → 0.4.9
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 +188 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -79,7 +79,7 @@ var GroupChatAgentClient = class {
|
|
|
79
79
|
return this.request("POST", `/runs/${runId}/comment`, { body });
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
|
-
function buildClaudePrompt(detail) {
|
|
82
|
+
function buildClaudePrompt(detail, repoResolved) {
|
|
83
83
|
const parts = [];
|
|
84
84
|
parts.push(`# Task: ${detail.task.title}`);
|
|
85
85
|
if (detail.task.description) {
|
|
@@ -87,6 +87,19 @@ function buildClaudePrompt(detail) {
|
|
|
87
87
|
## Description
|
|
88
88
|
${detail.task.description}`);
|
|
89
89
|
}
|
|
90
|
+
if (detail.repoUrl && !repoResolved) {
|
|
91
|
+
parts.push(
|
|
92
|
+
[
|
|
93
|
+
"\n## Repository",
|
|
94
|
+
detail.repoUrl,
|
|
95
|
+
"Check if the current working directory is already this repo (compare `git remote get-url origin`).",
|
|
96
|
+
"If not, check if it exists locally in common locations like ~/Developer, ~/Projects, ~/src, or ~.",
|
|
97
|
+
"If you find it locally, `cd` into it before starting work.",
|
|
98
|
+
"If you cannot find it locally, ask the user if they would like you to clone it and where.",
|
|
99
|
+
"Do NOT clone without the user's confirmation."
|
|
100
|
+
].join("\n")
|
|
101
|
+
);
|
|
102
|
+
}
|
|
90
103
|
if (detail.prompt && detail.prompt !== detail.task.title) {
|
|
91
104
|
parts.push(`
|
|
92
105
|
## Instructions
|
|
@@ -114,6 +127,12 @@ ${comment.body}`
|
|
|
114
127
|
);
|
|
115
128
|
}
|
|
116
129
|
}
|
|
130
|
+
const existingPrUrl = detail.pullRequestUrl ?? detail.task.pullRequestUrl;
|
|
131
|
+
if (existingPrUrl) {
|
|
132
|
+
parts.push(`
|
|
133
|
+
## Existing Pull Request
|
|
134
|
+
${existingPrUrl}`);
|
|
135
|
+
}
|
|
117
136
|
if (detail.task.dueDate) {
|
|
118
137
|
let dueStr = new Date(detail.task.dueDate).toLocaleDateString();
|
|
119
138
|
if (detail.task.dueTime != null) {
|
|
@@ -126,12 +145,18 @@ ${comment.body}`
|
|
|
126
145
|
parts.push(`
|
|
127
146
|
Due: ${dueStr}`);
|
|
128
147
|
}
|
|
148
|
+
const prRules = existingPrUrl ? [
|
|
149
|
+
`- This task already has a PR: ${existingPrUrl}. Push your changes to the EXISTING branch. Do NOT create a new PR.`,
|
|
150
|
+
"- You may push to existing branches and contribute to existing PRs."
|
|
151
|
+
] : [
|
|
152
|
+
"- When you have made code changes, you MUST create a new branch, commit your changes, push to the remote, and open a PR using `gh pr create`. Do NOT skip PR creation \u2014 always open a real PR.",
|
|
153
|
+
"- You may push to existing branches and contribute to existing PRs."
|
|
154
|
+
];
|
|
129
155
|
parts.push(
|
|
130
156
|
[
|
|
131
157
|
"\n---",
|
|
132
158
|
"RULES:",
|
|
133
|
-
|
|
134
|
-
"- You may push to existing branches and contribute to existing PRs.",
|
|
159
|
+
...prRules,
|
|
135
160
|
"- NEVER run `gh pr merge`. Do NOT merge any PR.",
|
|
136
161
|
"- NEVER run `gh pr close`. Do NOT close any PR.",
|
|
137
162
|
"- NEVER run `git push --force` or `git push -f`. No force pushes.",
|
|
@@ -315,7 +340,23 @@ function safeFormatRateLimitPayload(o) {
|
|
|
315
340
|
}
|
|
316
341
|
function parseClaudeNdjsonEvents(rawOutput) {
|
|
317
342
|
const events = [];
|
|
318
|
-
|
|
343
|
+
const cleaned = stripAnsi(rawOutput).trim();
|
|
344
|
+
try {
|
|
345
|
+
const parsed = JSON.parse(cleaned);
|
|
346
|
+
if (Array.isArray(parsed)) {
|
|
347
|
+
for (const item of parsed) {
|
|
348
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
349
|
+
events.push(item);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return events;
|
|
353
|
+
}
|
|
354
|
+
if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
|
|
355
|
+
return [parsed];
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
for (const line of cleaned.split("\n")) {
|
|
319
360
|
const t = line.trim();
|
|
320
361
|
if (!t.startsWith("{")) continue;
|
|
321
362
|
try {
|
|
@@ -381,11 +422,7 @@ function claudeRunShouldReportAsError(exitCode, events) {
|
|
|
381
422
|
return false;
|
|
382
423
|
}
|
|
383
424
|
function compactTaskErrorForApi(detail) {
|
|
384
|
-
|
|
385
|
-
if (/^rate limit \(/i.test(s)) {
|
|
386
|
-
return s.split(/\s*—\s*/)[0]?.trim() ?? s;
|
|
387
|
-
}
|
|
388
|
-
return s;
|
|
425
|
+
return detail.trim();
|
|
389
426
|
}
|
|
390
427
|
function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverride) {
|
|
391
428
|
const format = config.verbose ? "stream-json" : "json";
|
|
@@ -576,6 +613,14 @@ function runShellCommand(cmd, args, cwd) {
|
|
|
576
613
|
});
|
|
577
614
|
}
|
|
578
615
|
var sessionCache = /* @__PURE__ */ new Map();
|
|
616
|
+
var activeProcesses = /* @__PURE__ */ new Map();
|
|
617
|
+
var stoppedRunIds = /* @__PURE__ */ new Set();
|
|
618
|
+
function stopActiveProcess(runId) {
|
|
619
|
+
const child = activeProcesses.get(runId);
|
|
620
|
+
if (!child || child.killed) return;
|
|
621
|
+
stoppedRunIds.add(runId);
|
|
622
|
+
child.kill("SIGINT");
|
|
623
|
+
}
|
|
579
624
|
var runCounter = 0;
|
|
580
625
|
var claudeRunnerSignalTeardownDone = false;
|
|
581
626
|
function tryBeginClaudeRunnerSignalTeardown() {
|
|
@@ -628,25 +673,16 @@ function createWorktree(repoDir, taskId, existingBranch) {
|
|
|
628
673
|
async function resolveExistingPrBranch(detail, workDir) {
|
|
629
674
|
try {
|
|
630
675
|
if (detail.branchName) return detail.branchName;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
"gh",
|
|
635
|
-
["pr", "view", detail.pullRequestUrl, "--json", "headRefName", "--jq", ".headRefName"],
|
|
636
|
-
workDir
|
|
637
|
-
);
|
|
638
|
-
if (branch2?.trim()) return branch2.trim();
|
|
639
|
-
} catch {
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
const prUrls = [];
|
|
676
|
+
const candidatePrUrls = [];
|
|
677
|
+
if (detail.pullRequestUrl) candidatePrUrls.push(detail.pullRequestUrl);
|
|
678
|
+
if (detail.task.pullRequestUrl) candidatePrUrls.push(detail.task.pullRequestUrl);
|
|
643
679
|
for (const item of detail.activity) {
|
|
644
680
|
if (item.body) {
|
|
645
681
|
const matches = item.body.match(GITHUB_PR_URL_RE);
|
|
646
|
-
if (matches)
|
|
682
|
+
if (matches) candidatePrUrls.push(...matches);
|
|
647
683
|
}
|
|
648
684
|
}
|
|
649
|
-
const prUrl =
|
|
685
|
+
const prUrl = candidatePrUrls[0];
|
|
650
686
|
if (!prUrl) return void 0;
|
|
651
687
|
const branch = await runShellCommand(
|
|
652
688
|
"gh",
|
|
@@ -889,7 +925,7 @@ function truncateClaudeDebugString(text) {
|
|
|
889
925
|
truncated: true
|
|
890
926
|
};
|
|
891
927
|
}
|
|
892
|
-
async function processRun(client, run, config, worktreeDir, detail) {
|
|
928
|
+
async function processRun(client, run, config, worktreeDir, detail, runBaseDir, repoFound) {
|
|
893
929
|
const runNum = ++runCounter;
|
|
894
930
|
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
895
931
|
const log = (msg) => console.log(`${runTag} ${msg}`);
|
|
@@ -900,7 +936,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
900
936
|
log(`\u23ED Run is no longer PENDING (now ${detail.status}), skipping.`);
|
|
901
937
|
return;
|
|
902
938
|
}
|
|
903
|
-
const prompt = buildClaudePrompt(detail);
|
|
939
|
+
const prompt = buildClaudePrompt(detail, !!repoFound);
|
|
904
940
|
if (config.dryRun) {
|
|
905
941
|
log("\u{1F3DC} DRY RUN \u2014 would execute Claude Code with prompt:");
|
|
906
942
|
console.log("---");
|
|
@@ -922,7 +958,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
922
958
|
throw err;
|
|
923
959
|
}
|
|
924
960
|
log("\u25B6 Run started");
|
|
925
|
-
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
961
|
+
const effectiveCwd = worktreeDir ?? runBaseDir ?? config.workDir;
|
|
926
962
|
let lastClaude = {
|
|
927
963
|
stderr: "",
|
|
928
964
|
stdout: "",
|
|
@@ -958,8 +994,19 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
958
994
|
effectiveCwd
|
|
959
995
|
);
|
|
960
996
|
log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
|
|
997
|
+
activeProcesses.set(run.id, child);
|
|
998
|
+
if (stoppedRunIds.has(run.id)) {
|
|
999
|
+
child.kill("SIGINT");
|
|
1000
|
+
}
|
|
961
1001
|
let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
|
|
1002
|
+
activeProcesses.delete(run.id);
|
|
962
1003
|
lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
|
|
1004
|
+
if (stoppedRunIds.has(run.id)) {
|
|
1005
|
+
stoppedRunIds.delete(run.id);
|
|
1006
|
+
if (sessionId) sessionCache.set(run.taskId, sessionId);
|
|
1007
|
+
log(`\u{1F6D1} Run stopped from UI`);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
963
1010
|
if (exitCode !== 0 && isFollowUp) {
|
|
964
1011
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
965
1012
|
sessionCache.delete(run.taskId);
|
|
@@ -1098,6 +1145,66 @@ function loadEnvFile() {
|
|
|
1098
1145
|
}
|
|
1099
1146
|
}
|
|
1100
1147
|
}
|
|
1148
|
+
var repoRegistry = /* @__PURE__ */ new Map();
|
|
1149
|
+
function normalizeRepoUrl(url) {
|
|
1150
|
+
return url.replace(/\.git$/, "").replace(/^git@github\.com:/, "https://github.com/").toLowerCase();
|
|
1151
|
+
}
|
|
1152
|
+
function gitRemoteUrl(dir) {
|
|
1153
|
+
try {
|
|
1154
|
+
return execFileSync("git", ["remote", "get-url", "origin"], {
|
|
1155
|
+
cwd: dir,
|
|
1156
|
+
encoding: "utf-8",
|
|
1157
|
+
stdio: "pipe"
|
|
1158
|
+
}).trim();
|
|
1159
|
+
} catch {
|
|
1160
|
+
return void 0;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function dirMatchesRepo(dir, repoUrl) {
|
|
1164
|
+
const remote = gitRemoteUrl(dir);
|
|
1165
|
+
if (!remote) return false;
|
|
1166
|
+
return normalizeRepoUrl(remote) === normalizeRepoUrl(repoUrl);
|
|
1167
|
+
}
|
|
1168
|
+
function scanForRepo(repoUrl) {
|
|
1169
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1170
|
+
if (!home) return void 0;
|
|
1171
|
+
const searchDirs = ["Developer", "Projects", "src", "dev", "code", "repos"].map(
|
|
1172
|
+
(d) => path.join(home, d)
|
|
1173
|
+
);
|
|
1174
|
+
for (const searchDir of searchDirs) {
|
|
1175
|
+
let entries;
|
|
1176
|
+
try {
|
|
1177
|
+
entries = readdirSync(searchDir);
|
|
1178
|
+
} catch {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
for (const entry of entries) {
|
|
1182
|
+
if (entry.startsWith(".")) continue;
|
|
1183
|
+
const candidate = path.join(searchDir, entry);
|
|
1184
|
+
try {
|
|
1185
|
+
if (!statSync(candidate).isDirectory()) continue;
|
|
1186
|
+
} catch {
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (dirMatchesRepo(candidate, repoUrl)) return candidate;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return void 0;
|
|
1193
|
+
}
|
|
1194
|
+
function resolveRepoCwd(repoUrl, currentWorkDir) {
|
|
1195
|
+
const cached = repoRegistry.get(normalizeRepoUrl(repoUrl));
|
|
1196
|
+
if (cached) return cached;
|
|
1197
|
+
if (dirMatchesRepo(currentWorkDir, repoUrl)) {
|
|
1198
|
+
repoRegistry.set(normalizeRepoUrl(repoUrl), currentWorkDir);
|
|
1199
|
+
return currentWorkDir;
|
|
1200
|
+
}
|
|
1201
|
+
const found = scanForRepo(repoUrl);
|
|
1202
|
+
if (found) {
|
|
1203
|
+
repoRegistry.set(normalizeRepoUrl(repoUrl), found);
|
|
1204
|
+
return found;
|
|
1205
|
+
}
|
|
1206
|
+
return void 0;
|
|
1207
|
+
}
|
|
1101
1208
|
function getRunnerPackageInfo() {
|
|
1102
1209
|
try {
|
|
1103
1210
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -1312,12 +1419,21 @@ async function processRunWithDrain(client, run, scheduler, config) {
|
|
|
1312
1419
|
drainSchedulerSlot(scheduler, run);
|
|
1313
1420
|
return;
|
|
1314
1421
|
}
|
|
1422
|
+
let runBaseDir = config.workDir;
|
|
1423
|
+
let repoFound = false;
|
|
1424
|
+
if (detail.repoUrl) {
|
|
1425
|
+
const resolved = resolveRepoCwd(detail.repoUrl, config.workDir);
|
|
1426
|
+
if (resolved) {
|
|
1427
|
+
runBaseDir = resolved;
|
|
1428
|
+
repoFound = true;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1315
1431
|
let worktreeDir;
|
|
1316
|
-
if (config.useWorktrees) {
|
|
1432
|
+
if (config.useWorktrees && (!detail.repoUrl || repoFound)) {
|
|
1317
1433
|
try {
|
|
1318
|
-
const existingBranch = await resolveExistingPrBranch(detail,
|
|
1319
|
-
worktreeDir = createWorktree(
|
|
1320
|
-
const rel = path.relative(
|
|
1434
|
+
const existingBranch = await resolveExistingPrBranch(detail, runBaseDir);
|
|
1435
|
+
worktreeDir = createWorktree(runBaseDir, run.taskId, existingBranch);
|
|
1436
|
+
const rel = path.relative(runBaseDir, worktreeDir);
|
|
1321
1437
|
if (existingBranch) {
|
|
1322
1438
|
console.log(` \u{1F333} Worktree from existing PR branch ${existingBranch} \u2192 ${rel}`);
|
|
1323
1439
|
} else {
|
|
@@ -1331,7 +1447,7 @@ async function processRunWithDrain(client, run, scheduler, config) {
|
|
|
1331
1447
|
let currentDetail = detail;
|
|
1332
1448
|
while (current) {
|
|
1333
1449
|
try {
|
|
1334
|
-
await processRun(client, current, config, worktreeDir, currentDetail);
|
|
1450
|
+
await processRun(client, current, config, worktreeDir, currentDetail, runBaseDir, repoFound);
|
|
1335
1451
|
} catch (err) {
|
|
1336
1452
|
console.error(`Unhandled error processing run ${current.id}:`, err);
|
|
1337
1453
|
}
|
|
@@ -1348,14 +1464,19 @@ async function processRunWithDrain(client, run, scheduler, config) {
|
|
|
1348
1464
|
}
|
|
1349
1465
|
}
|
|
1350
1466
|
if (worktreeDir) {
|
|
1351
|
-
sessionCache.delete(run.taskId);
|
|
1352
1467
|
try {
|
|
1353
|
-
removeWorktreeSimple(
|
|
1468
|
+
removeWorktreeSimple(runBaseDir, worktreeDir);
|
|
1354
1469
|
console.log(` \u{1F9F9} Worktree cleaned up`);
|
|
1355
1470
|
} catch (err) {
|
|
1356
1471
|
console.error(` \u26A0 Failed to clean up worktree:`, err);
|
|
1357
1472
|
}
|
|
1358
1473
|
}
|
|
1474
|
+
if (detail.repoUrl && !repoFound) {
|
|
1475
|
+
const found = resolveRepoCwd(detail.repoUrl, config.workDir);
|
|
1476
|
+
if (found) {
|
|
1477
|
+
console.log(` \u{1F4C2} Repo cached for future runs: ${found}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1359
1480
|
}
|
|
1360
1481
|
async function runWithWebSocket(client, config, scheduler) {
|
|
1361
1482
|
let ConvexClient;
|
|
@@ -1380,6 +1501,30 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
1380
1501
|
handlePendingRuns(runs, scheduler, client, config);
|
|
1381
1502
|
}
|
|
1382
1503
|
);
|
|
1504
|
+
let stoppedRunsWarned = false;
|
|
1505
|
+
convex.onUpdate(
|
|
1506
|
+
anyApi.agentWebSocket.stoppedRuns,
|
|
1507
|
+
{ token: config.token },
|
|
1508
|
+
(runIds) => {
|
|
1509
|
+
if (!runIds) return;
|
|
1510
|
+
for (const runId of runIds) {
|
|
1511
|
+
if (activeProcesses.has(runId)) {
|
|
1512
|
+
console.log(` \u{1F6D1} Run stopped from UI \u2014 sending stop signal to Claude Code\u2026`);
|
|
1513
|
+
stopActiveProcess(runId);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
() => {
|
|
1518
|
+
if (!stoppedRunsWarned) {
|
|
1519
|
+
stoppedRunsWarned = true;
|
|
1520
|
+
if (config.verbose) {
|
|
1521
|
+
console.log(
|
|
1522
|
+
`${C.dim}\u2139 stoppedRuns query not available on server \u2014 stop-from-UI disabled${C.reset}`
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
);
|
|
1383
1528
|
await new Promise((resolve) => {
|
|
1384
1529
|
function shutdown() {
|
|
1385
1530
|
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
@@ -1429,6 +1574,16 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1429
1574
|
return;
|
|
1430
1575
|
}
|
|
1431
1576
|
while (running) {
|
|
1577
|
+
for (const runId of activeProcesses.keys()) {
|
|
1578
|
+
try {
|
|
1579
|
+
const detail = await client.getRunDetail(runId);
|
|
1580
|
+
if (detail.status === "STOPPED") {
|
|
1581
|
+
console.log(` \u{1F6D1} Run stopped from UI \u2014 sending stop signal to Claude Code\u2026`);
|
|
1582
|
+
stopActiveProcess(runId);
|
|
1583
|
+
}
|
|
1584
|
+
} catch {
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1432
1587
|
try {
|
|
1433
1588
|
const pending = await client.listPendingRuns();
|
|
1434
1589
|
handlePendingRuns(pending, scheduler, client, config);
|