pullfrog 0.0.203 → 0.0.204

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/cli.mjs CHANGED
@@ -108099,14 +108099,14 @@ var providers = {
108099
108099
  models: {
108100
108100
  grok: {
108101
108101
  displayName: "Grok",
108102
- resolve: "xai/grok-4",
108103
- openRouterResolve: "openrouter/x-ai/grok-4",
108102
+ resolve: "xai/grok-4.3",
108103
+ openRouterResolve: "openrouter/x-ai/grok-4.3",
108104
108104
  preferred: true
108105
108105
  },
108106
108106
  "grok-fast": {
108107
108107
  displayName: "Grok Fast",
108108
- resolve: "xai/grok-4-fast",
108109
- openRouterResolve: "openrouter/x-ai/grok-4-fast"
108108
+ resolve: "xai/grok-4-1-fast",
108109
+ openRouterResolve: "openrouter/x-ai/grok-4.1-fast"
108110
108110
  },
108111
108111
  "grok-code-fast": {
108112
108112
  displayName: "Grok Code Fast",
@@ -108313,8 +108313,8 @@ var providers = {
108313
108313
  },
108314
108314
  grok: {
108315
108315
  displayName: "Grok",
108316
- resolve: "openrouter/x-ai/grok-4",
108317
- openRouterResolve: "openrouter/x-ai/grok-4"
108316
+ resolve: "openrouter/x-ai/grok-4.3",
108317
+ openRouterResolve: "openrouter/x-ai/grok-4.3"
108318
108318
  },
108319
108319
  "deepseek-pro": {
108320
108320
  displayName: "DeepSeek Pro",
@@ -108583,6 +108583,109 @@ function aggregateUsage(entries) {
108583
108583
  return out;
108584
108584
  }
108585
108585
 
108586
+ // utils/progressComment.ts
108587
+ function parseProgressComment(raw2) {
108588
+ if (!raw2?.id) return void 0;
108589
+ const id = parseInt(raw2.id, 10);
108590
+ if (Number.isNaN(id) || id <= 0) return void 0;
108591
+ return { id, type: raw2.type };
108592
+ }
108593
+ async function getProgressComment(ctx, comment) {
108594
+ const result = await (comment.type === "review" ? ctx.octokit.rest.pulls.getReviewComment({
108595
+ owner: ctx.owner,
108596
+ repo: ctx.repo,
108597
+ comment_id: comment.id
108598
+ }) : ctx.octokit.rest.issues.getComment({
108599
+ owner: ctx.owner,
108600
+ repo: ctx.repo,
108601
+ comment_id: comment.id
108602
+ }));
108603
+ return {
108604
+ id: result.data.id,
108605
+ body: result.data.body ?? void 0,
108606
+ html_url: result.data.html_url
108607
+ };
108608
+ }
108609
+ async function updateProgressComment(ctx, comment, body) {
108610
+ const result = await (comment.type === "review" ? ctx.octokit.rest.pulls.updateReviewComment({
108611
+ owner: ctx.owner,
108612
+ repo: ctx.repo,
108613
+ comment_id: comment.id,
108614
+ body
108615
+ }) : ctx.octokit.rest.issues.updateComment({
108616
+ owner: ctx.owner,
108617
+ repo: ctx.repo,
108618
+ comment_id: comment.id,
108619
+ body
108620
+ }));
108621
+ return {
108622
+ id: result.data.id,
108623
+ body: result.data.body ?? void 0,
108624
+ html_url: result.data.html_url,
108625
+ node_id: result.data.node_id
108626
+ };
108627
+ }
108628
+ async function deleteProgressCommentApi(ctx, comment) {
108629
+ if (comment.type === "review") {
108630
+ await ctx.octokit.rest.pulls.deleteReviewComment({
108631
+ owner: ctx.owner,
108632
+ repo: ctx.repo,
108633
+ comment_id: comment.id
108634
+ });
108635
+ return;
108636
+ }
108637
+ await ctx.octokit.rest.issues.deleteComment({
108638
+ owner: ctx.owner,
108639
+ repo: ctx.repo,
108640
+ comment_id: comment.id
108641
+ });
108642
+ }
108643
+ async function createLeapingProgressComment(ctx, target, body) {
108644
+ if (target.kind === "reviewReply") {
108645
+ try {
108646
+ const result2 = await ctx.octokit.rest.pulls.createReplyForReviewComment({
108647
+ owner: ctx.owner,
108648
+ repo: ctx.repo,
108649
+ pull_number: target.pullNumber,
108650
+ comment_id: target.replyToCommentId,
108651
+ body
108652
+ });
108653
+ return {
108654
+ comment: { id: result2.data.id, type: "review" },
108655
+ body: result2.data.body ?? void 0,
108656
+ html_url: result2.data.html_url
108657
+ };
108658
+ } catch (error49) {
108659
+ console.warn(
108660
+ `[progressComment] review reply failed (parent ${target.replyToCommentId} on PR #${target.pullNumber}), falling back to issue comment:`,
108661
+ error49
108662
+ );
108663
+ const fallback = await ctx.octokit.rest.issues.createComment({
108664
+ owner: ctx.owner,
108665
+ repo: ctx.repo,
108666
+ issue_number: target.pullNumber,
108667
+ body
108668
+ });
108669
+ return {
108670
+ comment: { id: fallback.data.id, type: "issue" },
108671
+ body: fallback.data.body ?? void 0,
108672
+ html_url: fallback.data.html_url
108673
+ };
108674
+ }
108675
+ }
108676
+ const result = await ctx.octokit.rest.issues.createComment({
108677
+ owner: ctx.owner,
108678
+ repo: ctx.repo,
108679
+ issue_number: target.issueNumber,
108680
+ body
108681
+ });
108682
+ return {
108683
+ comment: { id: result.data.id, type: "issue" },
108684
+ body: result.data.body ?? void 0,
108685
+ html_url: result.data.html_url
108686
+ };
108687
+ }
108688
+
108586
108689
  // node_modules/.pnpm/@toon-format+toon@1.4.0/node_modules/@toon-format/toon/dist/index.mjs
108587
108690
  var LIST_ITEM_MARKER = "-";
108588
108691
  var LIST_ITEM_PREFIX = "- ";
@@ -109249,6 +109352,7 @@ async function reportProgress(ctx, params) {
109249
109352
  }
109250
109353
  const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
109251
109354
  const isPlanMode = ctx.toolState.selectedMode === "Plan";
109355
+ const apiCtx = { octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name };
109252
109356
  if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === void 0) {
109253
109357
  log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
109254
109358
  }
@@ -109258,86 +109362,74 @@ async function reportProgress(ctx, params) {
109258
109362
  const bodyWithoutFooter = stripExistingFooter(body);
109259
109363
  const footer = buildCommentFooter(ctx, customParts);
109260
109364
  const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
109261
- const result2 = await ctx.octokit.rest.issues.updateComment({
109262
- owner: ctx.repo.owner,
109263
- repo: ctx.repo.name,
109264
- comment_id: commentId,
109265
- body: bodyWithFooter
109266
- });
109365
+ const result = await updateProgressComment(
109366
+ apiCtx,
109367
+ { id: commentId, type: "issue" },
109368
+ bodyWithFooter
109369
+ );
109267
109370
  ctx.toolState.wasUpdated = true;
109268
- if (isPlanMode && result2.data.node_id) {
109269
- await patchWorkflowRunFields(ctx, { planCommentNodeId: result2.data.node_id });
109371
+ if (isPlanMode && result.node_id) {
109372
+ await patchWorkflowRunFields(ctx, { planCommentNodeId: result.node_id });
109270
109373
  }
109271
109374
  return {
109272
- commentId: result2.data.id,
109273
- url: result2.data.html_url,
109274
- body: result2.data.body || "",
109375
+ commentId: result.id,
109376
+ url: result.html_url,
109377
+ body: result.body || "",
109275
109378
  action: "updated"
109276
109379
  };
109277
109380
  }
109278
- const existingCommentId = ctx.toolState.progressCommentId;
109279
- if (existingCommentId) {
109280
- const customParts = isPlanMode && issueNumber !== void 0 ? [buildImplementPlanLink(ctx, issueNumber, existingCommentId)] : void 0;
109381
+ const existingComment = ctx.toolState.progressComment;
109382
+ if (existingComment) {
109383
+ const customParts = isPlanMode && issueNumber !== void 0 ? [buildImplementPlanLink(ctx, issueNumber, existingComment.id)] : void 0;
109281
109384
  const bodyWithoutFooter = stripExistingFooter(body);
109282
109385
  const footer = buildCommentFooter(ctx, customParts);
109283
109386
  const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
109284
- const result2 = await ctx.octokit.rest.issues.updateComment({
109285
- owner: ctx.repo.owner,
109286
- repo: ctx.repo.name,
109287
- comment_id: existingCommentId,
109288
- body: bodyWithFooter
109289
- });
109387
+ const result = await updateProgressComment(apiCtx, existingComment, bodyWithFooter);
109290
109388
  ctx.toolState.wasUpdated = true;
109291
- if (isPlanMode && result2.data.node_id) {
109292
- await patchWorkflowRunFields(ctx, { planCommentNodeId: result2.data.node_id });
109389
+ if (isPlanMode && result.node_id) {
109390
+ await patchWorkflowRunFields(ctx, { planCommentNodeId: result.node_id });
109293
109391
  }
109294
109392
  return {
109295
- commentId: result2.data.id,
109296
- url: result2.data.html_url,
109297
- body: result2.data.body || "",
109393
+ commentId: result.id,
109394
+ url: result.html_url,
109395
+ body: result.body || "",
109298
109396
  action: "updated"
109299
109397
  };
109300
109398
  }
109301
- if (existingCommentId === null) {
109399
+ if (existingComment === null) {
109302
109400
  return { body, action: "skipped" };
109303
109401
  }
109304
109402
  if (issueNumber === void 0) {
109305
109403
  return { body, action: "skipped" };
109306
109404
  }
109307
109405
  const initialBody = addFooter(ctx, body);
109308
- const result = await ctx.octokit.rest.issues.createComment({
109309
- owner: ctx.repo.owner,
109310
- repo: ctx.repo.name,
109311
- issue_number: issueNumber,
109312
- body: initialBody
109313
- });
109314
- ctx.toolState.progressCommentId = result.data.id;
109406
+ const created = await createLeapingProgressComment(
109407
+ apiCtx,
109408
+ { kind: "issue", issueNumber },
109409
+ initialBody
109410
+ );
109411
+ ctx.toolState.progressComment = created.comment;
109315
109412
  ctx.toolState.wasUpdated = true;
109316
109413
  if (isPlanMode) {
109317
- const customParts = [buildImplementPlanLink(ctx, issueNumber, result.data.id)];
109414
+ const customParts = [buildImplementPlanLink(ctx, issueNumber, created.comment.id)];
109318
109415
  const bodyWithoutFooter = stripExistingFooter(body);
109319
109416
  const footer = buildCommentFooter(ctx, customParts);
109320
109417
  const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
109321
- const updateResult = await ctx.octokit.rest.issues.updateComment({
109322
- owner: ctx.repo.owner,
109323
- repo: ctx.repo.name,
109324
- comment_id: result.data.id,
109325
- body: bodyWithPlanLink
109326
- });
109327
- if (updateResult.data.node_id) {
109328
- await patchWorkflowRunFields(ctx, { planCommentNodeId: updateResult.data.node_id });
109418
+ const updateResult = await updateProgressComment(apiCtx, created.comment, bodyWithPlanLink);
109419
+ if (updateResult.node_id) {
109420
+ await patchWorkflowRunFields(ctx, { planCommentNodeId: updateResult.node_id });
109329
109421
  }
109330
109422
  return {
109331
- commentId: updateResult.data.id,
109332
- url: updateResult.data.html_url,
109333
- body: updateResult.data.body || "",
109423
+ commentId: updateResult.id,
109424
+ url: updateResult.html_url,
109425
+ body: updateResult.body || "",
109334
109426
  action: "created"
109335
109427
  };
109336
109428
  }
109337
109429
  return {
109338
- commentId: result.data.id,
109339
- url: result.data.html_url,
109340
- body: result.data.body || "",
109430
+ commentId: created.comment.id,
109431
+ url: created.html_url,
109432
+ body: created.body || "",
109341
109433
  action: "created"
109342
109434
  };
109343
109435
  }
@@ -109382,23 +109474,22 @@ ${collapsible}`;
109382
109474
  });
109383
109475
  }
109384
109476
  async function deleteProgressComment(ctx) {
109385
- const existingCommentId = ctx.toolState.progressCommentId;
109386
- if (!existingCommentId) {
109477
+ const existing = ctx.toolState.progressComment;
109478
+ if (!existing) {
109387
109479
  return false;
109388
109480
  }
109389
109481
  try {
109390
- await ctx.octokit.rest.issues.deleteComment({
109391
- owner: ctx.repo.owner,
109392
- repo: ctx.repo.name,
109393
- comment_id: existingCommentId
109394
- });
109482
+ await deleteProgressCommentApi(
109483
+ { octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name },
109484
+ existing
109485
+ );
109395
109486
  } catch (error49) {
109396
109487
  if (error49 instanceof Error && error49.message.includes("Not Found")) {
109397
109488
  } else {
109398
109489
  throw error49;
109399
109490
  }
109400
109491
  }
109401
- ctx.toolState.progressCommentId = null;
109492
+ ctx.toolState.progressComment = null;
109402
109493
  return true;
109403
109494
  }
109404
109495
  var ReplyToReviewComment = type({
@@ -142479,7 +142570,7 @@ var import_semver = __toESM(require_semver2(), 1);
142479
142570
  // package.json
142480
142571
  var package_default = {
142481
142572
  name: "pullfrog",
142482
- version: "0.0.203",
142573
+ version: "0.0.204",
142483
142574
  type: "module",
142484
142575
  bin: {
142485
142576
  pullfrog: "dist/cli.mjs",
@@ -143016,8 +143107,13 @@ async function $git(subcommand, args2, options) {
143016
143107
  }
143017
143108
  if (result.exitCode !== 0) {
143018
143109
  const stderr = result.stderr.trim();
143019
- log.info(`git ${subcommand} failed: ${stderr}`);
143020
- throw new Error(`git ${subcommand} failed: ${stderr}`);
143110
+ const stdout = result.stdout.trim();
143111
+ const detail = stderr && stdout ? `${stderr}
143112
+ --- stdout ---
143113
+ ${stdout}` : stderr || stdout || "(no output)";
143114
+ const message = `git ${subcommand} failed (exit ${result.exitCode}): ${detail}`;
143115
+ log.info(message);
143116
+ throw new Error(message);
143021
143117
  }
143022
143118
  return {
143023
143119
  stdout: result.stdout.trim(),
@@ -143294,6 +143390,34 @@ var PushBranch = type({
143294
143390
  branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
143295
143391
  force: type.boolean.describe("Force push (use with caution)").default(false)
143296
143392
  });
143393
+ var CONCURRENT_PUSH_PATTERNS = ["fetch first", "non-fast-forward", "cannot lock ref"];
143394
+ var TRANSIENT_PATTERNS = [
143395
+ /RPC failed/i,
143396
+ /early EOF/,
143397
+ /the remote end hung up unexpectedly/,
143398
+ /Connection reset/i,
143399
+ /Could not resolve host/i,
143400
+ /Operation timed out/i,
143401
+ /HTTP\/2 stream \d+ was not closed cleanly/i,
143402
+ /unexpected disconnect while reading sideband packet/i,
143403
+ // libcurl HTTP 5xx surfaced by git over https. matches both the
143404
+ // libcurl-style "The requested URL returned error: 502" and the more
143405
+ // recent "HTTP 502" wording. most 4xx is intentionally excluded —
143406
+ // 401/403/404 indicate auth/permission problems that are not
143407
+ // retry-safe — but 429 (rate-limited / abuse detection) IS retry-safe
143408
+ // and GitHub occasionally surfaces it on git push, so it's included
143409
+ // explicitly below.
143410
+ /HTTP 5\d\d/,
143411
+ /returned error: 5\d\d/i,
143412
+ /HTTP 429/,
143413
+ /returned error: 429/i
143414
+ ];
143415
+ function classifyPushError(msg) {
143416
+ if (CONCURRENT_PUSH_PATTERNS.some((p2) => msg.includes(p2))) return "concurrent-push";
143417
+ if (TRANSIENT_PATTERNS.some((p2) => p2.test(msg))) return "transient";
143418
+ return "unknown";
143419
+ }
143420
+ var TRANSIENT_RETRY_DELAYS_MS = [2e3, 5e3];
143297
143421
  function PushBranchTool(ctx) {
143298
143422
  const defaultBranch = ctx.repo.data.default_branch || "main";
143299
143423
  const pushPermission = ctx.payload.push;
@@ -143344,25 +143468,48 @@ ${postHookStatus}`
143344
143468
  if (force) {
143345
143469
  log.warning(`force pushing - this will overwrite remote history`);
143346
143470
  }
143347
- try {
143348
- await $git("push", pushArgs, {
143349
- token: ctx.gitToken
143350
- });
143351
- } catch (err) {
143352
- const msg = err instanceof Error ? err.message : String(err);
143353
- if (msg.includes("fetch first") || msg.includes("non-fast-forward")) {
143354
- const integrateStep = ctx.payload.shell === "disabled" ? `2. use the git tool to merge the remote branch into yours: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] })` : `2. use the git tool to rebase or merge your changes on top: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] }) (or 'rebase')`;
143355
- throw new Error(
143356
- `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally.
143471
+ let lastErr;
143472
+ let pushed = false;
143473
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
143474
+ try {
143475
+ await $git("push", pushArgs, {
143476
+ token: ctx.gitToken
143477
+ });
143478
+ if (attempt > 0) {
143479
+ log.info(`push succeeded on attempt ${attempt + 1}`);
143480
+ }
143481
+ pushed = true;
143482
+ break;
143483
+ } catch (err) {
143484
+ lastErr = err;
143485
+ const msg = err instanceof Error ? err.message : String(err);
143486
+ const kind = classifyPushError(msg);
143487
+ if (kind === "concurrent-push") {
143488
+ const integrateStep = ctx.payload.shell === "disabled" ? `2. use the git tool to merge the remote branch into yours: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] })` : `2. use the git tool to rebase or merge your changes on top: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] }) (or 'rebase')`;
143489
+ throw new Error(
143490
+ `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
143357
143491
 
143358
143492
  to resolve this:
143359
143493
  1. use git_fetch to fetch the remote branch: git_fetch({ ref: "${pushDest.remoteBranch}" })
143360
143494
  ${integrateStep}
143361
143495
  3. resolve any merge conflicts if needed
143362
143496
  4. retry push_branch`
143363
- );
143497
+ );
143498
+ }
143499
+ if (kind === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
143500
+ const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
143501
+ const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
143502
+ log.info(
143503
+ `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
143504
+ );
143505
+ await new Promise((r) => setTimeout(r, delay2));
143506
+ continue;
143507
+ }
143508
+ throw err;
143364
143509
  }
143365
- throw err;
143510
+ }
143511
+ if (!pushed) {
143512
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
143366
143513
  }
