@groupchatai/claude-runner 0.4.8 → 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 +151 -11
- 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
|
|
@@ -600,6 +613,14 @@ function runShellCommand(cmd, args, cwd) {
|
|
|
600
613
|
});
|
|
601
614
|
}
|
|
602
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
|
+
}
|
|
603
624
|
var runCounter = 0;
|
|
604
625
|
var claudeRunnerSignalTeardownDone = false;
|
|
605
626
|
function tryBeginClaudeRunnerSignalTeardown() {
|
|
@@ -904,7 +925,7 @@ function truncateClaudeDebugString(text) {
|
|
|
904
925
|
truncated: true
|
|
905
926
|
};
|
|
906
927
|
}
|
|
907
|
-
async function processRun(client, run, config, worktreeDir, detail) {
|
|
928
|
+
async function processRun(client, run, config, worktreeDir, detail, runBaseDir, repoFound) {
|
|
908
929
|
const runNum = ++runCounter;
|
|
909
930
|
const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
|
|
910
931
|
const log = (msg) => console.log(`${runTag} ${msg}`);
|
|
@@ -915,7 +936,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
915
936
|
log(`\u23ED Run is no longer PENDING (now ${detail.status}), skipping.`);
|
|
916
937
|
return;
|
|
917
938
|
}
|
|
918
|
-
const prompt = buildClaudePrompt(detail);
|
|
939
|
+
const prompt = buildClaudePrompt(detail, !!repoFound);
|
|
919
940
|
if (config.dryRun) {
|
|
920
941
|
log("\u{1F3DC} DRY RUN \u2014 would execute Claude Code with prompt:");
|
|
921
942
|
console.log("---");
|
|
@@ -937,7 +958,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
937
958
|
throw err;
|
|
938
959
|
}
|
|
939
960
|
log("\u25B6 Run started");
|
|
940
|
-
const effectiveCwd = worktreeDir ?? config.workDir;
|
|
961
|
+
const effectiveCwd = worktreeDir ?? runBaseDir ?? config.workDir;
|
|
941
962
|
let lastClaude = {
|
|
942
963
|
stderr: "",
|
|
943
964
|
stdout: "",
|
|
@@ -973,8 +994,19 @@ async function processRun(client, run, config, worktreeDir, detail) {
|
|
|
973
994
|
effectiveCwd
|
|
974
995
|
);
|
|
975
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
|
+
}
|
|
976
1001
|
let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
|
|
1002
|
+
activeProcesses.delete(run.id);
|
|
977
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
|
+
}
|
|
978
1010
|
if (exitCode !== 0 && isFollowUp) {
|
|
979
1011
|
log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
|
|
980
1012
|
sessionCache.delete(run.taskId);
|
|
@@ -1113,6 +1145,66 @@ function loadEnvFile() {
|
|
|
1113
1145
|
}
|
|
1114
1146
|
}
|
|
1115
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
|
+
}
|
|
1116
1208
|
function getRunnerPackageInfo() {
|
|
1117
1209
|
try {
|
|
1118
1210
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -1327,12 +1419,21 @@ async function processRunWithDrain(client, run, scheduler, config) {
|
|
|
1327
1419
|
drainSchedulerSlot(scheduler, run);
|
|
1328
1420
|
return;
|
|
1329
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
|
+
}
|
|
1330
1431
|
let worktreeDir;
|
|
1331
|
-
if (config.useWorktrees) {
|
|
1432
|
+
if (config.useWorktrees && (!detail.repoUrl || repoFound)) {
|
|
1332
1433
|
try {
|
|
1333
|
-
const existingBranch = await resolveExistingPrBranch(detail,
|
|
1334
|
-
worktreeDir = createWorktree(
|
|
1335
|
-
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);
|
|
1336
1437
|
if (existingBranch) {
|
|
1337
1438
|
console.log(` \u{1F333} Worktree from existing PR branch ${existingBranch} \u2192 ${rel}`);
|
|
1338
1439
|
} else {
|
|
@@ -1346,7 +1447,7 @@ async function processRunWithDrain(client, run, scheduler, config) {
|
|
|
1346
1447
|
let currentDetail = detail;
|
|
1347
1448
|
while (current) {
|
|
1348
1449
|
try {
|
|
1349
|
-
await processRun(client, current, config, worktreeDir, currentDetail);
|
|
1450
|
+
await processRun(client, current, config, worktreeDir, currentDetail, runBaseDir, repoFound);
|
|
1350
1451
|
} catch (err) {
|
|
1351
1452
|
console.error(`Unhandled error processing run ${current.id}:`, err);
|
|
1352
1453
|
}
|
|
@@ -1363,14 +1464,19 @@ async function processRunWithDrain(client, run, scheduler, config) {
|
|
|
1363
1464
|
}
|
|
1364
1465
|
}
|
|
1365
1466
|
if (worktreeDir) {
|
|
1366
|
-
sessionCache.delete(run.taskId);
|
|
1367
1467
|
try {
|
|
1368
|
-
removeWorktreeSimple(
|
|
1468
|
+
removeWorktreeSimple(runBaseDir, worktreeDir);
|
|
1369
1469
|
console.log(` \u{1F9F9} Worktree cleaned up`);
|
|
1370
1470
|
} catch (err) {
|
|
1371
1471
|
console.error(` \u26A0 Failed to clean up worktree:`, err);
|
|
1372
1472
|
}
|
|
1373
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
|
+
}
|
|
1374
1480
|
}
|
|
1375
1481
|
async function runWithWebSocket(client, config, scheduler) {
|
|
1376
1482
|
let ConvexClient;
|
|
@@ -1395,6 +1501,30 @@ async function runWithWebSocket(client, config, scheduler) {
|
|
|
1395
1501
|
handlePendingRuns(runs, scheduler, client, config);
|
|
1396
1502
|
}
|
|
1397
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
|
+
);
|
|
1398
1528
|
await new Promise((resolve) => {
|
|
1399
1529
|
function shutdown() {
|
|
1400
1530
|
if (!tryBeginClaudeRunnerSignalTeardown()) return;
|
|
@@ -1444,6 +1574,16 @@ async function runWithPolling(client, config, scheduler) {
|
|
|
1444
1574
|
return;
|
|
1445
1575
|
}
|
|
1446
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
|
+
}
|
|
1447
1587
|
try {
|
|
1448
1588
|
const pending = await client.listPendingRuns();
|
|
1449
1589
|
handlePendingRuns(pending, scheduler, client, config);
|