@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.
- package/dist/index.js +136 -29
- 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 {
|
|
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
|
-
|
|
87
|
-
for (let i =
|
|
96
|
+
const agentCommentIndices = [];
|
|
97
|
+
for (let i = 0; i < comments.length; i++) {
|
|
88
98
|
if (comments[i].userId === agentUserId) {
|
|
89
|
-
|
|
90
|
-
break;
|
|
99
|
+
agentCommentIndices.push(i);
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
|
-
|
|
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}
|
|
720
|
+
const branchName = `agent/${name}`;
|
|
707
721
|
try {
|
|
708
722
|
execGit(["fetch", "origin", baseBranch, "--quiet"], repoDir);
|
|
709
723
|
} catch {
|
|
710
724
|
}
|
|
711
|
-
|
|
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)
|
|
1325
|
-
|
|
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
|
|
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:
|
|
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("
|
|
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_")) {
|