143367
143514
  return {
143368
143515
  success: true,
@@ -145416,35 +145563,20 @@ async function getReviewThreads(input) {
145416
145563
  const username = input.approvedBy;
145417
145564
  return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
145418
145565
  }
145419
- async function getReviewData(input) {
145420
- const [review, threads] = await Promise.all([
145421
- input.octokit.rest.pulls.getReview({
145422
- owner: input.owner,
145423
- repo: input.name,
145424
- pull_number: input.pullNumber,
145425
- review_id: input.reviewId
145426
- }),
145427
- getReviewThreads(input)
145428
- ]);
145429
- const rawReviewBody = review.data.body;
145566
+ function formatReviewData(input) {
145567
+ const rawReviewBody = input.review.body;
145430
145568
  const reviewBody = rawReviewBody ? stripExistingFooter(rawReviewBody) : "";
145431
- const reviewer = review.data.user?.login ?? "unknown";
145432
- if (threads.length === 0 && !reviewBody) return void 0;
145569
+ const reviewer = input.review.user?.login ?? "unknown";
145570
+ if (input.threads.length === 0 && !reviewBody) return void 0;
145433
145571
  let threadBlocks = [];
145434
- if (threads.length > 0) {
145435
- const prFiles = await input.octokit.paginate(input.octokit.rest.pulls.listFiles, {
145436
- owner: input.owner,
145437
- repo: input.name,
145438
- pull_number: input.pullNumber,
145439
- per_page: 100
145440
- });
145572
+ if (input.threads.length > 0) {
145441
145573
  const filePatchMap = /* @__PURE__ */ new Map();
145442
- for (const file2 of prFiles) {
145574
+ for (const file2 of input.prFiles) {
145443
145575
  if (file2.patch) {
145444
145576
  filePatchMap.set(file2.filename, parseFilePatches(file2.patch));
145445
145577
  }
145446
145578
  }
145447
- threadBlocks = buildThreadBlocks(threads, filePatchMap, input.reviewId);
145579
+ threadBlocks = buildThreadBlocks(input.threads, filePatchMap, input.reviewId);
145448
145580
  }
145449
145581
  const formatted = formatReviewThreads(threadBlocks, {
145450
145582
  pullNumber: input.pullNumber,
@@ -145454,6 +145586,30 @@ async function getReviewData(input) {
145454
145586
  });
145455
145587
  return { threadBlocks, reviewer, formatted };
145456
145588
  }
145589
+ async function getReviewData(input) {
145590
+ const [review, threads] = await Promise.all([
145591
+ input.octokit.rest.pulls.getReview({
145592
+ owner: input.owner,
145593
+ repo: input.name,
145594
+ pull_number: input.pullNumber,
145595
+ review_id: input.reviewId
145596
+ }),
145597
+ getReviewThreads(input)
145598
+ ]);
145599
+ const prFiles = threads.length > 0 ? await input.octokit.paginate(input.octokit.rest.pulls.listFiles, {
145600
+ owner: input.owner,
145601
+ repo: input.name,
145602
+ pull_number: input.pullNumber,
145603
+ per_page: 100
145604
+ }) : [];
145605
+ return formatReviewData({
145606
+ review: review.data,
145607
+ threads,
145608
+ prFiles,
145609
+ pullNumber: input.pullNumber,
145610
+ reviewId: input.reviewId
145611
+ });
145612
+ }
145457
145613
  function GetReviewCommentsTool(ctx) {
145458
145614
  return tool({
145459
145615
  name: "get_review_comments",
@@ -146470,14 +146626,13 @@ function UploadFileTool(ctx) {
146470
146626
 
146471
146627
  // mcp/server.ts
146472
146628
  function initToolState(params) {
146473
- const parsed2 = params.progressCommentId ? parseInt(params.progressCommentId, 10) : NaN;
146474
- const resolvedId = Number.isNaN(parsed2) || parsed2 <= 0 ? void 0 : parsed2;
146475
- if (resolvedId) {
146476
- log.info(`\xBB using pre-created progress comment: ${resolvedId}`);
146629
+ const resolved = parseProgressComment(params.progressComment);
146630
+ if (resolved) {
146631
+ log.info(`\xBB using pre-created progress comment: ${resolved.id} (${resolved.type})`);
146477
146632
  }
146478
146633
  return {
146479
- progressCommentId: resolvedId,
146480
- hadProgressComment: !!resolvedId,
146634
+ progressComment: resolved,
146635
+ hadProgressComment: !!resolved,
146481
146636
  backgroundProcesses: /* @__PURE__ */ new Map(),
146482
146637
  usageEntries: []
146483
146638
  };
@@ -152214,8 +152369,8 @@ async function reportErrorToComment(ctx) {
152214
152369
  const formattedError = ctx.title ? `${ctx.title}
152215
152370
 
152216
152371
  ${ctx.error}` : ctx.error;
152217
- const commentId = ctx.toolState.progressCommentId;
152218
- if (!commentId) {
152372
+ const comment = ctx.toolState.progressComment;
152373
+ if (!comment) {
152219
152374
  return;
152220
152375
  }
152221
152376
  const repoContext = parseRepoContext();
@@ -152234,12 +152389,11 @@ ${ctx.error}` : ctx.error;
152234
152389
  customParts,
152235
152390
  model: ctx.toolState.model
152236
152391
  });
152237
- await octokit.rest.issues.updateComment({
152238
- owner: repoContext.owner,
152239
- repo: repoContext.name,
152240
- comment_id: commentId,
152241
- body: `${formattedError}${footer}`
152242
- });
152392
+ await updateProgressComment(
152393
+ { octokit, owner: repoContext.owner, repo: repoContext.name },
152394
+ comment,
152395
+ `${formattedError}${footer}`
152396
+ );
152243
152397
  ctx.toolState.wasUpdated = true;
152244
152398
  }
152245
152399
 
@@ -152739,7 +152893,10 @@ var JsonPayload = type({
152739
152893
  "eventInstructions?": "string",
152740
152894
  "event?": "object",
152741
152895
  "timeout?": "string | undefined",
152742
- "progressCommentId?": "string | undefined"
152896
+ "progressComment?": type({
152897
+ id: "string",
152898
+ type: "'issue' | 'review'"
152899
+ }).or("undefined")
152743
152900
  });
152744
152901
  var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
152745
152902
  function isCollaborator(event) {
@@ -152821,7 +152978,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
152821
152978
  event,
152822
152979
  timeout: inputs.timeout ?? jsonPayload?.timeout,
152823
152980
  cwd: resolveCwd(inputs.cwd),
152824
- progressCommentId: jsonPayload?.progressCommentId,
152981
+ progressComment: jsonPayload?.progressComment,
152825
152982
  // permissions: inputs > repoSettings > fallbacks
152826
152983
  push: inputs.push ?? repoSettings.push ?? "restricted",
152827
152984
  shell: resolvedShell,
@@ -152929,10 +153086,10 @@ async function handleAgentResult(ctx) {
152929
153086
  };
152930
153087
  }
152931
153088
 
152932
- // utils/runContextData.ts
152933
- var core5 = __toESM(require_core(), 1);
152934
-
152935
153089
  // utils/runContext.ts
153090
+ function isInfraCovered(params) {
153091
+ return params.isOss || params.plan === "payg";
153092
+ }
152936
153093
  var defaultSettings = {
152937
153094
  model: null,
152938
153095
  modes: [],
@@ -152950,7 +153107,8 @@ var defaultSettings = {
152950
153107
  var defaultRunContext = {
152951
153108
  settings: defaultSettings,
152952
153109
  apiToken: "",
152953
- oss: false
153110
+ oss: false,
153111
+ plan: "none"
152954
153112
  };
152955
153113
  async function fetchRunContext(params) {
152956
153114
  const timeoutMs = 3e4;
@@ -152989,6 +153147,7 @@ async function fetchRunContext(params) {
152989
153147
  },
152990
153148
  apiToken: data.apiToken,
152991
153149
  oss: data.oss ?? false,
153150
+ plan: data.plan ?? "none",
152992
153151
  proxyModel: data.proxyModel,
152993
153152
  dbSecrets: data.dbSecrets
152994
153153
  };
@@ -152999,6 +153158,7 @@ async function fetchRunContext(params) {
152999
153158
  }
153000
153159
 
153001
153160
  // utils/runContextData.ts
153161
+ var core5 = __toESM(require_core(), 1);
153002
153162
  async function resolveRunContextData(params) {
153003
153163
  log.info(`\xBB running Pullfrog v${package_default.version}...`);
153004
153164
  const repoContext = parseRepoContext();
@@ -153020,6 +153180,7 @@ async function resolveRunContextData(params) {
153020
153180
  repoSettings: runContext.settings,
153021
153181
  apiToken: runContext.apiToken,
153022
153182
  oss: runContext.oss,
153183
+ plan: runContext.plan,
153023
153184
  proxyModel: runContext.proxyModel,
153024
153185
  dbSecrets: runContext.dbSecrets
153025
153186
  };
@@ -153346,6 +153507,59 @@ function resolveAgentForLog(ctx) {
153346
153507
  }
153347
153508
  return ctx.agentName;
153348
153509
  }
153510
+ var BillingError = class extends Error {
153511
+ code;
153512
+ declineCode;
153513
+ needsReauthentication;
153514
+ constructor(message, opts = {}) {
153515
+ super(message);
153516
+ this.name = "BillingError";
153517
+ this.code = opts.code ?? null;
153518
+ this.declineCode = opts.declineCode ?? null;
153519
+ this.needsReauthentication = opts.needsReauthentication ?? false;
153520
+ }
153521
+ };
153522
+ var TransientError = class extends Error {
153523
+ constructor(message) {
153524
+ super(message);
153525
+ this.name = "TransientError";
153526
+ }
153527
+ };
153528
+ function formatBillingErrorSummary(error49) {
153529
+ if (error49.code === "router_requires_card") {
153530
+ return [
153531
+ "### \u26D4 Pullfrog Router requires a card",
153532
+ "",
153533
+ "This run was going to use Pullfrog Router, which bills at raw OpenRouter cost and needs a card on file. Runs won't proceed until a card is added.",
153534
+ "",
153535
+ "[Add a card \u2192](https://pullfrog.com/console#model-access) \u2014 your first $20 of Router usage is free."
153536
+ ].join("\n");
153537
+ }
153538
+ if (error49.needsReauthentication) {
153539
+ return [
153540
+ "### \u274C Pullfrog billing error \u2014 card requires 3DS on every charge",
153541
+ "",
153542
+ `Your card issuer requires a 3D Secure challenge on each off-session charge (\`${error49.declineCode ?? "authentication_required"}\`), which we can't run from the agent. Top up your Router credit balance manually \u2014 3DS runs interactively in Stripe Checkout, and subsequent runs draw from the prepaid balance without triggering another off-session charge.`,
153543
+ "",
153544
+ "[Top up your Router credit balance \u2192](https://pullfrog.com/console)"
153545
+ ].join("\n");
153546
+ }
153547
+ const codeSuffix = error49.declineCode ? ` (\`${error49.declineCode}\`)` : "";
153548
+ return `### \u274C Pullfrog billing error
153549
+
153550
+ ${error49.message}${codeSuffix}
153551
+
153552
+ [Manage billing \u2192](https://pullfrog.com/console)`;
153553
+ }
153554
+ function formatTransientErrorSummary(error49) {
153555
+ return [
153556
+ "### \u26A0\uFE0F Pullfrog temporarily unavailable",
153557
+ "",
153558
+ error49.message,
153559
+ "",
153560
+ "This is typically transient \u2014 the next dispatch should succeed. If it persists, check [status.pullfrog.com](https://status.pullfrog.com)."
153561
+ ].join("\n");
153562
+ }
153349
153563
  async function mintProxyKey(ctx) {
153350
153564
  try {
153351
153565
  process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
@@ -153358,6 +153572,20 @@ async function mintProxyKey(ctx) {
153358
153572
  method: "POST",
153359
153573
  headers: { Authorization: `Bearer ${oidcToken}` }
153360
153574
  });
153575
+ if (response.status === 402) {
153576
+ const body = await response.json().catch(() => null);
153577
+ throw new BillingError(body?.error ?? "insufficient balance", {
153578
+ code: body?.code ?? null,
153579
+ declineCode: body?.declineCode ?? null,
153580
+ needsReauthentication: body?.needsReauthentication ?? false
153581
+ });
153582
+ }
153583
+ if (response.status === 503) {
153584
+ const body = await response.json().catch(() => null);
153585
+ throw new TransientError(
153586
+ body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
153587
+ );
153588
+ }
153361
153589
  if (!response.ok) {
153362
153590
  log.warning(`proxy key mint failed (${response.status})`);
153363
153591
  return null;
@@ -153365,6 +153593,8 @@ async function mintProxyKey(ctx) {
153365
153593
  const data = await response.json();
153366
153594
  return data.key;
153367
153595
  } catch (error49) {
153596
+ if (error49 instanceof BillingError) throw error49;
153597
+ if (error49 instanceof TransientError) throw error49;
153368
153598
  log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
153369
153599
  return null;
153370
153600
  } finally {
@@ -153374,19 +153604,19 @@ async function mintProxyKey(ctx) {
153374
153604
  }
153375
153605
  async function resolveProxyModel(ctx) {
153376
153606
  if (process.env.PULLFROG_MODEL?.trim()) return;
153377
- if (ctx.oss && ctx.proxyModel) {
153378
- if (!ctx.oidcCredentials) {
153379
- log.warning("\xBB oss repo but no OIDC credentials available \u2014 skipping proxy");
153380
- return;
153381
- }
153382
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
153383
- if (!key) return;
153384
- process.env.OPENROUTER_API_KEY = key;
153385
- core6.setSecret(key);
153386
- ctx.payload.proxyModel = ctx.proxyModel;
153387
- log.info(`\xBB proxy: oss \u2192 ${ctx.proxyModel}`);
153607
+ const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
153608
+ if (!needsProxy) return;
153609
+ if (!ctx.oidcCredentials) {
153610
+ log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
153388
153611
  return;
153389
153612
  }
153613
+ const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
153614
+ if (!key) return;
153615
+ process.env.OPENROUTER_API_KEY = key;
153616
+ core6.setSecret(key);
153617
+ ctx.payload.proxyModel = ctx.proxyModel;
153618
+ const label = ctx.oss ? "oss" : "router";
153619
+ log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
153390
153620
  }
153391
153621
  async function writeJobSummary(toolState) {
153392
153622
  const usageSummary = formatUsageSummary(toolState.usageEntries);
@@ -153408,7 +153638,7 @@ async function main() {
153408
153638
  let safetyNetTimer;
153409
153639
  const resolvedPromptInput = resolvePromptInput();
153410
153640
  const toolState = initToolState({
153411
- progressCommentId: typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : void 0
153641
+ progressComment: typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressComment : void 0
153412
153642
  });
153413
153643
  resolveGit();
153414
153644
  const jobToken = getJobToken();
@@ -153442,12 +153672,33 @@ async function main() {
153442
153672
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153443
153673
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153444
153674
  }
153445
- await resolveProxyModel({
153446
- payload,
153447
- oss: runContext.oss,
153448
- proxyModel: runContext.proxyModel,
153449
- oidcCredentials
153450
- });
153675
+ try {
153676
+ await resolveProxyModel({
153677
+ payload,
153678
+ oss: runContext.oss,
153679
+ plan: runContext.plan,
153680
+ proxyModel: runContext.proxyModel,
153681
+ oidcCredentials
153682
+ });
153683
+ } catch (error49) {
153684
+ if (error49 instanceof BillingError) {
153685
+ const summary2 = formatBillingErrorSummary(error49);
153686
+ await writeSummary(summary2).catch(() => {
153687
+ });
153688
+ await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
153689
+ });
153690
+ throw error49;
153691
+ }
153692
+ if (error49 instanceof TransientError) {
153693
+ const summary2 = formatTransientErrorSummary(error49);
153694
+ await writeSummary(summary2).catch(() => {
153695
+ });
153696
+ await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
153697
+ });
153698
+ throw error49;
153699
+ }
153700
+ throw error49;
153701
+ }
153451
153702
  const octokit = createOctokit(tokenRef.mcpToken);
153452
153703
  const runInfo = await resolveRun({ octokit });
153453
153704
  let toolContext;
@@ -153521,6 +153772,8 @@ async function main() {
153521
153772
  jobId: runInfo.jobId,
153522
153773
  mcpServerUrl: "",
153523
153774
  tmpdir: tmpdir3,
153775
+ oss: runContext.oss,
153776
+ plan: runContext.plan,
153524
153777
  resolvedModel
153525
153778
  };
153526
153779
  const mcpHttpServer = __using(_stack, await startMcpHttpServer(toolContext, { outputSchema }), true);
@@ -153584,6 +153837,9 @@ ${instructions.user}` : null,
153584
153837
  }
153585
153838
  });
153586
153839
  toolState.todoTracker = todoTracker;
153840
+ onExitSignal(() => {
153841
+ todoTracker?.cancel();
153842
+ });
153587
153843
  let innerTimeoutFired = false;
153588
153844
  const onInnerActivityTimeout = () => {
153589
153845
  if (innerTimeoutFired) return;
@@ -153669,7 +153925,7 @@ ${instructions.user}` : null,
153669
153925
  });
153670
153926
  }
153671
153927
  const trackerWasLastWriter = todoTracker?.hasPublished && !toolState.finalSummaryWritten;
153672
- if (toolContext && toolState.progressCommentId && (!toolState.wasUpdated || trackerWasLastWriter)) {
153928
+ if (toolContext && toolState.progressComment && (!toolState.wasUpdated || trackerWasLastWriter)) {
153673
153929
  await deleteProgressComment(toolContext).catch((error49) => {
153674
153930
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
153675
153931
  });
@@ -153777,32 +154033,36 @@ The workflow encountered an error before any progress could be reported.`;
153777
154033
  return `${errorMessage}${footer}`;
153778
154034
  }
153779
154035
  async function validateStuckProgressComment(ctx) {
153780
- if (!ctx.promptInput?.progressCommentId) {
153781
- log.info("[post] no progressCommentId in prompt input, skipping cleanup");
154036
+ const promptComment = ctx.promptInput?.progressComment;
154037
+ if (!promptComment) {
154038
+ log.info("[post] no progressComment in prompt input, skipping cleanup");
154039
+ return null;
154040
+ }
154041
+ const comment = parseProgressComment(promptComment);
154042
+ if (!comment) {
154043
+ log.info(`[post] progressComment.id is not a positive integer: ${promptComment.id}`);
153782
154044
  return null;
153783
154045
  }
153784
- const commentId = parseInt(ctx.promptInput.progressCommentId, 10);
153785
- log.info(`[post] validating progressCommentId from prompt input: ${commentId}`);
154046
+ log.info(`[post] validating progressComment from prompt input: ${comment.id} (${comment.type})`);
153786
154047
  try {
153787
- const commentResult = await ctx.octokit.rest.issues.getComment({
153788
- owner: ctx.repoContext.owner,
153789
- repo: ctx.repoContext.name,
153790
- comment_id: commentId
153791
- });
153792
- const body = commentResult.data.body ?? "";
154048
+ const fetched = await getProgressComment(
154049
+ { octokit: ctx.octokit, owner: ctx.repoContext.owner, repo: ctx.repoContext.name },
154050
+ comment
154051
+ );
154052
+ const body = fetched.body ?? "";
153793
154053
  if (isLeapingIntoActionCommentBody(body)) {
153794
- log.info(`[post] comment ${commentId} is stuck on "Leaping into action"`);
153795
- return commentId;
154054
+ log.info(`[post] comment ${comment.id} is stuck on "Leaping into action"`);
154055
+ return comment;
153796
154056
  }
153797
154057
  if (/^- \[[ x]\] |^- \*\*→\*\* |^- ~~/.test(body)) {
153798
- log.info(`[post] comment ${commentId} is stuck on a todo checklist`);
153799
- return commentId;
154058
+ log.info(`[post] comment ${comment.id} is stuck on a todo checklist`);
154059
+ return comment;
153800
154060
  }
153801
- log.info(`[post] comment ${commentId} is not stuck (already updated or different content)`);
154061
+ log.info(`[post] comment ${comment.id} is not stuck (already updated or different content)`);
153802
154062
  return null;
153803
154063
  } catch (error49) {
153804
154064
  const errorMessage = error49 instanceof Error ? error49.message : String(error49);
153805
- log.info(`[post] failed to get comment ${commentId}: ${errorMessage}`);
154065
+ log.info(`[post] failed to get comment ${comment.id}: ${errorMessage}`);
153806
154066
  return null;
153807
154067
  }
153808
154068
  }
@@ -153853,26 +154113,56 @@ async function runPostCleanup() {
153853
154113
  const repoContext = parseRepoContext();
153854
154114
  const octokit = createOctokit(token);
153855
154115
  const ctx = { repoContext, octokit, runId, promptInput };
153856
- const commentId = await validateStuckProgressComment(ctx);
153857
- if (!commentId) return log.info("\xBB [post] no stuck progress comment to update, skipping cleanup");
153858
- log.info(`\xBB [post] validated stuck comment: ${commentId}, updating with error message`);
154116
+ const stuck = await validateStuckProgressComment(ctx);
154117
+ if (!stuck) return log.info("\xBB [post] no stuck progress comment to update, skipping cleanup");
154118
+ log.info(
154119
+ `\xBB [post] validated stuck comment: ${stuck.id} (${stuck.type}), updating with error message`
154120
+ );
153859
154121
  try {
153860
154122
  const body = buildErrorCommentBody(
153861
154123
  ctx,
153862
154124
  SHOULD_CHECK_REASON ? await getIsCancelled(ctx) : false
153863
154125
  );
153864
- await ctx.octokit.rest.issues.updateComment({
153865
- owner: ctx.repoContext.owner,
153866
- repo: ctx.repoContext.name,
153867
- comment_id: commentId,
153868
- body
153869
- });
153870
- log.info("\xBB [post] successfully updated progress comment");
154126
+ await writeAndVerify(ctx, stuck, body);
153871
154127
  } catch (error49) {
153872
154128
  const errorMessage = error49 instanceof Error ? error49.message : String(error49);
153873
154129
  log.info(`[post] failed to update comment: ${errorMessage}`);
153874
154130
  }
153875
154131
  }
154132
+ var VERIFY_DELAY_MS = 3e3;
154133
+ var MAX_WRITE_ATTEMPTS = 3;
154134
+ async function writeAndVerify(ctx, comment, body) {
154135
+ const apiCtx = {
154136
+ octokit: ctx.octokit,
154137
+ owner: ctx.repoContext.owner,
154138
+ repo: ctx.repoContext.name
154139
+ };
154140
+ for (let attempt = 1; attempt <= MAX_WRITE_ATTEMPTS; attempt++) {
154141
+ await updateProgressComment(apiCtx, comment, body);
154142
+ await new Promise((resolve3) => setTimeout(resolve3, VERIFY_DELAY_MS));
154143
+ let fetched;
154144
+ try {
154145
+ fetched = await getProgressComment(apiCtx, comment);
154146
+ } catch (error49) {
154147
+ log.warning(
154148
+ `[post] verify GET failed after attempt ${attempt} \u2014 trusting our PUT landed: ${error49 instanceof Error ? error49.message : String(error49)}`
154149
+ );
154150
+ return;
154151
+ }
154152
+ if (fetched.body === body) {
154153
+ log.info(
154154
+ `\xBB [post] successfully updated progress comment (attempt ${attempt}/${MAX_WRITE_ATTEMPTS})`
154155
+ );
154156
+ return;
154157
+ }
154158
+ log.info(
154159
+ `[post] body was overwritten after our write (attempt ${attempt}/${MAX_WRITE_ATTEMPTS}), retrying`
154160
+ );
154161
+ }
154162
+ log.warning(
154163
+ `[post] gave up after ${MAX_WRITE_ATTEMPTS} attempts \u2014 comment may be stale (in-flight writes from the cancelled run kept clobbering us)`
154164
+ );
154165
+ }
153876
154166
 
153877
154167
  // commands/gha.ts
153878
154168
  process.env.PATH = `${dirname4(process.execPath)}:${process.env.PATH}`;
@@ -155659,7 +155949,7 @@ async function run2() {
155659
155949
  }
155660
155950
 
155661
155951
  // cli.ts
155662
- var VERSION10 = "0.0.203";
155952
+ var VERSION10 = "0.0.204";
155663
155953
  var bin = basename2(process.argv[1] || "");
155664
155954
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
155665
155955
  var rawArgs = process.argv.slice(2);