@groupchatai/claude-runner 0.2.3 → 0.3.1

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 +123 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -240,11 +240,15 @@ var ALLOWED_TOOLS = [
240
240
  "TodoWrite",
241
241
  "NotebookEdit"
242
242
  ];
243
- function spawnClaudeCode(prompt, config, runOptions) {
243
+ function spawnClaudeCode(prompt, config, runOptions, resumeSessionId) {
244
244
  const format = config.verbose ? "stream-json" : "json";
245
- const args = [
246
- "-p",
247
- prompt,
245
+ const args = [];
246
+ if (resumeSessionId) {
247
+ args.push("--resume", resumeSessionId, "-p", prompt);
248
+ } else {
249
+ args.push("-p", prompt);
250
+ }
251
+ args.push(
248
252
  "--output-format",
249
253
  format,
250
254
  "--verbose",
@@ -252,7 +256,7 @@ function spawnClaudeCode(prompt, config, runOptions) {
252
256
  "default",
253
257
  "--allowedTools",
254
258
  ...ALLOWED_TOOLS
255
- ];
259
+ );
256
260
  const model = runOptions?.model ?? config.model;
257
261
  if (model) {
258
262
  args.push("--model", model);
@@ -263,61 +267,97 @@ function spawnClaudeCode(prompt, config, runOptions) {
263
267
  env: { ...process.env }
264
268
  });
265
269
  const pid = child.pid ?? 0;
266
- const output = new Promise(
267
- (resolve, reject) => {
268
- const chunks = [];
269
- const errChunks = [];
270
- let lineBuf = "";
271
- let lastResultJson = "";
272
- child.stdout?.on("data", (data) => {
273
- chunks.push(data);
274
- if (!config.verbose) return;
275
- lineBuf += data.toString("utf-8");
276
- const lines = lineBuf.split("\n");
277
- lineBuf = lines.pop() ?? "";
278
- for (const line of lines) {
279
- const trimmed = line.trim();
280
- if (!trimmed) continue;
281
- try {
282
- const event = JSON.parse(trimmed);
283
- if (event.type === "result") lastResultJson = trimmed;
270
+ const output = new Promise((resolve, reject) => {
271
+ const chunks = [];
272
+ const errChunks = [];
273
+ let lineBuf = "";
274
+ let lastResultJson = "";
275
+ let capturedSessionId;
276
+ let capturedPrUrl;
277
+ function checkEventForPrUrl(event) {
278
+ if (capturedPrUrl) return;
279
+ const texts = [];
280
+ if (event.message?.content) {
281
+ for (const block of event.message.content) {
282
+ if (block.text) texts.push(block.text);
283
+ }
284
+ }
285
+ if (event.content_block?.text) texts.push(event.content_block.text);
286
+ if (typeof event.result === "string") texts.push(event.result);
287
+ const combined = texts.join("\n");
288
+ const match = combined.match(GITHUB_PR_URL_RE);
289
+ if (match) capturedPrUrl = match[match.length - 1];
290
+ }
291
+ child.stdout?.on("data", (data) => {
292
+ chunks.push(data);
293
+ const text = data.toString("utf-8");
294
+ lineBuf += text;
295
+ const lines = lineBuf.split("\n");
296
+ lineBuf = lines.pop() ?? "";
297
+ for (const line of lines) {
298
+ const trimmed = line.trim();
299
+ if (!trimmed) continue;
300
+ try {
301
+ const event = JSON.parse(trimmed);
302
+ if (event.type === "system" && event.subtype === "init" && event.session_id) {
303
+ capturedSessionId = event.session_id;
304
+ }
305
+ if (event.type === "result") lastResultJson = trimmed;
306
+ checkEventForPrUrl(event);
307
+ if (config.verbose) {
284
308
  const formatted = formatStreamEvent(event, pid);
285
309
  if (formatted) console.log(formatted);
286
- } catch {
310
+ }
311
+ } catch {
312
+ if (config.verbose) {
287
313
  console.log(`${pidTag(pid)} ${C.dim}${trimmed}${C.reset}`);
288
314
  }
289
315
  }
290
- });
291
- child.stderr?.on("data", (data) => {
292
- errChunks.push(data);
293
- if (config.verbose) {
294
- const text = data.toString("utf-8").trimEnd();
295
- if (text) console.error(`${pidTag(pid)} ${C.dim}${text}${C.reset}`);
296
- }
297
- });
298
- child.on("error", (err) => reject(new Error(`Failed to spawn claude: ${err.message}`)));
299
- child.on("close", (code) => {
300
- if (config.verbose && lineBuf.trim()) {
301
- try {
302
- const event = JSON.parse(lineBuf.trim());
303
- if (event.type === "result") lastResultJson = lineBuf.trim();
316
+ }
317
+ });
318
+ child.stderr?.on("data", (data) => {
319
+ errChunks.push(data);
320
+ if (config.verbose) {
321
+ const text = data.toString("utf-8").trimEnd();
322
+ if (text) console.error(`${pidTag(pid)} ${C.dim}${text}${C.reset}`);
323
+ }
324
+ });
325
+ child.on("error", (err) => reject(new Error(`Failed to spawn claude: ${err.message}`)));
326
+ child.on("close", (code) => {
327
+ if (lineBuf.trim()) {
328
+ try {
329
+ const event = JSON.parse(lineBuf.trim());
330
+ if (event.type === "system" && event.subtype === "init" && event.session_id) {
331
+ capturedSessionId = event.session_id;
332
+ }
333
+ if (event.type === "result") lastResultJson = lineBuf.trim();
334
+ checkEventForPrUrl(event);
335
+ if (config.verbose) {
304
336
  const formatted = formatStreamEvent(event, pid);
305
337
  if (formatted) console.log(formatted);
306
- } catch {
338
+ }
339
+ } catch {
340
+ if (config.verbose) {
307
341
  console.log(`${pidTag(pid)} ${C.dim}${lineBuf.trim()}${C.reset}`);
308
342
  }
309
343
  }
310
- const rawOutput = Buffer.concat(chunks).toString("utf-8");
311
- const stdout = config.verbose ? lastResultJson || rawOutput : rawOutput;
312
- const stderr = Buffer.concat(errChunks).toString("utf-8");
313
- if (code !== 0 && stdout.trim().length === 0) {
314
- reject(new Error(`claude exited with code ${code}: ${stderr}`));
315
- } else {
316
- resolve({ stdout, rawOutput, exitCode: code ?? 0 });
317
- }
318
- });
319
- }
320
- );
344
+ }
345
+ const rawOutput = Buffer.concat(chunks).toString("utf-8");
346
+ const stdout = config.verbose ? lastResultJson || rawOutput : rawOutput;
347
+ const stderr = Buffer.concat(errChunks).toString("utf-8");
348
+ if (code !== 0 && stdout.trim().length === 0) {
349
+ reject(new Error(`claude exited with code ${code}: ${stderr}`));
350
+ } else {
351
+ resolve({
352
+ stdout,
353
+ rawOutput,
354
+ exitCode: code ?? 0,
355
+ sessionId: capturedSessionId,
356
+ streamPrUrl: capturedPrUrl
357
+ });
358
+ }
359
+ });
360
+ });
321
361
  return { process: child, output };
322
362
  }
323
363
  function findResultEvent(stdout) {
@@ -404,6 +444,7 @@ function runShellCommand(cmd, args, cwd) {
404
444
  });
405
445
  });
406
446
  }
447
+ var sessionCache = /* @__PURE__ */ new Map();
407
448
  async function processRun(client, run, config) {
408
449
  const runTag = ` ${C.pid}[${run.id.slice(-8)}]${C.reset}`;
409
450
  const log = (msg) => console.log(`${runTag} ${msg}`);
@@ -446,14 +487,41 @@ async function processRun(client, run, config) {
446
487
  });
447
488
  await new Promise((resolve) => git.on("close", () => resolve()));
448
489
  }
490
+ const cachedSessionId = sessionCache.get(run.taskId);
491
+ const isFollowUp = cachedSessionId !== void 0;
492
+ const effectivePrompt = isFollowUp ? detail.prompt : prompt;
493
+ const resumeSession = isFollowUp ? cachedSessionId : void 0;
449
494
  if (config.verbose) {
450
- log(`\u{1F4DD} Prompt (${prompt.length} chars)`);
495
+ if (isFollowUp) {
496
+ log(`\u{1F504} Resuming session ${cachedSessionId.slice(0, 12)}\u2026`);
497
+ }
498
+ log(`\u{1F4DD} Prompt (${effectivePrompt.length} chars)`);
451
499
  if (effectiveModel) log(`\u{1F9E0} Model: ${effectiveModel}`);
452
500
  }
453
- const { process: child, output } = spawnClaudeCode(prompt, config, runOptions);
454
- log(`\u{1F916} Claude Code spawned (pid ${child.pid})`);
455
- const { stdout, rawOutput, exitCode } = await output;
456
- const pullRequestUrl = await detectPullRequestUrl(config.workDir) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
501
+ const { process: child, output } = spawnClaudeCode(
502
+ effectivePrompt,
503
+ config,
504
+ runOptions,
505
+ resumeSession
506
+ );
507
+ log(`\u{1F916} Claude Code spawned (pid ${child.pid})${isFollowUp ? " (follow-up)" : ""}`);
508
+ let { stdout, rawOutput, exitCode, sessionId, streamPrUrl } = await output;
509
+ if (exitCode !== 0 && isFollowUp) {
510
+ log(`\u26A0 Session resume failed, retrying with fresh session\u2026`);
511
+ sessionCache.delete(run.taskId);
512
+ const retry = spawnClaudeCode(prompt, config, runOptions);
513
+ log(`\u{1F916} Claude Code spawned (pid ${retry.process.pid}) (fresh)`);
514
+ const retryResult = await retry.output;
515
+ stdout = retryResult.stdout;
516
+ rawOutput = retryResult.rawOutput;
517
+ exitCode = retryResult.exitCode;
518
+ sessionId = retryResult.sessionId;
519
+ streamPrUrl = retryResult.streamPrUrl;
520
+ }
521
+ if (sessionId) {
522
+ sessionCache.set(run.taskId, sessionId);
523
+ }
524
+ const pullRequestUrl = streamPrUrl ?? await detectPullRequestUrl(config.workDir) ?? extractPullRequestUrlFromOutput(stdout) ?? extractPullRequestUrlFromOutput(rawOutput);
457
525
  if (exitCode !== 0) {
458
526
  const errorMsg = `Claude Code exited with code ${exitCode}:
459
527
  \`\`\`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupchatai/claude-runner",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Run GroupChat AI agent tasks locally with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {