@groupchatai/claude-runner 0.2.0 → 0.2.2

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 +67 -23
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -111,11 +111,12 @@ Due: ${new Date(detail.task.dueDate).toLocaleDateString()}`);
111
111
  [
112
112
  "\n---",
113
113
  "RULES:",
114
- "- You may create PRs with `gh pr create`. You may push branches and contribute to existing PRs.",
114
+ "- When you have made code changes, you MUST create a new branch, commit your changes, push to the remote, and open a PR using `gh pr create`. Do NOT skip PR creation \u2014 always open a real PR.",
115
+ "- You may push to existing branches and contribute to existing PRs.",
115
116
  "- NEVER run `gh pr merge`. Do NOT merge any PR.",
116
117
  "- NEVER run `gh pr close`. Do NOT close any PR.",
117
118
  "- NEVER run `git push --force` or `git push -f`. No force pushes.",
118
- "- When you are done, provide a clear summary of what you accomplished. If you created a PR or made commits, include the PR URL and branch name."
119
+ "- When you are done, provide a clear summary of what you accomplished including the PR URL and branch name."
119
120
  ].join("\n")
120
121
  );
121
122
  return parts.join("\n");
@@ -125,6 +126,7 @@ var C = {
125
126
  dim: "\x1B[2m",
126
127
  white: "\x1B[97m",
127
128
  grey: "\x1B[90m",
129
+ lightGrey: "\x1B[37m",
128
130
  green: "\x1B[32m",
129
131
  red: "\x1B[31m",
130
132
  pid: "\x1B[38;2;193;95;60m"
@@ -132,8 +134,21 @@ var C = {
132
134
  function pidTag(pid) {
133
135
  return ` ${C.pid}[pid ${pid}]${C.reset}`;
134
136
  }
137
+ function padForTag(pid) {
138
+ const tagLen = ` [pid ${pid}] `.length;
139
+ return " ".repeat(tagLen);
140
+ }
141
+ function wrapLines(tag, pad, text, color) {
142
+ const lines = text.split("\n").filter((l) => l.trim());
143
+ if (lines.length === 0) return "";
144
+ const first = `${tag} ${color}${lines[0]}${C.reset}`;
145
+ if (lines.length === 1) return first;
146
+ const rest = lines.slice(1).map((l) => `${pad}${color}${l}${C.reset}`);
147
+ return [first, ...rest].join("\n");
148
+ }
135
149
  function formatStreamEvent(event, pid) {
136
150
  const tag = pidTag(pid);
151
+ const pad = padForTag(pid);
137
152
  switch (event.type) {
138
153
  case "system":
139
154
  if (event.subtype === "init") {
@@ -149,18 +164,17 @@ function formatStreamEvent(event, pid) {
149
164
  const parts = [];
150
165
  for (const block of blocks) {
151
166
  if (block.type === "text" && block.text) {
152
- for (const line of block.text.split("\n")) {
153
- if (line.trim()) parts.push(`${tag} ${C.white}${line}${C.reset}`);
154
- }
167
+ const wrapped = wrapLines(tag, pad, block.text, C.white);
168
+ if (wrapped) parts.push(wrapped);
155
169
  } else if (block.type === "tool_use" && block.name) {
156
170
  let detail = block.name;
157
171
  const input = block.input;
158
172
  if (input) {
159
173
  if (typeof input.file_path === "string") {
160
- detail += ` ${C.dim}\u2192${C.grey} ${input.file_path}`;
174
+ detail += ` ${C.grey}\u2192${C.lightGrey} ${input.file_path}`;
161
175
  } else if (typeof input.command === "string") {
162
176
  const cmd = input.command.length > 80 ? input.command.slice(0, 77) + "\u2026" : input.command;
163
- detail += ` ${C.dim}\u2192${C.grey} ${cmd}`;
177
+ detail += ` ${C.grey}\u2192${C.lightGrey} ${cmd}`;
164
178
  }
165
179
  }
166
180
  parts.push(`${tag} ${C.grey}${detail}${C.reset}`);
@@ -177,8 +191,8 @@ function formatStreamEvent(event, pid) {
177
191
  lines.push(`${tag} ${C.dim}cost: $${cost?.toFixed(4)}${C.reset}`);
178
192
  }
179
193
  if (event.result) {
180
- const preview = event.result.length > 120 ? event.result.slice(0, 117) + "\u2026" : event.result;
181
- lines.push(`${tag} ${C.green}\u2713${C.reset} ${C.white}${preview}${C.reset}`);
194
+ const wrapped = wrapLines(tag, pad, `\u2713 ${event.result}`, C.green);
195
+ if (wrapped) lines.push(wrapped);
182
196
  }
183
197
  return lines.length > 0 ? lines.join("\n") : null;
184
198
  }
@@ -350,19 +364,50 @@ function extractCost(stdout) {
350
364
  return void 0;
351
365
  }
352
366
  var GITHUB_PR_URL_RE = /https:\/\/github\.com\/[^\s"'<>]+\/pull\/\d+/g;
353
- function extractPullRequestUrl(stdout) {
367
+ function extractPullRequestUrlFromText(text) {
368
+ const match = text.match(GITHUB_PR_URL_RE);
369
+ if (match) return match[match.length - 1];
370
+ return void 0;
371
+ }
372
+ function extractPullRequestUrlFromOutput(stdout) {
354
373
  const event = findResultEvent(stdout);
355
374
  if (event) {
356
375
  const text = typeof event.result === "string" ? event.result : typeof event.text === "string" ? event.text : "";
357
- const match2 = text.match(GITHUB_PR_URL_RE);
358
- if (match2) return match2[match2.length - 1];
376
+ const found = extractPullRequestUrlFromText(text);
377
+ if (found) return found;
378
+ }
379
+ return extractPullRequestUrlFromText(stdout);
380
+ }
381
+ async function detectPullRequestUrl(workDir) {
382
+ try {
383
+ const branch = await runShellCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"], workDir);
384
+ if (!branch || branch === "main" || branch === "master") return void 0;
385
+ const prUrl = await runShellCommand(
386
+ "gh",
387
+ ["pr", "view", branch, "--json", "url", "--jq", ".url"],
388
+ workDir
389
+ );
390
+ if (prUrl && prUrl.includes("github.com")) return prUrl;
391
+ } catch {
359
392
  }
360
- const match = stdout.match(GITHUB_PR_URL_RE);
361
- if (match) return match[match.length - 1];
362
393
  return void 0;
363
394
  }
395
+ function runShellCommand(cmd, args, cwd) {
396
+ return new Promise((resolve, reject) => {
397
+ const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
398
+ const chunks = [];
399
+ child.stdout?.on("data", (d) => chunks.push(d));
400
+ child.on("error", reject);
401
+ child.on("close", (code) => {
402
+ if (code !== 0) return reject(new Error(`${cmd} exited with ${code}`));
403
+ resolve(Buffer.concat(chunks).toString("utf-8").trim());
404
+ });
405
+ });
406
+ }
364
407
  async function processRun(client, run, config) {
365
- const log = (msg) => console.log(` [${run.id.slice(-8)}] ${msg}`);
408
+ const runTag = ` ${C.pid}[${run.id.slice(-8)}]${C.reset}`;
409
+ const log = (msg) => console.log(`${runTag} ${msg}`);
410
+ const logGreen = (msg) => console.log(`${runTag} ${C.green}${msg}${C.reset}`);
366
411
  const detail = await client.getRunDetail(run.id);
367
412
  const ownerName = run.owner?.name ?? detail.owner?.name ?? "unknown";
368
413
  log(`\u{1F4CB} "${run.taskTitle}" \u2014 delegated by ${ownerName}`);
@@ -408,22 +453,21 @@ async function processRun(client, run, config) {
408
453
  const { process: child, output } = spawnClaudeCode(prompt, config, runOptions);
409
454
  log(`\u{1F916} Claude Code spawned (pid ${child.pid})`);
410
455
  const { stdout, rawOutput, exitCode } = await output;
411
- const pullRequestUrl = extractPullRequestUrl(stdout) ?? extractPullRequestUrl(rawOutput);
456
+ const pullRequestUrl = await detectPullRequestUrl(config.workDir) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
412
457
  if (exitCode !== 0) {
413
458
  const errorMsg = `Claude Code exited with code ${exitCode}:
414
459
  \`\`\`
415
460
  ${stdout.slice(0, 2e3)}
416
461
  \`\`\``;
417
462
  await client.errorRun(run.id, errorMsg, { pullRequestUrl });
418
- log(`\u274C Run errored (exit code ${exitCode})`);
463
+ log(`${C.red}\u274C Run errored (exit code ${exitCode})${C.reset}`);
419
464
  return;
420
465
  }
421
466
  const resultText = extractResultText(stdout);
422
467
  const cost = extractCost(stdout);
423
- const summary = resultText;
424
- await client.completeRun(run.id, summary, { ...cost, pullRequestUrl });
425
- if (pullRequestUrl) log(`\u{1F517} PR: ${pullRequestUrl}`);
426
- log(`\u2705 Run completed`);
468
+ await client.completeRun(run.id, resultText, { ...cost, pullRequestUrl });
469
+ if (pullRequestUrl) logGreen(`\u{1F517} PR: ${pullRequestUrl}`);
470
+ logGreen(`\u2705 Run completed`);
427
471
  } catch (err) {
428
472
  const message = err instanceof Error ? err.message : String(err);
429
473
  const errorBody = `Claude Code runner error:
@@ -433,9 +477,9 @@ ${message.slice(0, 2e3)}
433
477
  try {
434
478
  await client.errorRun(run.id, errorBody);
435
479
  } catch {
436
- log(`\u26A0 Failed to report error to API`);
480
+ log(`${C.dim}\u26A0 Failed to report error to API${C.reset}`);
437
481
  }
438
- log(`\u274C Error: ${message}`);
482
+ log(`${C.red}\u274C Error: ${message}${C.reset}`);
439
483
  }
440
484
  }
441
485
  function loadEnvFile() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupchatai/claude-runner",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {