@groupchatai/claude-runner 0.4.11 → 0.4.13

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 +136 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,8 +2,18 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { spawn, execFileSync } from "child_process";
5
- import { readFileSync, readdirSync, statSync, writeFileSync, existsSync, rmSync } from "fs";
5
+ import {
6
+ readFileSync,
7
+ readdirSync,
8
+ statSync,
9
+ writeFileSync,
10
+ existsSync,
11
+ rmSync,
12
+ mkdirSync,
13
+ unlinkSync
14
+ } from "fs";
6
15
  import path from "path";
16
+ import { homedir } from "os";
7
17
  import { fileURLToPath } from "url";
8
18
  var API_URL = "https://groupchat.ai";
9
19
  var CONVEX_URL = "https://fantastic-jay-464.convex.cloud";
@@ -83,14 +93,19 @@ function buildResumedPrompt(detail, agentUserId) {
83
93
  const comments = detail.activity.filter(
84
94
  (a) => a.type === "comment" && a.body && a.body.trim().length > 0
85
95
  );
86
- let lastAgentCommentIdx = -1;
87
- for (let i = comments.length - 1; i >= 0; i--) {
96
+ const agentCommentIndices = [];
97
+ for (let i = 0; i < comments.length; i++) {
88
98
  if (comments[i].userId === agentUserId) {
89
- lastAgentCommentIdx = i;
90
- break;
99
+ agentCommentIndices.push(i);
91
100
  }
92
101
  }
93
- const newComments = comments.slice(lastAgentCommentIdx + 1).filter((c) => c.userId !== agentUserId);
102
+ let cutoffIdx = -1;
103
+ if (agentCommentIndices.length >= 2) {
104
+ cutoffIdx = agentCommentIndices[agentCommentIndices.length - 2];
105
+ } else if (agentCommentIndices.length === 1) {
106
+ cutoffIdx = agentCommentIndices[0];
107
+ }
108
+ const newComments = comments.slice(cutoffIdx + 1).filter((c) => c.userId !== agentUserId);
94
109
  const parts = [];
95
110
  if (newComments.length > 0) {
96
111
  parts.push("New comments since your last response:");
@@ -200,8 +215,7 @@ Due: ${dueStr}`);
200
215
  ...prRules,
201
216
  "- NEVER run `gh pr merge`. Do NOT merge any PR.",
202
217
  "- NEVER run `gh pr close`. Do NOT close any PR.",
203
- "- NEVER run `git push --force` or `git push -f`. No force pushes.",
204
- "- When you are done, provide a clear summary of what you accomplished."
218
+ "- NEVER run `git push --force` or `git push -f`. No force pushes."
205
219
  ].join("\n")
206
220
  );
207
221
  return parts.join("\n");
@@ -230,13 +244,13 @@ function wrapLines(tag, pad, text, color) {
230
244
  const rest = lines.slice(1).map((l) => `${pad}${color}${l}${C.reset}`);
231
245
  return [first, ...rest].join("\n");
232
246
  }
233
- function formatStreamEvent(event, pid) {
247
+ function formatStreamEvent(event, pid, isResumed) {
234
248
  const tag = pidTag(pid);
235
249
  const pad = padForTag(pid);
236
250
  switch (event.type) {
237
251
  case "system":
238
252
  if (event.subtype === "init") {
239
- let line = `${tag} ${C.dim}session started`;
253
+ let line = `${tag} ${C.dim}session ${isResumed ? "resumed" : "started"}`;
240
254
  if (event.session_id) line += ` (${event.session_id})`;
241
255
  line += C.reset;
242
256
  return line;
@@ -521,7 +535,7 @@ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId, cwdOverrid
521
535
  if (event.type === "result") lastResultJson = trimmed;
522
536
  checkEventForPrUrl(event);
523
537
  if (config.verbose) {
524
- const formatted = formatStreamEvent(event, pid);
538
+ const formatted = formatStreamEvent(event, pid, !!resumeSessionId);
525
539
  if (formatted) console.log(formatted);
526
540
  }
527
541
  } catch {
@@ -703,12 +717,22 @@ function createWorktree(repoDir, taskId, existingBranch) {
703
717
  return worktreePath;
704
718
  }
705
719
  const baseBranch = getDefaultBranch(repoDir);
706
- const branchName = `agent/${name}-${Date.now()}`;
720
+ const branchName = `agent/${name}`;
707
721
  try {
708
722
  execGit(["fetch", "origin", baseBranch, "--quiet"], repoDir);
709
723
  } catch {
710
724
  }
711
- execGit(["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], repoDir);
725
+ let branchExists = false;
726
+ try {
727
+ execGit(["rev-parse", "--verify", branchName], repoDir);
728
+ branchExists = true;
729
+ } catch {
730
+ }
731
+ if (branchExists) {
732
+ execGit(["worktree", "add", worktreePath, branchName], repoDir);
733
+ } else {
734
+ execGit(["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], repoDir);
735
+ }
712
736
  writeFileSync(path.join(worktreePath, ".agent-branch"), branchName, "utf-8");
713
737
  return worktreePath;
714
738
  }
@@ -819,11 +843,6 @@ async function removeWorktree(workDir, info) {
819
843
  }
820
844
  }
821
845
  function removeWorktreeSimple(repoDir, worktreePath) {
822
- let branchName;
823
- try {
824
- branchName = readFileSync(path.join(worktreePath, ".agent-branch"), "utf-8").trim();
825
- } catch {
826
- }
827
846
  try {
828
847
  execGit(["worktree", "remove", worktreePath, "--force"], repoDir);
829
848
  } catch {
@@ -833,12 +852,6 @@ function removeWorktreeSimple(repoDir, worktreePath) {
833
852
  } catch {
834
853
  }
835
854
  }
836
- if (branchName) {
837
- try {
838
- execGit(["branch", "-D", branchName], repoDir);
839
- } catch {
840
- }
841
- }
842
855
  }
843
856
  async function startupSweep(workDir) {
844
857
  const worktrees = await listOurWorktrees(workDir);
@@ -1213,6 +1226,76 @@ ${message.slice(0, 2e3)}
1213
1226
  log(`${C.red}\u274C Error: ${message}${C.reset}`);
1214
1227
  }
1215
1228
  }
1229
+ function globalConfigDir() {
1230
+ return path.join(homedir(), ".config", "groupchat");
1231
+ }
1232
+ function globalConfigPath() {
1233
+ return path.join(globalConfigDir(), "config");
1234
+ }
1235
+ function readGlobalConfig() {
1236
+ try {
1237
+ const contents = readFileSync(globalConfigPath(), "utf-8");
1238
+ const config = {};
1239
+ for (const line of contents.split("\n")) {
1240
+ const trimmed = line.trim();
1241
+ if (!trimmed || trimmed.startsWith("#")) continue;
1242
+ const eqIdx = trimmed.indexOf("=");
1243
+ if (eqIdx === -1) continue;
1244
+ const key = trimmed.slice(0, eqIdx).trim();
1245
+ let value = trimmed.slice(eqIdx + 1).trim();
1246
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1247
+ value = value.slice(1, -1);
1248
+ }
1249
+ config[key] = value;
1250
+ }
1251
+ return config;
1252
+ } catch {
1253
+ return {};
1254
+ }
1255
+ }
1256
+ function writeGlobalConfig(config) {
1257
+ const dir = globalConfigDir();
1258
+ mkdirSync(dir, { recursive: true });
1259
+ const lines = Object.entries(config).map(([k, v]) => `${k}=${v}`);
1260
+ writeFileSync(globalConfigPath(), lines.join("\n") + "\n", "utf-8");
1261
+ }
1262
+ function handleLogin(token) {
1263
+ if (!token) {
1264
+ console.error("Usage: npx @groupchatai/claude-runner login <gca_token>");
1265
+ process.exit(1);
1266
+ }
1267
+ if (!token.startsWith("gca_")) {
1268
+ console.error(`Error: Token must start with gca_ (got "${token.slice(0, 8)}\u2026")`);
1269
+ process.exit(1);
1270
+ }
1271
+ const config = readGlobalConfig();
1272
+ config.GCA_TOKEN = token;
1273
+ writeGlobalConfig(config);
1274
+ console.log(`\u2705 Token saved to ${globalConfigPath()}`);
1275
+ console.log(` You can now run the agent from any directory.`);
1276
+ }
1277
+ function handleLogout() {
1278
+ const configFile = globalConfigPath();
1279
+ if (!existsSync(configFile)) {
1280
+ console.log("No saved token found.");
1281
+ return;
1282
+ }
1283
+ const config = readGlobalConfig();
1284
+ if (!config.GCA_TOKEN) {
1285
+ console.log("No saved token found.");
1286
+ return;
1287
+ }
1288
+ delete config.GCA_TOKEN;
1289
+ if (Object.keys(config).length === 0) {
1290
+ try {
1291
+ unlinkSync(configFile);
1292
+ } catch {
1293
+ }
1294
+ } else {
1295
+ writeGlobalConfig(config);
1296
+ }
1297
+ console.log("\u2705 Token removed.");
1298
+ }
1216
1299
  function loadEnvFile() {
1217
1300
  const candidates = [".env.local", ".env"];
1218
1301
  for (const file of candidates) {
@@ -1321,8 +1404,10 @@ function showHelp() {
1321
1404
  Usage: npx @groupchatai/claude-runner [command] [options]
1322
1405
 
1323
1406
  Commands:
1324
- (default) Start the agent runner
1325
- cleanup Interactively review and remove stale worktrees
1407
+ (default) Start the agent runner
1408
+ login <gca_token> Save your agent token for use from any directory
1409
+ logout Remove saved agent token
1410
+ cleanup Interactively review and remove stale worktrees
1326
1411
 
1327
1412
  Options:
1328
1413
  --work-dir <path> Repo directory for Claude Code to work in (default: cwd)
@@ -1338,6 +1423,12 @@ Options:
1338
1423
  -h, --help Show this help message
1339
1424
  -v, -version, --version Print version and exit
1340
1425
 
1426
+ Token resolution order:
1427
+ 1. --token flag
1428
+ 2. GCA_TOKEN env var
1429
+ 3. .env.local / .env in current directory
1430
+ 4. ~/.config/groupchat/config (saved via 'login' command)
1431
+
1341
1432
  Environment variables:
1342
1433
  GCA_TOKEN Agent token (gca_...)
1343
1434
  GCA_API_URL API URL override
@@ -1347,9 +1438,20 @@ Environment variables:
1347
1438
  }
1348
1439
  function parseArgs() {
1349
1440
  loadEnvFile();
1350
- const args = process.argv.slice(2);
1441
+ const rawArgs = process.argv.slice(2);
1442
+ if (rawArgs[0] === "login") {
1443
+ handleLogin(rawArgs[1] ?? "");
1444
+ process.exit(0);
1445
+ }
1446
+ if (rawArgs[0] === "logout") {
1447
+ handleLogout();
1448
+ process.exit(0);
1449
+ }
1450
+ const globalConfig = readGlobalConfig();
1451
+ const resolvedToken = process.env.GCA_TOKEN || globalConfig.GCA_TOKEN || "";
1452
+ const args = rawArgs;
1351
1453
  const config = {
1352
- token: process.env.GCA_TOKEN ?? "",
1454
+ token: resolvedToken,
1353
1455
  apiUrl: process.env.GCA_API_URL ?? API_URL,
1354
1456
  convexUrl: process.env.GCA_CONVEX_URL ?? CONVEX_URL,
1355
1457
  workDir: process.cwd(),
@@ -1415,7 +1517,12 @@ function parseArgs() {
1415
1517
  if (config.command !== "cleanup") {
1416
1518
  if (!config.token) {
1417
1519
  console.error("Error: No agent token found.");
1418
- console.error(" Add GCA_TOKEN=gca_... to .env.local, or pass --token gca_...");
1520
+ console.error("");
1521
+ console.error(" To save your token globally (recommended):");
1522
+ console.error(" npx @groupchatai/claude-runner login gca_...");
1523
+ console.error("");
1524
+ console.error(" Or pass it directly:");
1525
+ console.error(" --token gca_... or GCA_TOKEN=gca_... in .env.local");
1419
1526
  process.exit(1);
1420
1527
  }
1421
1528
  if (!config.token.startsWith("gca_")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupchatai/claude-runner",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {