@groupchatai/claude-runner 0.4.8 → 0.4.10

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 +218 -18
  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
@@ -114,6 +127,14 @@ ${comment.body}`
114
127
  );
115
128
  }
116
129
  }
130
+ if (detail.lastFollowUpMessage) {
131
+ parts.push(
132
+ `
133
+ ## Latest Follow-up Request
134
+ The user has sent a follow-up message. Address this:
135
+ ${detail.lastFollowUpMessage}`
136
+ );
137
+ }
117
138
  const existingPrUrl = detail.pullRequestUrl ?? detail.task.pullRequestUrl;
118
139
  if (existingPrUrl) {
119
140
  parts.push(`
@@ -600,6 +621,14 @@ function runShellCommand(cmd, args, cwd) {
600
621
  });
601
622
  }
602
623
  var sessionCache = /* @__PURE__ */ new Map();
624
+ var activeProcesses = /* @__PURE__ */ new Map();
625
+ var stoppedRunIds = /* @__PURE__ */ new Set();
626
+ function stopActiveProcess(runId) {
627
+ const child = activeProcesses.get(runId);
628
+ if (!child || child.killed) return;
629
+ stoppedRunIds.add(runId);
630
+ child.kill("SIGINT");
631
+ }
603
632
  var runCounter = 0;
604
633
  var claudeRunnerSignalTeardownDone = false;
605
634
  function tryBeginClaudeRunnerSignalTeardown() {
@@ -904,18 +933,35 @@ function truncateClaudeDebugString(text) {
904
933
  truncated: true
905
934
  };
906
935
  }
907
- async function processRun(client, run, config, worktreeDir, detail) {
936
+ async function processRun(client, run, config, worktreeDir, detail, runBaseDir, repoFound) {
908
937
  const runNum = ++runCounter;
909
938
  const runTag = ` ${C.pid}[${runNum}]${C.reset}`;
910
939
  const log = (msg) => console.log(`${runTag} ${msg}`);
911
940
  const logGreen = (msg) => console.log(`${runTag} ${C.green}${msg}${C.reset}`);
912
941
  const ownerName = run.owner?.name ?? detail.owner?.name ?? "unknown";
913
942
  log(`\u{1F4CB} "${run.taskTitle}" \u2014 delegated by ${ownerName}`);
943
+ if (config.verbose) {
944
+ log(`\u{1F4CE} Run ID: ${run.id} | Task ID: ${run.taskId}`);
945
+ log(
946
+ `\u{1F4CE} Detail status: ${detail.status} | Has lastFollowUpMessage: ${!!detail.lastFollowUpMessage} | Has lastSessionId: ${!!detail.lastSessionId}`
947
+ );
948
+ log(
949
+ `\u{1F4CE} In-memory session cache hit: ${sessionCache.has(run.taskId)} | Activity items: ${detail.activity.length}`
950
+ );
951
+ if (detail.lastFollowUpMessage) {
952
+ log(
953
+ `\u{1F4CE} lastFollowUpMessage: "${detail.lastFollowUpMessage.slice(0, 150)}${detail.lastFollowUpMessage.length > 150 ? "\u2026" : ""}"`
954
+ );
955
+ }
956
+ if (detail.lastSessionId) {
957
+ log(`\u{1F4CE} Server lastSessionId: ${detail.lastSessionId.slice(0, 12)}\u2026`);
958
+ }
959
+ }
914
960
  if (detail.status !== "PENDING") {
915
961
  log(`\u23ED Run is no longer PENDING (now ${detail.status}), skipping.`);
916
962
  return;
917
963
  }
918
- const prompt = buildClaudePrompt(detail);
964
+ const prompt = buildClaudePrompt(detail, !!repoFound);
919
965
  if (config.dryRun) {
920
966
  log("\u{1F3DC} DRY RUN \u2014 would execute Claude Code with prompt:");
921
967
  console.log("---");
@@ -937,7 +983,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
937
983
  throw err;
938
984
  }
939
985
  log("\u25B6 Run started");
940
- const effectiveCwd = worktreeDir ?? config.workDir;
986
+ const effectiveCwd = worktreeDir ?? runBaseDir ?? config.workDir;
941
987
  let lastClaude = {
942
988
  stderr: "",
943
989
  stdout: "",
@@ -954,16 +1000,43 @@ async function processRun(client, run, config, worktreeDir, detail) {
954
1000
  });
955
1001
  await new Promise((resolve) => git.on("close", () => resolve()));
956
1002
  }
957
- const cachedSessionId = sessionCache.get(run.taskId);
1003
+ const cachedSessionId = sessionCache.get(run.taskId) ?? detail.lastSessionId;
958
1004
  const isFollowUp = cachedSessionId !== void 0;
959
- const effectivePrompt = isFollowUp ? detail.prompt : prompt;
1005
+ let effectivePrompt;
1006
+ if (isFollowUp && detail.lastFollowUpMessage) {
1007
+ effectivePrompt = detail.lastFollowUpMessage;
1008
+ } else if (isFollowUp) {
1009
+ effectivePrompt = prompt;
1010
+ } else {
1011
+ effectivePrompt = prompt;
1012
+ }
960
1013
  const resumeSession = isFollowUp ? cachedSessionId : void 0;
961
1014
  if (config.verbose) {
962
1015
  if (isFollowUp) {
963
- log(`\u{1F504} Resuming session ${cachedSessionId.slice(0, 12)}\u2026`);
1016
+ const sessionSource = sessionCache.has(run.taskId) ? "in-memory cache" : "server (lastSessionId)";
1017
+ log(`\u{1F504} Resuming session ${cachedSessionId.slice(0, 12)}\u2026 (source: ${sessionSource})`);
1018
+ if (detail.lastFollowUpMessage) {
1019
+ log(
1020
+ `\u{1F4AC} Follow-up message: "${detail.lastFollowUpMessage.slice(0, 200)}${detail.lastFollowUpMessage.length > 200 ? "\u2026" : ""}"`
1021
+ );
1022
+ } else {
1023
+ log(`\u26A0 No lastFollowUpMessage found \u2014 using full prompt for resumed session`);
1024
+ }
1025
+ } else {
1026
+ log(`\u{1F195} Initial run (no session to resume)`);
964
1027
  }
965
- log(`\u{1F4DD} Prompt (${effectivePrompt.length} chars)`);
1028
+ log(
1029
+ `\u{1F4DD} Prompt (${effectivePrompt.length} chars)${isFollowUp ? " [follow-up]" : " [initial]"}`
1030
+ );
1031
+ const previewLen = 500;
1032
+ log(
1033
+ `\u{1F4DD} Prompt preview:
1034
+ ${effectivePrompt.slice(0, previewLen)}${effectivePrompt.length > previewLen ? "\n... (truncated)" : ""}`
1035
+ );
966
1036
  if (effectiveModel) log(`\u{1F9E0} Model: ${effectiveModel}`);
1037
+ log(
1038
+ `\u{1F4CA} Activity items: ${detail.activity.length}, Comments: ${detail.activity.filter((a) => a.type === "comment").length}`
1039
+ );
967
1040
  }
968
1041
  const { process: child, output } = spawnClaudeCode(
969
1042
  effectivePrompt,
@@ -973,8 +1046,19 @@ async function processRun(client, run, config, worktreeDir, detail) {
973
1046
  effectiveCwd
974
1047
  );
975
1048
  log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
1049
+ activeProcesses.set(run.id, child);
1050
+ if (stoppedRunIds.has(run.id)) {
1051
+ child.kill("SIGINT");
1052
+ }
976
1053
  let { stdout, rawOutput, stderr, exitCode, sessionId, streamPrUrl } = await output;
1054
+ activeProcesses.delete(run.id);
977
1055
  lastClaude = { stderr, stdout, rawOutput, exitCode, sessionId };
1056
+ if (stoppedRunIds.has(run.id)) {
1057
+ stoppedRunIds.delete(run.id);
1058
+ if (sessionId) sessionCache.set(run.taskId, sessionId);
1059
+ log(`\u{1F6D1} Run stopped from UI`);
1060
+ return;
1061
+ }
978
1062
  if (exitCode !== 0 && isFollowUp) {
979
1063
  log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
980
1064
  sessionCache.delete(run.taskId);
@@ -985,6 +1069,9 @@ async function processRun(client, run, config, worktreeDir, detail) {
985
1069
  }
986
1070
  if (sessionId) {
987
1071
  sessionCache.set(run.taskId, sessionId);
1072
+ if (config.verbose) {
1073
+ log(`\u{1F4BE} Session ${sessionId.slice(0, 12)}\u2026 cached for task ${run.taskId}`);
1074
+ }
988
1075
  }
989
1076
  const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(effectiveCwd) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
990
1077
  const streamEvents = parseClaudeNdjsonEvents(rawOutput);
@@ -1019,7 +1106,8 @@ async function processRun(client, run, config, worktreeDir, detail) {
1019
1106
  await client.errorRun(run.id, errorMsg, {
1020
1107
  pullRequestUrl,
1021
1108
  errorType: retryInfo?.errorType,
1022
- retryAfter: retryInfo?.retryAfterMs
1109
+ retryAfter: retryInfo?.retryAfterMs,
1110
+ sessionId: lastClaude.sessionId
1023
1111
  });
1024
1112
  if (retryInfo) {
1025
1113
  const retryAt = new Date(retryInfo.retryAfterMs).toLocaleString(void 0, {
@@ -1050,7 +1138,11 @@ async function processRun(client, run, config, worktreeDir, detail) {
1050
1138
  }
1051
1139
  const resultText = extractResultText(stdout);
1052
1140
  const cost = extractCost(stdout);
1053
- await client.completeRun(run.id, resultText, { ...cost, pullRequestUrl });
1141
+ await client.completeRun(run.id, resultText, {
1142
+ ...cost,
1143
+ pullRequestUrl,
1144
+ sessionId: lastClaude.sessionId
1145
+ });
1054
1146
  if (pullRequestUrl) logGreen(`\u{1F517} PR: ${pullRequestUrl}`);
1055
1147
  logGreen(`\u2705 Run completed`);
1056
1148
  } catch (err) {
@@ -1082,7 +1174,7 @@ async function processRun(client, run, config, worktreeDir, detail) {
1082
1174
  ${message.slice(0, 2e3)}
1083
1175
  \`\`\``;
1084
1176
  try {
1085
- await client.errorRun(run.id, errorBody);
1177
+ await client.errorRun(run.id, errorBody, { sessionId: lastClaude.sessionId });
1086
1178
  } catch {
1087
1179
  log(`${C.dim}\u26A0 Failed to report error to API${C.reset}`);
1088
1180
  }
@@ -1113,6 +1205,66 @@ function loadEnvFile() {
1113
1205
  }
1114
1206
  }
1115
1207
  }
1208
+ var repoRegistry = /* @__PURE__ */ new Map();
1209
+ function normalizeRepoUrl(url) {
1210
+ return url.replace(/\.git$/, "").replace(/^git@github\.com:/, "https://github.com/").toLowerCase();
1211
+ }
1212
+ function gitRemoteUrl(dir) {
1213
+ try {
1214
+ return execFileSync("git", ["remote", "get-url", "origin"], {
1215
+ cwd: dir,
1216
+ encoding: "utf-8",
1217
+ stdio: "pipe"
1218
+ }).trim();
1219
+ } catch {
1220
+ return void 0;
1221
+ }
1222
+ }
1223
+ function dirMatchesRepo(dir, repoUrl) {
1224
+ const remote = gitRemoteUrl(dir);
1225
+ if (!remote) return false;
1226
+ return normalizeRepoUrl(remote) === normalizeRepoUrl(repoUrl);
1227
+ }
1228
+ function scanForRepo(repoUrl) {
1229
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1230
+ if (!home) return void 0;
1231
+ const searchDirs = ["Developer", "Projects", "src", "dev", "code", "repos"].map(
1232
+ (d) => path.join(home, d)
1233
+ );
1234
+ for (const searchDir of searchDirs) {
1235
+ let entries;
1236
+ try {
1237
+ entries = readdirSync(searchDir);
1238
+ } catch {
1239
+ continue;
1240
+ }
1241
+ for (const entry of entries) {
1242
+ if (entry.startsWith(".")) continue;
1243
+ const candidate = path.join(searchDir, entry);
1244
+ try {
1245
+ if (!statSync(candidate).isDirectory()) continue;
1246
+ } catch {
1247
+ continue;
1248
+ }
1249
+ if (dirMatchesRepo(candidate, repoUrl)) return candidate;
1250
+ }
1251
+ }
1252
+ return void 0;
1253
+ }
1254
+ function resolveRepoCwd(repoUrl, currentWorkDir) {
1255
+ const cached = repoRegistry.get(normalizeRepoUrl(repoUrl));
1256
+ if (cached) return cached;
1257
+ if (dirMatchesRepo(currentWorkDir, repoUrl)) {
1258
+ repoRegistry.set(normalizeRepoUrl(repoUrl), currentWorkDir);
1259
+ return currentWorkDir;
1260
+ }
1261
+ const found = scanForRepo(repoUrl);
1262
+ if (found) {
1263
+ repoRegistry.set(normalizeRepoUrl(repoUrl), found);
1264
+ return found;
1265
+ }
1266
+ return void 0;
1267
+ }
1116
1268
  function getRunnerPackageInfo() {
1117
1269
  try {
1118
1270
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -1327,12 +1479,21 @@ async function processRunWithDrain(client, run, scheduler, config) {
1327
1479
  drainSchedulerSlot(scheduler, run);
1328
1480
  return;
1329
1481
  }
1482
+ let runBaseDir = config.workDir;
1483
+ let repoFound = false;
1484
+ if (detail.repoUrl) {
1485
+ const resolved = resolveRepoCwd(detail.repoUrl, config.workDir);
1486
+ if (resolved) {
1487
+ runBaseDir = resolved;
1488
+ repoFound = true;
1489
+ }
1490
+ }
1330
1491
  let worktreeDir;
1331
- if (config.useWorktrees) {
1492
+ if (config.useWorktrees && (!detail.repoUrl || repoFound)) {
1332
1493
  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);
1494
+ const existingBranch = await resolveExistingPrBranch(detail, runBaseDir);
1495
+ worktreeDir = createWorktree(runBaseDir, run.taskId, existingBranch);
1496
+ const rel = path.relative(runBaseDir, worktreeDir);
1336
1497
  if (existingBranch) {
1337
1498
  console.log(` \u{1F333} Worktree from existing PR branch ${existingBranch} \u2192 ${rel}`);
1338
1499
  } else {
@@ -1346,7 +1507,7 @@ async function processRunWithDrain(client, run, scheduler, config) {
1346
1507
  let currentDetail = detail;
1347
1508
  while (current) {
1348
1509
  try {
1349
- await processRun(client, current, config, worktreeDir, currentDetail);
1510
+ await processRun(client, current, config, worktreeDir, currentDetail, runBaseDir, repoFound);
1350
1511
  } catch (err) {
1351
1512
  console.error(`Unhandled error processing run ${current.id}:`, err);
1352
1513
  }
@@ -1363,14 +1524,19 @@ async function processRunWithDrain(client, run, scheduler, config) {
1363
1524
  }
1364
1525
  }
1365
1526
  if (worktreeDir) {
1366
- sessionCache.delete(run.taskId);
1367
1527
  try {
1368
- removeWorktreeSimple(config.workDir, worktreeDir);
1528
+ removeWorktreeSimple(runBaseDir, worktreeDir);
1369
1529
  console.log(` \u{1F9F9} Worktree cleaned up`);
1370
1530
  } catch (err) {
1371
1531
  console.error(` \u26A0 Failed to clean up worktree:`, err);
1372
1532
  }
1373
1533
  }
1534
+ if (detail.repoUrl && !repoFound) {
1535
+ const found = resolveRepoCwd(detail.repoUrl, config.workDir);
1536
+ if (found) {
1537
+ console.log(` \u{1F4C2} Repo cached for future runs: ${found}`);
1538
+ }
1539
+ }
1374
1540
  }
1375
1541
  async function runWithWebSocket(client, config, scheduler) {
1376
1542
  let ConvexClient;
@@ -1395,6 +1561,30 @@ async function runWithWebSocket(client, config, scheduler) {
1395
1561
  handlePendingRuns(runs, scheduler, client, config);
1396
1562
  }
1397
1563
  );
1564
+ let stoppedRunsWarned = false;
1565
+ convex.onUpdate(
1566
+ anyApi.agentWebSocket.stoppedRuns,
1567
+ { token: config.token },
1568
+ (runIds) => {
1569
+ if (!runIds) return;
1570
+ for (const runId of runIds) {
1571
+ if (activeProcesses.has(runId)) {
1572
+ console.log(` \u{1F6D1} Run stopped from UI \u2014 sending stop signal to Claude Code\u2026`);
1573
+ stopActiveProcess(runId);
1574
+ }
1575
+ }
1576
+ },
1577
+ () => {
1578
+ if (!stoppedRunsWarned) {
1579
+ stoppedRunsWarned = true;
1580
+ if (config.verbose) {
1581
+ console.log(
1582
+ `${C.dim}\u2139 stoppedRuns query not available on server \u2014 stop-from-UI disabled${C.reset}`
1583
+ );
1584
+ }
1585
+ }
1586
+ }
1587
+ );
1398
1588
  await new Promise((resolve) => {
1399
1589
  function shutdown() {
1400
1590
  if (!tryBeginClaudeRunnerSignalTeardown()) return;
@@ -1444,6 +1634,16 @@ async function runWithPolling(client, config, scheduler) {
1444
1634
  return;
1445
1635
  }
1446
1636
  while (running) {
1637
+ for (const runId of activeProcesses.keys()) {
1638
+ try {
1639
+ const detail = await client.getRunDetail(runId);
1640
+ if (detail.status === "STOPPED") {
1641
+ console.log(` \u{1F6D1} Run stopped from UI \u2014 sending stop signal to Claude Code\u2026`);
1642
+ stopActiveProcess(runId);
1643
+ }
1644
+ } catch {
1645
+ }
1646
+ }
1447
1647
  try {
1448
1648
  const pending = await client.listPendingRuns();
1449
1649
  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.10",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {