pullfrog 0.1.17 → 0.1.19

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
@@ -101880,7 +101880,7 @@ var import_semver = __toESM(require_semver2(), 1);
101880
101880
  // package.json
101881
101881
  var package_default = {
101882
101882
  name: "pullfrog",
101883
- version: "0.1.17",
101883
+ version: "0.1.19",
101884
101884
  type: "module",
101885
101885
  bin: {
101886
101886
  pullfrog: "dist/cli.mjs",
@@ -107960,6 +107960,20 @@ function buildSecurityConfig(ctx, model) {
107960
107960
  const slashIndex = model.indexOf("/");
107961
107961
  if (slashIndex > 0) {
107962
107962
  config3.enabled_providers = [model.slice(0, slashIndex).toLowerCase()];
107963
+ if (model.startsWith("openrouter/moonshotai/")) {
107964
+ const modelID = model.slice(slashIndex + 1);
107965
+ config3.provider = {
107966
+ ...config3.provider,
107967
+ openrouter: {
107968
+ npm: "@openrouter/ai-sdk-provider@2.9.0",
107969
+ options: {
107970
+ baseURL: "https://openrouter.ai/api/v1",
107971
+ apiKey: "{env:OPENROUTER_API_KEY}"
107972
+ },
107973
+ models: { [modelID]: {} }
107974
+ }
107975
+ };
107976
+ }
107963
107977
  }
107964
107978
  }
107965
107979
  return JSON.stringify(config3);
@@ -108084,7 +108098,7 @@ function newTurn() {
108084
108098
  };
108085
108099
  }
108086
108100
  async function consumeEvents(ctx, signal) {
108087
- const result = await ctx.client.event.subscribe();
108101
+ const result = await ctx.client.event.subscribe({}, { signal });
108088
108102
  for await (const event of result.stream) {
108089
108103
  if (signal.aborted) break;
108090
108104
  ctx.eventCount += 1;
@@ -151963,6 +151977,30 @@ function classifyPushError(msg) {
151963
151977
  return "unknown";
151964
151978
  }
151965
151979
  var TRANSIENT_RETRY_DELAYS_MS = [2e3, 5e3];
151980
+ async function pushWithRetry(args2, token) {
151981
+ let lastErr;
151982
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
151983
+ try {
151984
+ await $git("push", args2, { token });
151985
+ if (attempt > 0) log.info(`push succeeded on attempt ${attempt + 1}`);
151986
+ return;
151987
+ } catch (err) {
151988
+ lastErr = err;
151989
+ const msg = err instanceof Error ? err.message : String(err);
151990
+ if (classifyPushError(msg) === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
151991
+ const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
151992
+ const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
151993
+ log.info(
151994
+ `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
151995
+ );
151996
+ await new Promise((r) => setTimeout(r, delay2));
151997
+ continue;
151998
+ }
151999
+ throw err;
152000
+ }
152001
+ }
152002
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
152003
+ }
151966
152004
  function PushBranchTool(ctx) {
151967
152005
  const defaultBranch = ctx.repo.data.default_branch || "main";
151968
152006
  const pushPermission = ctx.payload.push;
@@ -152030,48 +152068,23 @@ ${postHookStatus}`
152030
152068
  if (force) {
152031
152069
  log.warning(`force pushing - this will overwrite remote history`);
152032
152070
  }
152033
- let lastErr;
152034
- let pushed = false;
152035
- for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
152036
- try {
152037
- await $git("push", pushArgs, {
152038
- token: ctx.gitToken
152039
- });
152040
- if (attempt > 0) {
152041
- log.info(`push succeeded on attempt ${attempt + 1}`);
152042
- }
152043
- pushed = true;
152044
- break;
152045
- } catch (err) {
152046
- lastErr = err;
152047
- const msg = err instanceof Error ? err.message : String(err);
152048
- const kind = classifyPushError(msg);
152049
- if (kind === "concurrent-push") {
152050
- 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')`;
152051
- throw new Error(
152052
- `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
152071
+ try {
152072
+ await pushWithRetry(pushArgs, ctx.gitToken);
152073
+ } catch (err) {
152074
+ const msg = err instanceof Error ? err.message : String(err);
152075
+ if (classifyPushError(msg) === "concurrent-push") {
152076
+ 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')`;
152077
+ throw new Error(
152078
+ `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
152053
152079
 
152054
152080
  to resolve this:
152055
152081
  1. use git_fetch to fetch the remote branch: git_fetch({ ref: "${pushDest.remoteBranch}" })
152056
152082
  ${integrateStep}
152057
152083
  3. resolve any merge conflicts if needed
152058
152084
  4. retry push_branch`
152059
- );
152060
- }
152061
- if (kind === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
152062
- const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
152063
- const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
152064
- log.info(
152065
- `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
152066
- );
152067
- await new Promise((r) => setTimeout(r, delay2));
152068
- continue;
152069
- }
152070
- throw err;
152085
+ );
152071
152086
  }
152072
- }
152073
- if (!pushed) {
152074
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
152087
+ throw err;
152075
152088
  }
152076
152089
  const pushedSha = $2("git", ["rev-parse", "HEAD"], { log: false }).trim();
152077
152090
  log.info(
@@ -152314,9 +152327,7 @@ function DeleteBranchTool(ctx) {
152314
152327
  `Blocked: cannot delete the default branch '${defaultBranch}'. If you really need to delete or rename it, do it manually via the repository settings.`
152315
152328
  );
152316
152329
  }
152317
- await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
152318
- token: ctx.gitToken
152319
- });
152330
+ await pushWithRetry(["origin", "--delete", `refs/heads/${params.branchName}`], ctx.gitToken);
152320
152331
  log.info(`\xBB deleted branch ${params.branchName}`);
152321
152332
  return { success: true, deleted: params.branchName };
152322
152333
  })
@@ -152340,9 +152351,7 @@ function PushTagsTool(ctx) {
152340
152351
  }
152341
152352
  validateTagName(params.tag);
152342
152353
  const pushArgs = [...params.force ? ["-f"] : [], "origin", `refs/tags/${params.tag}`];
152343
- await $git("push", pushArgs, {
152344
- token: ctx.gitToken
152345
- });
152354
+ await pushWithRetry(pushArgs, ctx.gitToken);
152346
152355
  log.info(`\xBB pushed tag ${params.tag}`);
152347
152356
  return { success: true, tag: params.tag };
152348
152357
  })
@@ -161267,6 +161276,13 @@ function formatGenericFailure(errorMessage) {
161267
161276
  "```"
161268
161277
  ].join("\n");
161269
161278
  }
161279
+ function formatMinimalFailureComment(repo) {
161280
+ const runId = process.env.GITHUB_RUN_ID;
161281
+ if (!runId) return "**Run failed.**";
161282
+ const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
161283
+ const url4 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
161284
+ return `**Run failed.** [View the logs \u2192](${url4})`;
161285
+ }
161270
161286
  var PROVIDER_BILLING_URLS = {
161271
161287
  deepseek: "https://platform.deepseek.com/top_up",
161272
161288
  anthropic: "https://console.anthropic.com/settings/billing",
@@ -161340,11 +161356,12 @@ ${body}`, comment: body };
161340
161356
  return { summary: body, comment: body };
161341
161357
  }
161342
161358
  if (hangBody) {
161359
+ const isBillingExhausted = input.agentDiagnostic?.lastProviderError === "provider billing exhausted";
161343
161360
  return {
161344
161361
  summary: `### \u274C Pullfrog failed
161345
161362
 
161346
161363
  ${hangBody}`,
161347
- comment: hangBody
161364
+ comment: isBillingExhausted ? hangBody : formatMinimalFailureComment(input.repo)
161348
161365
  };
161349
161366
  }
161350
161367
  const genericBody = formatGenericFailure(input.errorMessage);
@@ -161352,7 +161369,7 @@ ${hangBody}`,
161352
161369
  summary: `### \u274C Pullfrog failed
161353
161370
 
161354
161371
  ${genericBody}`,
161355
- comment: genericBody
161372
+ comment: formatMinimalFailureComment(input.repo)
161356
161373
  };
161357
161374
  }
161358
161375
 
@@ -163002,7 +163019,7 @@ async function run2() {
163002
163019
  }
163003
163020
 
163004
163021
  // cli.ts
163005
- var VERSION10 = "0.1.17";
163022
+ var VERSION10 = "0.1.19";
163006
163023
  var bin = basename2(process.argv[1] || "");
163007
163024
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
163008
163025
  var rawArgs = process.argv.slice(2);
package/dist/index.js CHANGED
@@ -100080,7 +100080,7 @@ var import_semver = __toESM(require_semver2(), 1);
100080
100080
  // package.json
100081
100081
  var package_default = {
100082
100082
  name: "pullfrog",
100083
- version: "0.1.17",
100083
+ version: "0.1.19",
100084
100084
  type: "module",
100085
100085
  bin: {
100086
100086
  pullfrog: "dist/cli.mjs",
@@ -106202,6 +106202,20 @@ function buildSecurityConfig(ctx, model) {
106202
106202
  const slashIndex = model.indexOf("/");
106203
106203
  if (slashIndex > 0) {
106204
106204
  config3.enabled_providers = [model.slice(0, slashIndex).toLowerCase()];
106205
+ if (model.startsWith("openrouter/moonshotai/")) {
106206
+ const modelID = model.slice(slashIndex + 1);
106207
+ config3.provider = {
106208
+ ...config3.provider,
106209
+ openrouter: {
106210
+ npm: "@openrouter/ai-sdk-provider@2.9.0",
106211
+ options: {
106212
+ baseURL: "https://openrouter.ai/api/v1",
106213
+ apiKey: "{env:OPENROUTER_API_KEY}"
106214
+ },
106215
+ models: { [modelID]: {} }
106216
+ }
106217
+ };
106218
+ }
106205
106219
  }
106206
106220
  }
106207
106221
  return JSON.stringify(config3);
@@ -106326,7 +106340,7 @@ function newTurn() {
106326
106340
  };
106327
106341
  }
106328
106342
  async function consumeEvents(ctx, signal) {
106329
- const result = await ctx.client.event.subscribe();
106343
+ const result = await ctx.client.event.subscribe({}, { signal });
106330
106344
  for await (const event of result.stream) {
106331
106345
  if (signal.aborted) break;
106332
106346
  ctx.eventCount += 1;
@@ -150205,6 +150219,30 @@ function classifyPushError(msg) {
150205
150219
  return "unknown";
150206
150220
  }
150207
150221
  var TRANSIENT_RETRY_DELAYS_MS = [2e3, 5e3];
150222
+ async function pushWithRetry(args2, token) {
150223
+ let lastErr;
150224
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
150225
+ try {
150226
+ await $git("push", args2, { token });
150227
+ if (attempt > 0) log.info(`push succeeded on attempt ${attempt + 1}`);
150228
+ return;
150229
+ } catch (err) {
150230
+ lastErr = err;
150231
+ const msg = err instanceof Error ? err.message : String(err);
150232
+ if (classifyPushError(msg) === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
150233
+ const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
150234
+ const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
150235
+ log.info(
150236
+ `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
150237
+ );
150238
+ await new Promise((r) => setTimeout(r, delay2));
150239
+ continue;
150240
+ }
150241
+ throw err;
150242
+ }
150243
+ }
150244
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
150245
+ }
150208
150246
  function PushBranchTool(ctx) {
150209
150247
  const defaultBranch = ctx.repo.data.default_branch || "main";
150210
150248
  const pushPermission = ctx.payload.push;
@@ -150272,48 +150310,23 @@ ${postHookStatus}`
150272
150310
  if (force) {
150273
150311
  log.warning(`force pushing - this will overwrite remote history`);
150274
150312
  }
150275
- let lastErr;
150276
- let pushed = false;
150277
- for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
150278
- try {
150279
- await $git("push", pushArgs, {
150280
- token: ctx.gitToken
150281
- });
150282
- if (attempt > 0) {
150283
- log.info(`push succeeded on attempt ${attempt + 1}`);
150284
- }
150285
- pushed = true;
150286
- break;
150287
- } catch (err) {
150288
- lastErr = err;
150289
- const msg = err instanceof Error ? err.message : String(err);
150290
- const kind = classifyPushError(msg);
150291
- if (kind === "concurrent-push") {
150292
- 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')`;
150293
- throw new Error(
150294
- `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
150313
+ try {
150314
+ await pushWithRetry(pushArgs, ctx.gitToken);
150315
+ } catch (err) {
150316
+ const msg = err instanceof Error ? err.message : String(err);
150317
+ if (classifyPushError(msg) === "concurrent-push") {
150318
+ 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')`;
150319
+ throw new Error(
150320
+ `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
150295
150321
 
150296
150322
  to resolve this:
150297
150323
  1. use git_fetch to fetch the remote branch: git_fetch({ ref: "${pushDest.remoteBranch}" })
150298
150324
  ${integrateStep}
150299
150325
  3. resolve any merge conflicts if needed
150300
150326
  4. retry push_branch`
150301
- );
150302
- }
150303
- if (kind === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
150304
- const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
150305
- const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
150306
- log.info(
150307
- `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
150308
- );
150309
- await new Promise((r) => setTimeout(r, delay2));
150310
- continue;
150311
- }
150312
- throw err;
150327
+ );
150313
150328
  }
150314
- }
150315
- if (!pushed) {
150316
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
150329
+ throw err;
150317
150330
  }
150318
150331
  const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
150319
150332
  log.info(
@@ -150556,9 +150569,7 @@ function DeleteBranchTool(ctx) {
150556
150569
  `Blocked: cannot delete the default branch '${defaultBranch}'. If you really need to delete or rename it, do it manually via the repository settings.`
150557
150570
  );
150558
150571
  }
150559
- await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
150560
- token: ctx.gitToken
150561
- });
150572
+ await pushWithRetry(["origin", "--delete", `refs/heads/${params.branchName}`], ctx.gitToken);
150562
150573
  log.info(`\xBB deleted branch ${params.branchName}`);
150563
150574
  return { success: true, deleted: params.branchName };
150564
150575
  })
@@ -150582,9 +150593,7 @@ function PushTagsTool(ctx) {
150582
150593
  }
150583
150594
  validateTagName(params.tag);
150584
150595
  const pushArgs = [...params.force ? ["-f"] : [], "origin", `refs/tags/${params.tag}`];
150585
- await $git("push", pushArgs, {
150586
- token: ctx.gitToken
150587
- });
150596
+ await pushWithRetry(pushArgs, ctx.gitToken);
150588
150597
  log.info(`\xBB pushed tag ${params.tag}`);
150589
150598
  return { success: true, tag: params.tag };
150590
150599
  })
@@ -159509,6 +159518,13 @@ function formatGenericFailure(errorMessage) {
159509
159518
  "```"
159510
159519
  ].join("\n");
159511
159520
  }
159521
+ function formatMinimalFailureComment(repo) {
159522
+ const runId = process.env.GITHUB_RUN_ID;
159523
+ if (!runId) return "**Run failed.**";
159524
+ const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
159525
+ const url4 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
159526
+ return `**Run failed.** [View the logs \u2192](${url4})`;
159527
+ }
159512
159528
  var PROVIDER_BILLING_URLS = {
159513
159529
  deepseek: "https://platform.deepseek.com/top_up",
159514
159530
  anthropic: "https://console.anthropic.com/settings/billing",
@@ -159582,11 +159598,12 @@ ${body}`, comment: body };
159582
159598
  return { summary: body, comment: body };
159583
159599
  }
159584
159600
  if (hangBody) {
159601
+ const isBillingExhausted = input.agentDiagnostic?.lastProviderError === "provider billing exhausted";
159585
159602
  return {
159586
159603
  summary: `### \u274C Pullfrog failed
159587
159604
 
159588
159605
  ${hangBody}`,
159589
- comment: hangBody
159606
+ comment: isBillingExhausted ? hangBody : formatMinimalFailureComment(input.repo)
159590
159607
  };
159591
159608
  }
159592
159609
  const genericBody = formatGenericFailure(input.errorMessage);
@@ -159594,7 +159611,7 @@ ${hangBody}`,
159594
159611
  summary: `### \u274C Pullfrog failed
159595
159612
 
159596
159613
  ${genericBody}`,
159597
- comment: genericBody
159614
+ comment: formatMinimalFailureComment(input.repo)
159598
159615
  };
159599
159616
  }
159600
159617
 
@@ -30,18 +30,24 @@
30
30
  * 5. Activity-timeout hang — `errorMessage` starts with
31
31
  * `"activity timeout"` or `"agent still pending"` AND none of the
32
32
  * above matched. The harness keeps structured diagnostic state on
33
- * `toolState.agentDiagnostic`; `formatAgentHangBody` renders that as
34
- * a markdown block.
35
- *
36
- * 6. Default a plain-English lead sentence explaining the run failed,
37
- * followed by the raw error message in a fenced code block (so the user
38
- * never sees a bare internal string). The job summary adds the
39
- * `### Pullfrog failed` banner on top of the same body.
40
- *
41
- * The hang body and the API-key body diverge between the two surfaces only
42
- * in that the job summary wraps them in the `### ❌ Pullfrog failed` H3
43
- * banner; the PR comment uses the bare body since it already has Pullfrog
44
- * branding in its footer.
33
+ * `toolState.agentDiagnostic`; `formatAgentHangBody` renders that into
34
+ * the job summary. The PR comment instead collapses to a one-line
35
+ * `**Run failed.** [View the logs →]` — the watchdog jargon, event
36
+ * counts, and benign stderr tail are operator-grade detail that only
37
+ * alarm the average user. The one exception is a hang masking billing
38
+ * exhaustion (#778), where `formatAgentHangBody` emits an actionable
39
+ * top-up CTA that the comment keeps verbatim.
40
+ *
41
+ * 6. Default the job summary gets a plain-English lead sentence plus the
42
+ * raw error in a fenced code block under the `### ❌ Pullfrog failed`
43
+ * banner; the PR comment collapses to the same one-line logs link as
44
+ * the hang case, since the raw internal string helps nobody on the PR.
45
+ *
46
+ * Net: the actionable classifications (billing, API-key, model-not-found)
47
+ * render identical bodies on both surfaces; the non-actionable ones (hang,
48
+ * generic) keep the forensics in the Actions job summary and show a calm
49
+ * one-liner in the PR comment, whose footer already carries Pullfrog
50
+ * branding + rerun links.
45
51
  */
46
52
  import type { AgentDiagnostic } from "./agentHangReport.ts";
47
53
  export type RenderedRunError = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",