pullfrog 0.1.31 → 0.1.33

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.
@@ -8,6 +8,7 @@ export type { Mode } from "../modes.ts";
8
8
  export { modes } from "../modes.ts";
9
9
  export type { BuildPullfrogFooterParams, WorkflowRunFooterInfo, } from "../utils/buildPullfrogFooter.ts";
10
10
  export { buildPullfrogFooter, PULLFROG_DIVIDER, stripExistingFooter, } from "../utils/buildPullfrogFooter.ts";
11
+ export { checkRunsGate, hasVerifiedCheck } from "../utils/checksGate.ts";
11
12
  export type { CodexAuthBody } from "../utils/codexOAuth.ts";
12
13
  export { decodeJwtExpMs, OAuthInvalidGrantError, parseCodexAuthBody, refreshCodexAuthBody, stringifyCodexAuthBody, } from "../utils/codexOAuth.ts";
13
14
  export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
package/dist/internal.js CHANGED
@@ -9,15 +9,20 @@ var providers = {
9
9
  displayName: "Anthropic",
10
10
  envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
11
11
  models: {
12
- // claude-fable-5 is disabled globally access has been revoked, so it
13
- // can't run as-is anywhere (BYOK, Claude Code, or Router). the fallback
14
- // redirects all resolution to opus and hides it from pickers; opus is the
15
- // universally-available flagship, the AUTO_INTELLIGENT tier target, and
16
- // the recommended pick. clear the fallback if fable access returns (#959).
12
+ // claude-fable-5 is selectable but not recommended/auto-selected it's
13
+ // moving to usage-credits-only billing and isn't broadly available yet, so
14
+ // opus stays the universally-available flagship, the AUTO_INTELLIGENT tier
15
+ // target, and the recommended pick. an explicit fable pick hits the real
16
+ // API and is access-gated: it errors for accounts without access today and
17
+ // just works once usage credits are live — no silent opus fallback, the
18
+ // API is the source of truth (#959).
17
19
  "claude-fable": {
18
20
  displayName: "Claude Fable",
19
21
  resolve: "anthropic/claude-fable-5",
20
- fallback: "anthropic/claude-opus",
22
+ // rolling alias: models.dev's OpenRouter mirror lags brand-new pinned
23
+ // versions (claude-fable-5 isn't indexed yet), so track ~…-latest to
24
+ // stay catalog-valid and auto-follow version bumps.
25
+ openRouterResolve: "openrouter/~anthropic/claude-fable-latest",
21
26
  subagentModel: "claude-sonnet"
22
27
  },
23
28
  "claude-opus": {
@@ -29,8 +34,8 @@ var providers = {
29
34
  },
30
35
  "claude-sonnet": {
31
36
  displayName: "Claude Sonnet",
32
- resolve: "anthropic/claude-sonnet-4-6",
33
- openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
37
+ resolve: "anthropic/claude-sonnet-5",
38
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-5"
34
39
  },
35
40
  "claude-haiku": {
36
41
  displayName: "Claude Haiku",
@@ -208,8 +213,8 @@ var providers = {
208
213
  },
209
214
  "claude-sonnet": {
210
215
  displayName: "Claude Sonnet",
211
- resolve: "opencode/claude-sonnet-4-6",
212
- openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
216
+ resolve: "opencode/claude-sonnet-5",
217
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-5"
213
218
  },
214
219
  "claude-haiku": {
215
220
  displayName: "Claude Haiku",
@@ -1240,6 +1245,26 @@ function stripExistingFooter(body) {
1240
1245
  return body.substring(0, dividerIndex).trimEnd();
1241
1246
  }
1242
1247
 
1248
+ // utils/checksGate.ts
1249
+ var OK_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
1250
+ var PULLFROG_CHECKS = /* @__PURE__ */ new Set(["pullfrog", "pullfrog-approval"]);
1251
+ function checkRunsGate(params) {
1252
+ for (const run of params.checkRuns) {
1253
+ if (PULLFROG_CHECKS.has(run.name.toLowerCase())) continue;
1254
+ if (run.status !== "completed")
1255
+ return { ok: false, reason: `check "${run.name}" ${run.status}` };
1256
+ if (!OK_CONCLUSIONS.has(run.conclusion ?? "")) {
1257
+ return { ok: false, reason: `check "${run.name}" concluded ${run.conclusion}` };
1258
+ }
1259
+ }
1260
+ return { ok: true, reason: "all checks passed" };
1261
+ }
1262
+ function hasVerifiedCheck(params) {
1263
+ return params.checkRuns.some(
1264
+ (r) => !PULLFROG_CHECKS.has(r.name.toLowerCase()) && r.status === "completed"
1265
+ );
1266
+ }
1267
+
1243
1268
  // utils/codexOAuth.ts
1244
1269
  var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1245
1270
  var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -1467,6 +1492,7 @@ export {
1467
1492
  PULLFROG_DIVIDER,
1468
1493
  TIMEOUT_DISABLED,
1469
1494
  buildPullfrogFooter,
1495
+ checkRunsGate,
1470
1496
  createLeapingProgressComment,
1471
1497
  decodeJwtExpMs,
1472
1498
  defaultAutoTier,
@@ -1477,6 +1503,7 @@ export {
1477
1503
  getModelProvider,
1478
1504
  getProgressComment,
1479
1505
  getProviderDisplayName,
1506
+ hasVerifiedCheck,
1480
1507
  isAutoTier,
1481
1508
  isCardGatedModel,
1482
1509
  isLeapingIntoActionCommentBody,
@@ -23,6 +23,41 @@ export declare const TRANSIENT_REVIEW_RETRY_DELAYS_MS: number[];
23
23
  */
24
24
  export declare function commentableLinesForFile(patch: string | undefined): CommentableLines;
25
25
  export declare function buildCommentableMap(ctx: ToolContext, pullNumber: number): Promise<Map<string, CommentableLines>>;
26
+ /**
27
+ * count unresolved review threads on a PR whose root comment was authored by
28
+ * Pullfrog's bot. this is the full set of open Pullfrog findings on the PR —
29
+ * the basis for both the never-approve-with-outstanding-issues invariant (the
30
+ * approval gate below) and the `pullfrog-approval` status verdict. it is a
31
+ * function of all open findings, NOT just the latest commit's delta.
32
+ *
33
+ * outdated-but-unresolved threads still count — GitHub marks a thread
34
+ * `[OUTDATED]` when the anchor line moved (reformat, rename, force-push), which
35
+ * is not the same as the concern being addressed. human-reviewer threads are
36
+ * excluded: they belong to those reviewers to resolve. walks every page so a
37
+ * long-lived PR with >100 threads can't hide an outstanding finding.
38
+ */
39
+ export declare function countOutstandingPullfrogThreads(ctx: ToolContext, pullNumber: number): Promise<number>;
40
+ /**
41
+ * proactive approve-when-clean for the Fix-all / Fix-👍s flow (`fix_review`
42
+ * trigger). when such a run completes successfully and every Pullfrog-originated
43
+ * review thread it raised is resolved, post a NEW approving review summarizing
44
+ * the run's work.
45
+ *
46
+ * the verification step shares `countOutstandingPullfrogThreads` with the
47
+ * approval gate in create_pull_request_review: that gate REACTIVELY blocks a
48
+ * bad approval; this is the PROACTIVE approve side. approval is contingent on
49
+ * Pullfrog's own findings actually being addressed — never a blind post-fix
50
+ * approval. a Fix-👍s run that only resolved a subset naturally fails the
51
+ * outstanding-thread check and is left unapproved.
52
+ *
53
+ * respects `prApproveEnabled` (the repo's "Allow Pullfrog to approve PRs"
54
+ * opt-in): a repo that hasn't enabled binding bot approvals never gets one
55
+ * here. skips when the agent already rendered an explicit review verdict this
56
+ * run (`toolState.approval`), so an agent that found a real issue and submitted
57
+ * a non-approving review is never overridden. best-effort — a failure must not
58
+ * flip the run's outcome.
59
+ */
60
+ export declare function approveAfterFix(ctx: ToolContext): Promise<void>;
26
61
  export type ReviewCommentInput = NonNullable<RestEndpointMethodTypes["pulls"]["createReview"]["parameters"]["comments"]>[number];
27
62
  export interface DroppedComment {
28
63
  path: string;
@@ -155,6 +190,11 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
155
190
  }>> & {
156
191
  mutates?: boolean;
157
192
  };
193
+ type FooterOpts = {
194
+ body: string;
195
+ approved: boolean;
196
+ hasComments: boolean;
197
+ };
158
198
  /**
159
199
  * clear a pending review draft stranded on the PR by a prior hard-killed run
160
200
  * (workflow timeout, OOM) so the next createReview can succeed.
@@ -196,6 +236,7 @@ export declare function clearStrandedPendingReview(ctx: ToolContext, params: {
196
236
  * comments-only) until a body-path run happened to clear the draft.
197
237
  */
198
238
  export declare function createReviewWithStrandedRecovery(ctx: ToolContext, params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"]): Promise<Awaited<ReturnType<typeof ctx.octokit.rest.pulls.createReview>>>;
239
+ export declare function createAndSubmitWithFooter(ctx: ToolContext, params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"], opts: FooterOpts): Promise<Awaited<ReturnType<typeof ctx.octokit.rest.pulls.submitReview>>>;
199
240
  /**
200
241
  * report the review node ID so the WorkflowRun is marked as "review submitted".
201
242
  * exported for use in main.ts post-agent cleanup.
@@ -22,6 +22,7 @@ export interface ToolContext {
22
22
  postCheckoutScript: string | null;
23
23
  prepushScript: string | null;
24
24
  prApproveEnabled: boolean;
25
+ autoMergeEnabled: boolean;
25
26
  signedCommits: boolean;
26
27
  modeInstructions: Record<string, string>;
27
28
  toolState: ToolState;
@@ -100,6 +100,10 @@ export interface ToolState {
100
100
  nodeId: string;
101
101
  reviewedSha: string | undefined;
102
102
  };
103
+ approval?: {
104
+ wouldApprove: boolean;
105
+ sha: string | undefined;
106
+ };
103
107
  reviewReplies?: Map<number, {
104
108
  commentId: number;
105
109
  url: string | undefined;
@@ -10,7 +10,9 @@ import type { Agent } from "../agents/index.ts";
10
10
  * Bedrock model, change `BEDROCK_MODEL_ID`, not `PULLFROG_MODEL`.
11
11
  * 2. slug from repo config / payload → alias registry. routing slugs
12
12
  * (e.g. `bedrock/byok`) defer to a separate env var (`BEDROCK_MODEL_ID`).
13
- * 3. undefined agent will auto-select.
13
+ * a non-curated value with a slash passes through unchanged as a raw
14
+ * models.dev specifier (same as `PULLFROG_MODEL`), not auto-selected away.
15
+ * 3. undefined (no slug, or a bare-word non-specifier) — agent auto-selects.
14
16
  */
15
17
  export declare function resolveModel(ctx: {
16
18
  slug?: string | undefined;
@@ -0,0 +1,37 @@
1
+ import type { ToolContext } from "../mcp/server.ts";
2
+ /**
3
+ * Run-end lifecycle action: merge ANY open PR that this run mechanically
4
+ * approved on its current head — Pullfrog acting as a full repo maintainer,
5
+ * contributor PRs included, not just merging its own work. There is deliberately
6
+ * NO agent-callable merge tool — the merge is a pure, deterministic consequence
7
+ * of the runtime observing a clean approved state, so a prompt-injected agent has
8
+ * no merge surface to reach for. Every clause is fail-closed and re-verified
9
+ * against GitHub's own state at merge time; any failure logs why and returns
10
+ * without merging. Best-effort — the caller wraps this in a `.catch`, and a
11
+ * merge/comment failure can never flip the run's outcome.
12
+ *
13
+ * There is NO self-authorship restriction: the approval verdict IS the trust
14
+ * decision (Pullfrog reviewed it and would approve), exactly like a human
15
+ * maintainer merging a contributor PR. The population of mergeable PRs is bounded
16
+ * upstream by the review triggers (`prCreated` / `prCreatedAllowNonCollaborator`)
17
+ * — Pullfrog can only merge what it was configured to review + approve — and the
18
+ * whole capability is opt-in per repo (clause 1). The remaining clauses are the
19
+ * maintainer's controls.
20
+ *
21
+ * The invariant (all must hold):
22
+ * 1. `ctx.autoMergeEnabled` — the per-repo toggle ANDed with the global
23
+ * `isAutonomousMaintenanceEnabled()` kill switch, server-side (run-context).
24
+ * 2. `ctx.prApproveEnabled` — cannot auto-merge what it may not approve.
25
+ * 3. this run recorded an APPROVE verdict (`toolState.approval.wouldApprove`) —
26
+ * Pullfrog's review is the merge decision.
27
+ * 4. the PR is open and not a draft.
28
+ * 5. `toolState.approval.sha === <current head sha>` — approved THIS head, so a
29
+ * commit pushed after the review can't ride the approval in (409-safe below).
30
+ * 6. `countOutstandingPullfrogThreads === 0` — re-queried at merge time.
31
+ * 7. no un-dismissed human CHANGES_REQUESTED — the human veto always wins.
32
+ * 8. GitHub reports the PR mergeable and not blocked/dirty/behind.
33
+ * 9. every external check-run/commit-status on the head is complete and
34
+ * non-failing (`checkRunsGate`) — red or in-flight CI never auto-merges,
35
+ * even on a repo without branch protection.
36
+ */
37
+ export declare function autoMergeAfterApprove(ctx: ToolContext): Promise<void>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared, dependency-free CI green/red predicate — the single source of truth for
3
+ * "given these check-runs, is this PR head safe to merge?". Consumed by the
4
+ * auto-merge path: `autoMerge.ts` gates the PR head at merge time, and the
5
+ * `pr-merge-completion` webhook re-checks it before dispatching a slow-CI re-wake.
6
+ * A red or in-flight sha never merges, and a sha with no confirmable green
7
+ * check-run is treated as unverified (refused, not merged blind).
8
+ *
9
+ * Gates on the Checks API only. The Pullfrog App deliberately holds no `statuses`
10
+ * permission (requesting it would force re-approval on every install — see
11
+ * wiki/app-permissions.md), so the legacy Commit Statuses API is unreadable
12
+ * (`listCommitStatusesForRef` 403s). Modern CI (GitHub Actions, etc.) reports via
13
+ * check-runs; a repo relying on the legacy Statuses API is covered by branch
14
+ * protection instead — a failing required status makes `mergeable_state` `blocked`,
15
+ * which the merge invariant already refuses (`NON_MERGEABLE_STATES`).
16
+ */
17
+ export type CheckRun = {
18
+ name: string;
19
+ status: string;
20
+ conclusion: string | null;
21
+ };
22
+ /**
23
+ * Refuse if any external check-run is incomplete (queued/in_progress) or concluded
24
+ * anything other than success/neutral/skipped. Fail-closed on unknown/absent
25
+ * conclusions. Pure so the pass/fail logic is inspectable directly.
26
+ */
27
+ export declare function checkRunsGate(params: {
28
+ checkRuns: CheckRun[];
29
+ }): {
30
+ ok: boolean;
31
+ reason: string;
32
+ };
33
+ /**
34
+ * true when the head has ≥1 CONCLUDED non-failing EXTERNAL check-run — CI
35
+ * positively ran, not merely "no red was seen". paired with `checkRunsGate.ok`
36
+ * (any completed check is non-failing), this == "≥1 concluded non-failing check".
37
+ * the merge requires it: a head with ZERO check-runs passes
38
+ * `checkRunsGate` (nothing red) but has no verification, and since the app can't
39
+ * read legacy commit statuses, "zero check-runs" is indistinguishable from "green
40
+ * Actions but a red CircleCI status" — so we only act on a head we can positively
41
+ * confirm is green. Pullfrog's own verdict checks don't count as external CI.
42
+ */
43
+ export declare function hasVerifiedCheck(params: {
44
+ checkRuns: CheckRun[];
45
+ }): boolean;
@@ -8,3 +8,12 @@
8
8
  * substrings on `error.message` (e.g. provider-specific framings).
9
9
  */
10
10
  export declare function isTransientNetworkError(error: unknown, extraPatterns?: string[]): boolean;
11
+ /**
12
+ * broader transient predicate for octokit calls: an HTTP 5xx (GitHub upstream
13
+ * blip) OR a network-class error. distinct from `isTransientNetworkError` — an
14
+ * HTTP 4xx (404/401/403/422) is deterministic and fails fast. the action
15
+ * octokit carries only the throttling plugin + a 401 re-mint (no 5xx retry),
16
+ * so callers on critical paths retry idempotent GitHub reads/dispatches with
17
+ * this. mirrors the server's `isTransientUpstreamError`. see #999.
18
+ */
19
+ export declare function isTransientOctokitError(error: unknown): boolean;
@@ -25,17 +25,25 @@ export declare const JsonPayload: import("arktype/internal/variants/object.ts").
25
25
  generateSummary?: boolean | undefined;
26
26
  }, {}>;
27
27
  export declare const Inputs: import("arktype/internal/variants/object.ts").ObjectType<{
28
- prompt: string;
28
+ prompt?: string | undefined;
29
+ prompt_file?: string | undefined;
29
30
  model?: string | undefined;
30
31
  timeout?: string | undefined;
31
32
  push?: "disabled" | "enabled" | "restricted" | undefined;
32
33
  shell?: "disabled" | "enabled" | "restricted" | undefined;
34
+ status_checks?: "disabled" | "enabled" | undefined;
33
35
  cwd?: string | undefined;
34
36
  output_schema?: string | undefined;
35
37
  }, {}>;
36
38
  export type Inputs = typeof Inputs.infer;
37
39
  export type ResolvedPromptInput = string | typeof JsonPayload.infer;
38
40
  export declare function resolvePromptInput(): ResolvedPromptInput;
41
+ /**
42
+ * true when `actor` is Pullfrog's own GitHub identity — the bot login
43
+ * (`pullfrog[bot]` / `pullfrogdev[bot]`) or the bare account. used to skip
44
+ * self-triggered events and to recognize Pullfrog-originated review threads.
45
+ */
46
+ export declare const isPullfrog: (actor: string | null | undefined) => boolean;
39
47
  export declare function resolvePayload(resolvedPromptInput: ResolvedPromptInput, repoSettings: RepoSettings): {
40
48
  "~pullfrog": true;
41
49
  version: string;
@@ -62,6 +70,7 @@ export declare function resolvePayload(resolvedPromptInput: ResolvedPromptInput,
62
70
  generateSummary: boolean | undefined;
63
71
  push: import("../external.ts").PushPermission;
64
72
  shell: import("../external.ts").ShellPermission;
73
+ statusChecks: boolean;
65
74
  proxyModel: string | undefined;
66
75
  };
67
76
  export type ResolvedPayload = ReturnType<typeof resolvePayload>;
@@ -31,6 +31,7 @@ export interface RepoSettings {
31
31
  push: PushPermission;
32
32
  shell: ShellPermission;
33
33
  prApproveEnabled: boolean;
34
+ autoMergeEnabled: boolean;
34
35
  signedCommits: boolean;
35
36
  modeInstructions: Record<string, string>;
36
37
  learnings: string | null;
@@ -0,0 +1,25 @@
1
+ import type { ToolContext } from "../mcp/server.ts";
2
+ /**
3
+ * post the opt-in `pullfrog` (run completion) and `pullfrog-approval` (review
4
+ * verdict) commit-status check-runs so they can be required by branch
5
+ * protection. no-op unless the `status_checks` input is enabled and the run is
6
+ * on a pull request.
7
+ *
8
+ * terminal-only by design: we never create an `in_progress` check. a
9
+ * hard-cancelled run (SIGKILL) would strand a required check `in_progress` and
10
+ * block merges forever; an absent required check already blocks merge the same
11
+ * way while the run is in flight, with no stuck-check failure mode.
12
+ *
13
+ * - `pullfrog` is posted on every PR run: success iff the run finished
14
+ * successfully, failure on error/timeout. review-verdict-agnostic.
15
+ * - `pullfrog-approval` is posted only when this run produced an approval
16
+ * verdict (`toolState.approval`, set by create_pull_request_review),
17
+ * anchored to the reviewed sha so a mid-run push leaves the new head
18
+ * unapproved until the follow-up re-review reports.
19
+ *
20
+ * best-effort throughout: a check-post failure (fork PR head not in the base
21
+ * repo, transient 5xx, closed PR) must never flip the run's own outcome.
22
+ */
23
+ export declare function reportStatusChecks(ctx: ToolContext, params: {
24
+ runSucceeded: boolean;
25
+ }): Promise<void>;
@@ -4,6 +4,21 @@ export declare function range(n: number): number[];
4
4
  export declare function range(start: number, end: number): number[];
5
5
  export declare function range(n: number, fn: (i: number) => number): number[];
6
6
  export declare function schedule(fn: (i: number) => number, count: number): number[];
7
+ /**
8
+ * read a retry delay (ms) from a thrown HTTP error's response headers, honoring
9
+ * GitHub's rate-limit backoff hints on octokit errors (`error.response.headers`):
10
+ * - `retry-after` (RFC 9110 delta-seconds or an HTTP-date) — an explicit
11
+ * backoff directive whenever present (covers secondary rate limits, which
12
+ * carry it with `x-ratelimit-remaining > 0`).
13
+ * - else `x-ratelimit-reset` (epoch seconds) but ONLY when
14
+ * `x-ratelimit-remaining` is `"0"` — GitHub sends the reset header on ~every
15
+ * response (~1h ahead), so it's a backoff hint only under actual primary
16
+ * rate-limit exhaustion (matching octokit's throttling plugin). without this
17
+ * gate a non-rate-limit retried error (e.g. a permissions 403) would be
18
+ * wrongly delayed to the reset time.
19
+ * returns `undefined` when no applicable hint is present/parseable.
20
+ */
21
+ export declare function retryAfterMs(error: unknown): number | undefined;
7
22
  type InferInput<S> = S extends StandardSchemaV1<infer I, any> ? I : never;
8
23
  type InferOutput<S> = S extends StandardSchemaV1<any, infer O> ? O : never;
9
24
  export interface OpOptions<TReturn = any> {
@@ -11,6 +26,17 @@ export interface OpOptions<TReturn = any> {
11
26
  ttl?: number;
12
27
  maxItems?: number;
13
28
  retries?: number[];
29
+ /**
30
+ * honor a server backoff hint on retries: when a retried error carries a
31
+ * `retry-after` / `x-ratelimit-reset` header, schedule the next retry from
32
+ * that value (clamped to `[fixed delay, retryAfterCap]`) instead of the fixed
33
+ * `retries[attempt]` backoff. `true` uses the built-in `retryAfterMs` reader;
34
+ * a function extracts a delay-ms from a custom error shape. errors without the
35
+ * header fall back to the fixed backoff.
36
+ */
37
+ retryAfter?: boolean | ((error: unknown) => number | undefined);
38
+ /** cap (ms) on a honored retry-after hint. default 20000. */
39
+ retryAfterCap?: number;
14
40
  cacheHit?: ((key: string) => void) | null;
15
41
  cacheMiss?: ((key: string) => void) | null;
16
42
  skipCache?: (result: TReturn) => boolean;
package/dist/yes.js CHANGED
@@ -1022,6 +1022,34 @@ function range(a, b) {
1022
1022
  function schedule(fn, count) {
1023
1023
  return Array.from({ length: count }, (_, i) => fn(i));
1024
1024
  }
1025
+ var DEFAULT_RETRY_AFTER_CAP_MS = 2e4;
1026
+ function getErrorResponseHeaders(error) {
1027
+ if (error === null || typeof error !== "object") return void 0;
1028
+ const response = error.response;
1029
+ if (response === null || typeof response !== "object") return void 0;
1030
+ const headers = response.headers;
1031
+ if (headers === null || typeof headers !== "object") return void 0;
1032
+ return headers;
1033
+ }
1034
+ function retryAfterMs(error) {
1035
+ const headers = getErrorResponseHeaders(error);
1036
+ if (!headers) return void 0;
1037
+ const retryAfter = headers["retry-after"];
1038
+ if (typeof retryAfter === "string" && retryAfter.trim() !== "") {
1039
+ const seconds = Number(retryAfter);
1040
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
1041
+ const dateMs = Date.parse(retryAfter);
1042
+ if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
1043
+ }
1044
+ if (headers["x-ratelimit-remaining"] === "0") {
1045
+ const reset = headers["x-ratelimit-reset"];
1046
+ if (typeof reset === "string" && reset.trim() !== "") {
1047
+ const resetEpoch = Number(reset);
1048
+ if (Number.isFinite(resetEpoch)) return Math.max(0, resetEpoch * 1e3 - Date.now());
1049
+ }
1050
+ }
1051
+ return void 0;
1052
+ }
1025
1053
  function validateSchema(schema, value) {
1026
1054
  const result = schema["~standard"].validate(value);
1027
1055
  if (result instanceof Promise) {
@@ -1159,7 +1187,15 @@ function _op(fn, options) {
1159
1187
  log.info({ key, error, attempt });
1160
1188
  }
1161
1189
  } else {
1162
- const delay = retries[attempt];
1190
+ let delay = retries[attempt];
1191
+ if (options.retryAfter) {
1192
+ const extract = options.retryAfter === true ? retryAfterMs : options.retryAfter;
1193
+ const hint = extract(error);
1194
+ if (hint !== void 0) {
1195
+ const cap = options.retryAfterCap ?? DEFAULT_RETRY_AFTER_CAP_MS;
1196
+ delay = Math.max(delay, Math.min(cap, hint));
1197
+ }
1198
+ }
1163
1199
  if (namePrefix) {
1164
1200
  log.info(
1165
1201
  `${namePrefix}attempt ${attempt + 1}/${retries.length + 1} failed, retrying in ${delay}ms`
@@ -1240,5 +1276,6 @@ function _op(fn, options) {
1240
1276
  export {
1241
1277
  op,
1242
1278
  range,
1279
+ retryAfterMs,
1243
1280
  schedule
1244
1281
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",