@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.
Files changed (2) hide show
  1. package/dist/index.js +151 -11
  2. 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, config.workDir);
1334
- worktreeDir = createWorktree(config.workDir, run.taskId, existingBranch);
1335
- const rel = path.relative(config.workDir, worktreeDir);
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(config.workDir, worktreeDir);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupchatai/claude-runner",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {