pullfrog 0.1.89 → 0.1.91

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/index.js CHANGED
@@ -83904,7 +83904,7 @@ var require_Element = __commonJS({
83904
83904
  var FilteredElementList = require_FilteredElementList();
83905
83905
  var DOMException2 = require_DOMException();
83906
83906
  var DOMTokenList = require_DOMTokenList();
83907
- var select = require_select();
83907
+ var select2 = require_select();
83908
83908
  var ContainerNode = require_ContainerNode();
83909
83909
  var ChildNode = require_ChildNode();
83910
83910
  var NonDocumentTypeChildNode = require_NonDocumentTypeChildNode();
@@ -84665,7 +84665,7 @@ var require_Element = __commonJS({
84665
84665
  this.className = v;
84666
84666
  } },
84667
84667
  matches: { value: function(selector) {
84668
- return select.matches(this, selector);
84668
+ return select2.matches(this, selector);
84669
84669
  } },
84670
84670
  closest: { value: function(selector) {
84671
84671
  var el = this;
@@ -84678,10 +84678,10 @@ var require_Element = __commonJS({
84678
84678
  return null;
84679
84679
  } },
84680
84680
  querySelector: { value: function(selector) {
84681
- return select(selector, this)[0];
84681
+ return select2(selector, this)[0];
84682
84682
  } },
84683
84683
  querySelectorAll: { value: function(selector) {
84684
- var nodes = select(selector, this);
84684
+ var nodes = select2(selector, this);
84685
84685
  return nodes.item ? nodes : new NodeList(nodes);
84686
84686
  } }
84687
84687
  });
@@ -85209,7 +85209,7 @@ var require_DocumentFragment = __commonJS({
85209
85209
  var NodeList = require_NodeList();
85210
85210
  var ContainerNode = require_ContainerNode();
85211
85211
  var Element = require_Element();
85212
- var select = require_select();
85212
+ var select2 = require_select();
85213
85213
  var utils = require_utils8();
85214
85214
  function DocumentFragment(doc) {
85215
85215
  ContainerNode.call(this);
@@ -85238,7 +85238,7 @@ var require_DocumentFragment = __commonJS({
85238
85238
  context.isHTML = true;
85239
85239
  context.getElementsByTagName = Element.prototype.getElementsByTagName;
85240
85240
  context.nextElement = Object.getOwnPropertyDescriptor(Element.prototype, "firstElementChild").get;
85241
- var nodes = select(selector, context);
85241
+ var nodes = select2(selector, context);
85242
85242
  return nodes.item ? nodes : new NodeList(nodes);
85243
85243
  } },
85244
85244
  // Utility methods
@@ -88353,7 +88353,7 @@ var require_Document = __commonJS({
88353
88353
  var NodeIterator = require_NodeIterator();
88354
88354
  var NodeFilter = require_NodeFilter();
88355
88355
  var URL2 = require_URL();
88356
- var select = require_select();
88356
+ var select2 = require_select();
88357
88357
  var events = require_events3();
88358
88358
  var xml = require_xmlnames();
88359
88359
  var html = require_htmlelts();
@@ -88914,10 +88914,10 @@ var require_Document = __commonJS({
88914
88914
  return this._templateDocCache;
88915
88915
  } },
88916
88916
  querySelector: { value: function(selector) {
88917
- return select(selector, this)[0];
88917
+ return select2(selector, this)[0];
88918
88918
  } },
88919
88919
  querySelectorAll: { value: function(selector) {
88920
- var nodes = select(selector, this);
88920
+ var nodes = select2(selector, this);
88921
88921
  return nodes.item ? nodes : new NodeList(nodes);
88922
88922
  } }
88923
88923
  });
@@ -105082,10 +105082,12 @@ async function preflightClaudeSubscription(params) {
105082
105082
  if (res.status !== 401 && res.status !== 403 && res.status !== 429)
105083
105083
  return { usable: true, status: res.status };
105084
105084
  const body = await res.text().catch(() => "");
105085
+ const reset = Number(res.headers.get("anthropic-ratelimit-unified-reset"));
105085
105086
  return {
105086
105087
  usable: false,
105087
105088
  status: res.status,
105088
- reason: `${res.status}: ${extractApiErrorMessage(body)}`
105089
+ reason: `${res.status}: ${extractApiErrorMessage(body)}`,
105090
+ resetAt: res.status === 429 && reset > 0 ? new Date(reset * 1e3) : void 0
105089
105091
  };
105090
105092
  }
105091
105093
  function extractApiErrorMessage(body) {
@@ -105729,7 +105731,7 @@ var import_semver = __toESM(require_semver2(), 1);
105729
105731
  // package.json
105730
105732
  var package_default = {
105731
105733
  name: "pullfrog",
105732
- version: "0.1.89",
105734
+ version: "0.1.91",
105733
105735
  cliContractVersion: "1",
105734
105736
  type: "module",
105735
105737
  bin: {
@@ -108001,6 +108003,17 @@ function parseXaiAuthBody(raw2) {
108001
108003
  var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
108002
108004
  var XAI_AUTH_ENV = "GROK_AUTH_JSON";
108003
108005
  var installedSubscriptionPaths = /* @__PURE__ */ new Map();
108006
+ function clearInstalledSubscription(name) {
108007
+ const path4 = installedSubscriptionPaths.get(name);
108008
+ if (!path4 || !existsSync4(path4)) return;
108009
+ const value2 = JSON.parse(readFileSync2(path4, "utf8"));
108010
+ if (!value2 || typeof value2 !== "object") return;
108011
+ if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
108012
+ if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
108013
+ writeFileSync4(path4, `${JSON.stringify(value2)}
108014
+ `, { mode: 384 });
108015
+ installedSubscriptionPaths.delete(name);
108016
+ }
108004
108017
  var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
108005
108018
  function isRejectedChain(body) {
108006
108019
  if (!body.refresh_rejected_at) return false;
@@ -116693,6 +116706,9 @@ function captureAuthorizedModels(cliPath) {
116693
116706
  }
116694
116707
  log.debug(`\xBB opencode authorized: ${authorized.size} models`);
116695
116708
  }
116709
+ function authorizeModel(model) {
116710
+ getAuthorizedModels().add(model);
116711
+ }
116696
116712
  function getAuthorizedModels() {
116697
116713
  if (!authorized) {
116698
116714
  throw new Error("getAuthorizedModels called before captureAuthorizedModels");
@@ -130812,8 +130828,8 @@ function liftWireOnlyMaterial(message, kind) {
130812
130828
  };
130813
130829
  }
130814
130830
  function codecResultValidator(codec2, method) {
130815
- const probe = codec2.validateResult(method, void 0);
130816
- if (!probe.ok && probe.reason === "not-in-era") return void 0;
130831
+ const probe2 = codec2.validateResult(method, void 0);
130832
+ if (!probe2.ok && probe2.reason === "not-in-era") return void 0;
130817
130833
  return { "~standard": {
130818
130834
  version: 1,
130819
130835
  vendor: "mcp-wire-codec",
@@ -145189,8 +145205,8 @@ function liftWireOnlyMaterial2(message, kind) {
145189
145205
  };
145190
145206
  }
145191
145207
  function codecResultValidator2(codec2, method) {
145192
- const probe = codec2.validateResult(method, void 0);
145193
- if (!probe.ok && probe.reason === "not-in-era") return void 0;
145208
+ const probe2 = codec2.validateResult(method, void 0);
145209
+ if (!probe2.ok && probe2.reason === "not-in-era") return void 0;
145194
145210
  return { "~standard": {
145195
145211
  version: 1,
145196
145212
  vendor: "mcp-wire-codec",
@@ -188369,10 +188385,10 @@ function CommitChangesTool(ctx) {
188369
188385
  }
188370
188386
  const requested = new Set(params.files);
188371
188387
  const known = new Set(changes.map((c) => c.path));
188372
- const unknown4 = [...requested].filter((p) => !known.has(p));
188373
- if (unknown4.length > 0) {
188388
+ const unknown5 = [...requested].filter((p) => !known.has(p));
188389
+ if (unknown5.length > 0) {
188374
188390
  throw new Error(
188375
- `no detected change at: ${unknown4.join(", ")} \u2014 run git status to list changed paths.`
188391
+ `no detected change at: ${unknown5.join(", ")} \u2014 run git status to list changed paths.`
188376
188392
  );
188377
188393
  }
188378
188394
  changes = changes.filter((c) => requested.has(c.path));
@@ -188765,15 +188781,6 @@ function PushTagsTool(ctx) {
188765
188781
  // utils/buildPullfrogFooter.ts
188766
188782
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
188767
188783
  var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
188768
- function providerDisplayName(slug2) {
188769
- try {
188770
- const key = getModelProvider(slug2);
188771
- const meta3 = providers[key];
188772
- return meta3?.displayName ?? key;
188773
- } catch {
188774
- return slug2;
188775
- }
188776
- }
188777
188784
  function formatModelLabel(params) {
188778
188785
  const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
188779
188786
  // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
@@ -188788,9 +188795,6 @@ function formatModelLabel(params) {
188788
188795
  return `${ossBase} (${configured} not used \u2014 pick one of the [funded models](https://docs.pullfrog.com/models#pullfrog-for-oss) or add a [provider key](https://docs.pullfrog.com/keys) to run your own)`;
188789
188796
  }
188790
188797
  const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
188791
- if (params.fallbackFrom) {
188792
- return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
188793
- }
188794
188798
  if (params.clamped?.reason === "trial") {
188795
188799
  return `${base} (model usage covered by Pullfrog)`;
188796
188800
  }
@@ -188831,7 +188835,6 @@ function buildPullfrogFooter(params) {
188831
188835
  parts.push(
188832
188836
  `Using ${formatModelLabel({
188833
188837
  model: params.model,
188834
- fallbackFrom: params.fallbackFrom,
188835
188838
  clamped: params.clamped,
188836
188839
  unselectedProxyDefault: params.unselectedProxyDefault,
188837
188840
  oss: params.oss
@@ -189031,7 +189034,6 @@ function buildCommentFooter(ctx, customParts) {
189031
189034
  } : void 0,
189032
189035
  customParts,
189033
189036
  model: ctx.toolState.model,
189034
- fallbackFrom: ctx.toolState.modelFallback?.from,
189035
189037
  clamped: ctx.toolState.modelClamped,
189036
189038
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
189037
189039
  oss: ctx.oss,
@@ -190021,7 +190023,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
190021
190023
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
190022
190024
  customParts,
190023
190025
  model: ctx.toolState.model,
190024
- fallbackFrom: ctx.toolState.modelFallback?.from,
190025
190026
  clamped: ctx.toolState.modelClamped,
190026
190027
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
190027
190028
  oss: ctx.oss,
@@ -192397,7 +192398,6 @@ function buildPrBodyWithFooter(ctx, body) {
192397
192398
  triggeredBy: true,
192398
192399
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
192399
192400
  model: ctx.toolState.model,
192400
- fallbackFrom: ctx.toolState.modelFallback?.from,
192401
192401
  clamped: ctx.toolState.modelClamped,
192402
192402
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
192403
192403
  oss: ctx.oss,
@@ -193165,8 +193165,9 @@ function ResolveReviewThreadTool(ctx) {
193165
193165
  }
193166
193166
 
193167
193167
  // mcp/searchIssues.ts
193168
+ var TERM = /^[\p{L}\p{N}_./-]+$/u;
193168
193169
  var SearchIssues = type({
193169
- terms: type.string.matching(/^[\p{L}\p{N}_./-]+$/u).atMostLength(40).array().atLeastLength(1).atMostLength(5).describe("1-5 keywords from the issue; no GitHub qualifiers or query operators")
193170
+ terms: type.string.atMostLength(40).array().atLeastLength(1).atMostLength(5).describe("1-5 keywords from the issue; no GitHub qualifiers or query operators")
193170
193171
  });
193171
193172
  function SearchIssuesTool(ctx) {
193172
193173
  return tool({
@@ -193174,6 +193175,10 @@ function SearchIssuesTool(ctx) {
193174
193175
  description: "Find open or closed issues in this repository by keyword. Inspect promising matches with get_issue before deciding whether they are duplicates or related. Results are bounded, not an exhaustive duplicate check.",
193175
193176
  parameters: SearchIssues,
193176
193177
  execute: execute(async (params) => {
193178
+ const invalid = params.terms.find((term) => !TERM.test(term));
193179
+ if (invalid) {
193180
+ throw new Error(`search term "${invalid}" may contain only letters, digits and _ . / -`);
193181
+ }
193177
193182
  const repoPath = `/repos/${ctx.repo.owner}/${ctx.repo.name}`.toLowerCase();
193178
193183
  const terms = params.terms.map((term) => `"${term}"`).join(" ");
193179
193184
  const response = await ctx.octokit.rest.search.issuesAndPullRequests({
@@ -194541,17 +194546,17 @@ async function verifyCredential(params) {
194541
194546
  const value2 = params.value.trim();
194542
194547
  if (value2.length === 0) return "dead";
194543
194548
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") return verifyClaudeSubscription(value2);
194544
- const probe = PROBES[params.envVar];
194545
- if (!probe) return "unknown";
194546
- const request2 = probe.request(value2);
194549
+ const probe2 = PROBES[params.envVar];
194550
+ if (!probe2) return "unknown";
194551
+ const request2 = probe2.request(value2);
194547
194552
  try {
194548
194553
  const response = await fetch(request2.url, {
194549
194554
  headers: request2.headers,
194550
194555
  signal: AbortSignal.timeout(5e3)
194551
194556
  });
194552
194557
  if (response.ok) return "alive";
194553
- if (probe.deadWhen) {
194554
- return probe.deadWhen(response.status, await response.text()) ? "dead" : "unknown";
194558
+ if (probe2.deadWhen) {
194559
+ return probe2.deadWhen(response.status, await response.text()) ? "dead" : "unknown";
194555
194560
  }
194556
194561
  return response.status === 401 ? "dead" : "unknown";
194557
194562
  } catch {
@@ -194582,14 +194587,6 @@ async function checkOne(params) {
194582
194587
  const verdict = await verifyCredential({ envVar: params.envVar, value: value2 });
194583
194588
  return verdict === "dead" ? { severity: "dead", reason: void 0 } : null;
194584
194589
  }
194585
- function pickReplacement(params) {
194586
- const candidates = modelAliases.filter(
194587
- (alias) => !alias.hidden && !alias.fallback && params.authorized.has(alias.resolve) && // a model with no known env vars is routable through credentials we can't
194588
- // see (Codex auth lands as an on-disk auth.json), so it stays a candidate.
194589
- !envVarsFor(alias.resolve).some((envVar) => params.deadVars.has(envVar))
194590
- );
194591
- return (candidates.find((alias) => alias.preferred) ?? candidates[0])?.resolve;
194592
- }
194593
194590
  async function checkConfiguredCredentials(params) {
194594
194591
  if (!params.model) return { kind: "ok" };
194595
194592
  const present = envVarsFor(params.model).filter(hasEnvVar4);
@@ -194615,12 +194612,396 @@ async function checkConfiguredCredentials(params) {
194615
194612
  failed.forEach(drop);
194616
194613
  const first = failed[0];
194617
194614
  if (!first) return { kind: "ok" };
194618
- const outcome = { credential: first.envVar, reason: first.failure.reason };
194619
- const replacement = pickReplacement({
194620
- authorized: params.authorized,
194621
- deadVars: new Set(failed.map((entry) => entry.envVar))
194615
+ return { kind: "dead", credential: first.envVar, reason: first.failure.reason };
194616
+ }
194617
+
194618
+ // utils/normalizeEnv.ts
194619
+ function hasControlCharacter(value2) {
194620
+ return [...value2].some((ch) => {
194621
+ const code = ch.charCodeAt(0);
194622
+ return code < 32 || code === 127;
194623
+ });
194624
+ }
194625
+ function sanitizeSecret(key, value2) {
194626
+ const trimmed = value2.trim();
194627
+ if (trimmed.length === 0) {
194628
+ log.warning(
194629
+ `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
194630
+ );
194631
+ return null;
194632
+ }
194633
+ if (key.endsWith("_API_KEY") && hasControlCharacter(trimmed)) {
194634
+ log.warning(
194635
+ `\xBB ${key} contains a line break or control character and cannot be sent as an HTTP header \u2014 leaving env var unchanged. re-save the secret as a single line.`
194636
+ );
194637
+ return null;
194638
+ }
194639
+ if (trimmed !== value2) {
194640
+ log.warning(
194641
+ `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
194642
+ );
194643
+ }
194644
+ maskSecret(trimmed);
194645
+ return trimmed;
194646
+ }
194647
+ function normalizeEnv() {
194648
+ const upperKeys = /* @__PURE__ */ new Map();
194649
+ for (const key of Object.keys(process.env)) {
194650
+ const upper2 = key.toUpperCase();
194651
+ const existing = upperKeys.get(upper2) || [];
194652
+ existing.push(key);
194653
+ upperKeys.set(upper2, existing);
194654
+ }
194655
+ for (const [upperKey, keys] of upperKeys) {
194656
+ if (keys.length === 1) {
194657
+ const key = keys[0];
194658
+ if (key !== upperKey) {
194659
+ process.env[upperKey] = process.env[key];
194660
+ delete process.env[key];
194661
+ }
194662
+ continue;
194663
+ }
194664
+ const values = keys.map((k2) => process.env[k2]);
194665
+ const uniqueValues = new Set(values);
194666
+ if (uniqueValues.size > 1) {
194667
+ log.warning(
194668
+ `env var conflict: ${keys.join(", ")} have different values. using uppercase ${upperKey}.`
194669
+ );
194670
+ }
194671
+ const preferredKey = keys.find((k2) => k2 === upperKey) || keys[0];
194672
+ const preferredValue = process.env[preferredKey];
194673
+ for (const key of keys) {
194674
+ delete process.env[key];
194675
+ }
194676
+ process.env[upperKey] = preferredValue;
194677
+ }
194678
+ for (const key of Object.keys(process.env)) {
194679
+ if (!isSensitiveEnvName(key)) continue;
194680
+ const value2 = process.env[key];
194681
+ if (typeof value2 !== "string" || value2.length === 0) continue;
194682
+ const sanitized = sanitizeSecret(key, value2);
194683
+ if (sanitized !== null) process.env[key] = sanitized;
194684
+ }
194685
+ }
194686
+
194687
+ // utils/subscriptionCredentials.ts
194688
+ var subscriptionNameSchema = external_exports.enum([
194689
+ "CLAUDE_CODE_OAUTH_TOKEN",
194690
+ "CODEX_AUTH_JSON",
194691
+ "GROK_AUTH_JSON"
194692
+ ]);
194693
+ var subscriptionSchema = external_exports.object({
194694
+ id: external_exports.string(),
194695
+ credentialId: external_exports.string(),
194696
+ name: subscriptionNameSchema,
194697
+ label: external_exports.string(),
194698
+ owner: external_exports.string(),
194699
+ inherited: external_exports.boolean(),
194700
+ shared: external_exports.boolean(),
194701
+ registeredAt: external_exports.string(),
194702
+ /** the provider's last verdict on the current value; null = usable or never checked. */
194703
+ status: external_exports.enum(["rejected", "exhausted"]).nullish(),
194704
+ resetAt: external_exports.string().nullish()
194705
+ });
194706
+ var subscriptionListSchema = external_exports.object({
194707
+ subscriptions: external_exports.array(subscriptionSchema),
194708
+ writable: external_exports.boolean()
194709
+ });
194710
+ var subscriptionInputSchema = external_exports.object({
194711
+ name: subscriptionNameSchema,
194712
+ value: external_exports.string().trim().min(1).refine((value2) => Buffer.byteLength(value2, "utf8") <= 49152),
194713
+ replaceId: external_exports.string().optional()
194714
+ });
194715
+ var credentialCandidateSchema = external_exports.object({
194716
+ id: external_exports.string(),
194717
+ name: external_exports.string(),
194718
+ source: external_exports.enum(["repo", "account", "personal"])
194719
+ });
194720
+ var credentialAccessSchema = external_exports.object({
194721
+ token: external_exports.string(),
194722
+ candidates: external_exports.array(credentialCandidateSchema)
194723
+ });
194724
+ var selectedCredentialSchema = external_exports.object({
194725
+ name: external_exports.string(),
194726
+ value: external_exports.string(),
194727
+ receipt: external_exports.string()
194728
+ });
194729
+
194730
+ // utils/subscriptionProbe.ts
194731
+ var identitySchema = external_exports.object({
194732
+ email: external_exports.string().email().optional(),
194733
+ sub: external_exports.string().optional(),
194734
+ user_id: external_exports.string().optional(),
194735
+ account_id: external_exports.string().optional(),
194736
+ email_verified: external_exports.boolean().optional()
194737
+ });
194738
+ var codexWindowSchema = external_exports.object({ used_percent: external_exports.number().optional(), reset_at: external_exports.number().optional() }).nullish();
194739
+ var codexUsageSchema = external_exports.object({
194740
+ rate_limit: external_exports.object({
194741
+ allowed: external_exports.boolean(),
194742
+ limit_reached: external_exports.boolean(),
194743
+ primary_window: codexWindowSchema,
194744
+ secondary_window: codexWindowSchema
194745
+ }).optional()
194746
+ });
194747
+ var unknown4 = { status: "unknown" };
194748
+ async function readSubscriptionEndpoint(input) {
194749
+ const headers = new Headers({ authorization: `Bearer ${input.token}` });
194750
+ if (input.accountId) headers.set("ChatGPT-Account-Id", input.accountId);
194751
+ if (input.grokBilling) headers.set("x-xai-token-auth", "xai-grok-cli");
194752
+ return fetch(input.url, {
194753
+ headers,
194754
+ redirect: "error",
194755
+ signal: AbortSignal.timeout(1e4)
194756
+ }).catch(() => null);
194757
+ }
194758
+ async function probeSubscription(input) {
194759
+ if (input.name === "CLAUDE_CODE_OAUTH_TOKEN") {
194760
+ const result = await preflightClaudeSubscription({ token: input.value, model: input.model });
194761
+ if (result.usable)
194762
+ return result.status !== void 0 && result.status < 300 ? { status: "usable" } : unknown4;
194763
+ return result.status === 429 ? { status: "exhausted", detail: result.reason, resetAt: result.resetAt } : { status: "rejected", detail: result.reason };
194764
+ }
194765
+ const codex2 = input.name === "CODEX_AUTH_JSON" ? parseCodexAuthBody(input.value) : null;
194766
+ const grok = input.name === "GROK_AUTH_JSON" ? parseXaiAuthBody(input.value) : null;
194767
+ const auth2 = codex2 ?? grok;
194768
+ if (!auth2) return { status: "rejected", detail: "the stored credential is malformed" };
194769
+ if (auth2.refresh_rejected_at)
194770
+ return { status: "rejected", detail: `refresh rejected at ${auth2.refresh_rejected_at}` };
194771
+ if (grok && input.model)
194772
+ return probeInference({
194773
+ name: "XAI_API_KEY",
194774
+ value: grok.tokens.access_token,
194775
+ model: input.model
194776
+ });
194777
+ const response = await readSubscriptionEndpoint({
194778
+ url: codex2 ? "https://chatgpt.com/backend-api/wham/usage" : "https://cli-chat-proxy.grok.com/v1/billing?format=credits",
194779
+ token: auth2.tokens.access_token,
194780
+ accountId: codex2?.tokens.account_id,
194781
+ grokBilling: !!grok
194782
+ });
194783
+ if (response?.status === 401) return { status: "rejected", detail: "rejected with 401" };
194784
+ if (!response?.ok) return unknown4;
194785
+ const body = await response.json().catch(() => null);
194786
+ if (!codex2) return unknown4;
194787
+ const parsed2 = codexUsageSchema.safeParse(body);
194788
+ const limit = parsed2.success ? parsed2.data.rate_limit : void 0;
194789
+ if (!limit) return unknown4;
194790
+ if (limit.allowed && !limit.limit_reached) return { status: "usable" };
194791
+ const windows = [limit.primary_window, limit.secondary_window];
194792
+ const full = windows.filter((window2) => (window2?.used_percent ?? 0) >= 100);
194793
+ const resets = (full.length ? full : windows).flatMap(
194794
+ (window2) => window2?.reset_at ? [window2.reset_at] : []
194795
+ );
194796
+ return {
194797
+ status: "exhausted",
194798
+ detail: "the ChatGPT plan's usage limit is reached",
194799
+ resetAt: resets.length ? new Date(Math.max(...resets) * 1e3) : void 0
194800
+ };
194801
+ }
194802
+ async function probeInference(input) {
194803
+ const anthropic = input.name === "ANTHROPIC_API_KEY";
194804
+ const openai = input.name === "OPENAI_API_KEY";
194805
+ if (!anthropic && !openai && input.name !== "XAI_API_KEY") {
194806
+ const result = await verifyCredential({ envVar: input.name, value: input.value });
194807
+ return result === "dead" ? { status: "rejected", detail: "rejected by its provider" } : result === "alive" ? { status: "usable" } : unknown4;
194808
+ }
194809
+ const headers = new Headers({ "content-type": "application/json" });
194810
+ if (anthropic) {
194811
+ headers.set("x-api-key", input.value);
194812
+ headers.set("anthropic-version", "2023-06-01");
194813
+ } else headers.set("authorization", `Bearer ${input.value}`);
194814
+ const response = await fetch(
194815
+ anthropic ? "https://api.anthropic.com/v1/messages" : openai ? "https://api.openai.com/v1/responses" : "https://api.x.ai/v1/chat/completions",
194816
+ {
194817
+ method: "POST",
194818
+ headers,
194819
+ body: JSON.stringify(
194820
+ openai ? { model: input.model, input: "Reply OK.", max_output_tokens: 16, store: false } : {
194821
+ model: input.model,
194822
+ messages: [{ role: "user", content: "Reply OK." }],
194823
+ max_tokens: 1
194824
+ }
194825
+ ),
194826
+ redirect: "error",
194827
+ signal: AbortSignal.timeout(1e4)
194828
+ }
194829
+ ).catch(() => null);
194830
+ if (response?.status === 400 && input.name === "XAI_API_KEY") {
194831
+ const error49 = external_exports.object({ error: external_exports.string() }).safeParse(await response.json().catch(() => null));
194832
+ return error49.success && error49.data.error.startsWith("Incorrect API key provided.") ? { status: "rejected", detail: "rejected with 400: incorrect API key" } : unknown4;
194833
+ }
194834
+ if (response?.status === 429 && openai) {
194835
+ const error49 = external_exports.object({ error: external_exports.object({ code: external_exports.string() }) }).safeParse(await response.json().catch(() => null));
194836
+ return error49.success && error49.data.error.code === "insufficient_quota" ? { status: "exhausted", detail: "refused with 429: insufficient quota", resetAt: void 0 } : unknown4;
194837
+ }
194838
+ await response?.body?.cancel();
194839
+ if (!response) return unknown4;
194840
+ if (response.ok) return { status: "usable" };
194841
+ if (response.status === 401 || response.status === 403)
194842
+ return { status: "rejected", detail: `rejected with ${response.status}` };
194843
+ if (response.status === 402)
194844
+ return { status: "exhausted", detail: "refused with 402", resetAt: void 0 };
194845
+ return unknown4;
194846
+ }
194847
+
194848
+ // utils/credentialPool.ts
194849
+ var receipts = {};
194850
+ var workflowCredentials = {};
194851
+ function saveReceipts() {
194852
+ for (const receipt of Object.values(receipts)) maskSecret(receipt);
194853
+ saveSecretState("credential_receipts", JSON.stringify(receipts));
194854
+ }
194855
+ var select = op(
194856
+ async (input) => {
194857
+ const response = await apiFetch({
194858
+ path: "/api/runtime/credentials",
194859
+ method: "POST",
194860
+ headers: {
194861
+ authorization: `Bearer ${input.access.token}`,
194862
+ "content-type": "application/json"
194863
+ },
194864
+ body: JSON.stringify({ id: input.candidate.id }),
194865
+ signal: AbortSignal.timeout(35e3)
194866
+ });
194867
+ if (response.status === 404) return null;
194868
+ if (!response.ok)
194869
+ throw new Error("could not load a configured credential; no fallback was charged", {
194870
+ cause: response.status
194871
+ });
194872
+ return selectedCredentialSchema.parse(await response.json());
194873
+ },
194874
+ {
194875
+ retries: [250, 1e3],
194876
+ bail: (error49) => error49 instanceof Error && typeof error49.cause === "number" && error49.cause >= 400 && error49.cause < 500
194877
+ }
194878
+ );
194879
+ function subscriptionForModel(model) {
194880
+ if (!model.includes("/")) return null;
194881
+ const provider2 = getModelProvider(model);
194882
+ return provider2 === "anthropic" ? "CLAUDE_CODE_OAUTH_TOKEN" : provider2 === "openai" ? "CODEX_AUTH_JSON" : provider2 === "xai" ? "GROK_AUTH_JSON" : null;
194883
+ }
194884
+ async function initializeCredentialPool(input) {
194885
+ const access = input.ctx.credentialAccess;
194886
+ if (!access) return;
194887
+ const model = discoveryModel(input.slug);
194888
+ maskSecret(access.token);
194889
+ const names = /* @__PURE__ */ new Set([
194890
+ ...access.candidates.map((candidate) => candidate.name),
194891
+ ...subscriptionNameSchema.options,
194892
+ "ANTHROPIC_API_KEY",
194893
+ "OPENAI_API_KEY",
194894
+ "XAI_API_KEY"
194895
+ ]);
194896
+ for (const name of names) {
194897
+ const value2 = process.env[name];
194898
+ if (value2 && !input.injected.includes(name)) workflowCredentials[name] = value2;
194899
+ }
194900
+ for (const name of subscriptionNameSchema.options) {
194901
+ if (model && subscriptionForModel(model) !== name) continue;
194902
+ if (workflowCredentials[name]) continue;
194903
+ const candidate = access.candidates.find((item) => item.name === name);
194904
+ if (!candidate) continue;
194905
+ const selected = await select({ access, candidate }).catch(() => {
194906
+ log.warning(`\xBB could not load ${name} for model discovery; selection will retry if needed`);
194907
+ return null;
194908
+ });
194909
+ if (!selected) continue;
194910
+ const value2 = sanitizeSecret(name, selected.value);
194911
+ if (!value2) continue;
194912
+ process.env[name] = value2;
194913
+ receipts[name] = selected.receipt;
194914
+ }
194915
+ saveReceipts();
194916
+ }
194917
+ function discoveryModel(slug2) {
194918
+ const value2 = process.env.PULLFROG_MODEL?.trim() || slug2?.trim();
194919
+ if (!value2) return void 0;
194920
+ return resolveCliModel(value2) ?? (value2.includes("/") ? value2 : void 0);
194921
+ }
194922
+ function resolvePoolModel(input) {
194923
+ const agent2 = resolveAgent({
194924
+ model: void 0,
194925
+ proxyModel: void 0,
194926
+ codexAgent: input.codexAgent
194622
194927
  });
194623
- return replacement ? { kind: "fellBack", ...outcome, replacement } : { kind: "dead", ...outcome };
194928
+ if (agent2.name === "opencode") return autoSelectModel();
194929
+ const provider2 = agent2.name === "claude" ? "anthropic" : "openai";
194930
+ return modelAliases.find(
194931
+ (alias) => alias.provider === provider2 && alias.preferred && !alias.hidden && !alias.fallback && !alias.routing
194932
+ )?.resolve;
194933
+ }
194934
+ async function selectConfiguredCredential(input) {
194935
+ const access = input.ctx.credentialAccess;
194936
+ const model = input.model;
194937
+ if (!access || !model?.includes("/") || getProviderGatewayUrl(model)) return false;
194938
+ const subscription = subscriptionForModel(model);
194939
+ const names = getModelEnvVars(model).filter((name) => name !== subscription);
194940
+ if (subscription) names.unshift(subscription);
194941
+ const candidates = access.candidates.filter((candidate) => names.includes(candidate.name));
194942
+ if (!candidates.length) return false;
194943
+ const refused = [];
194944
+ for (const name of names.filter((item) => workflowCredentials[item])) {
194945
+ const value2 = workflowCredentials[name];
194946
+ const verdict = await probe({ name, value: value2, model });
194947
+ if (usable(verdict)) {
194948
+ activate({ names, name, value: value2, receipt: void 0, model });
194949
+ return true;
194950
+ }
194951
+ refused.push(`${name} from the workflow: ${verdict.detail}`);
194952
+ log.info(`\xBB ${name} from the workflow unavailable; trying the next credential`);
194953
+ }
194954
+ for (const candidate of candidates) {
194955
+ const selected = await select({ access, candidate });
194956
+ if (!selected) continue;
194957
+ const verdict = await probe({ ...selected, model });
194958
+ await reportVerdict({ receipt: selected.receipt, verdict });
194959
+ if (usable(verdict)) {
194960
+ activate({ names, ...selected, model });
194961
+ log.info(`\xBB selected ${candidate.name} from ${candidate.source} scope`);
194962
+ return true;
194963
+ }
194964
+ refused.push(`${candidate.name} from ${candidate.source} scope: ${verdict.detail}`);
194965
+ log.info(
194966
+ `\xBB ${candidate.name} from ${candidate.source} scope unavailable; trying the next credential`
194967
+ );
194968
+ }
194969
+ throw new Error(
194970
+ `all configured credentials for ${model} were rejected or exhausted (${refused.join("; ")}); no other model or provider was selected`
194971
+ );
194972
+ }
194973
+ async function probe(input) {
194974
+ const subscription = subscriptionNameSchema.safeParse(input.name);
194975
+ const model = stripProviderPrefix(input.model);
194976
+ return subscription.success ? probeSubscription({ name: subscription.data, value: input.value, model }) : probeInference({ ...input, model });
194977
+ }
194978
+ function usable(verdict) {
194979
+ return verdict.status !== "rejected" && verdict.status !== "exhausted";
194980
+ }
194981
+ async function reportVerdict(input) {
194982
+ if (input.verdict.status === "unknown") return;
194983
+ await apiFetch({
194984
+ path: "/api/runtime/credentials/status",
194985
+ method: "POST",
194986
+ headers: { authorization: `Bearer ${input.receipt}`, "content-type": "application/json" },
194987
+ body: JSON.stringify(input.verdict),
194988
+ signal: AbortSignal.timeout(5e3)
194989
+ }).catch(() => log.debug("\xBB could not record a credential verdict"));
194990
+ }
194991
+ function activate(input) {
194992
+ for (const name of input.names) {
194993
+ delete process.env[name];
194994
+ delete receipts[name];
194995
+ if (name === "CODEX_AUTH_JSON" || name === "GROK_AUTH_JSON") clearInstalledSubscription(name);
194996
+ }
194997
+ const value2 = sanitizeSecret(input.name, input.value);
194998
+ if (!value2) throw new Error("configured credential was empty");
194999
+ process.env[input.name] = value2;
195000
+ if (input.receipt) receipts[input.name] = input.receipt;
195001
+ installCodexAuth();
195002
+ installXaiAuth();
195003
+ saveReceipts();
195004
+ authorizeModel(input.model);
194624
195005
  }
194625
195006
 
194626
195007
  // utils/gitAuthServer.ts
@@ -195326,75 +195707,6 @@ function buildModelAccessError(input) {
195326
195707
  return [headline, "", why, "", cta].join("\n");
195327
195708
  }
195328
195709
 
195329
- // utils/normalizeEnv.ts
195330
- function hasControlCharacter(value2) {
195331
- return [...value2].some((ch) => {
195332
- const code = ch.charCodeAt(0);
195333
- return code < 32 || code === 127;
195334
- });
195335
- }
195336
- function sanitizeSecret(key, value2) {
195337
- const trimmed = value2.trim();
195338
- if (trimmed.length === 0) {
195339
- log.warning(
195340
- `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
195341
- );
195342
- return null;
195343
- }
195344
- if (key.endsWith("_API_KEY") && hasControlCharacter(trimmed)) {
195345
- log.warning(
195346
- `\xBB ${key} contains a line break or control character and cannot be sent as an HTTP header \u2014 leaving env var unchanged. re-save the secret as a single line.`
195347
- );
195348
- return null;
195349
- }
195350
- if (trimmed !== value2) {
195351
- log.warning(
195352
- `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
195353
- );
195354
- }
195355
- maskSecret(trimmed);
195356
- return trimmed;
195357
- }
195358
- function normalizeEnv() {
195359
- const upperKeys = /* @__PURE__ */ new Map();
195360
- for (const key of Object.keys(process.env)) {
195361
- const upper2 = key.toUpperCase();
195362
- const existing = upperKeys.get(upper2) || [];
195363
- existing.push(key);
195364
- upperKeys.set(upper2, existing);
195365
- }
195366
- for (const [upperKey, keys] of upperKeys) {
195367
- if (keys.length === 1) {
195368
- const key = keys[0];
195369
- if (key !== upperKey) {
195370
- process.env[upperKey] = process.env[key];
195371
- delete process.env[key];
195372
- }
195373
- continue;
195374
- }
195375
- const values = keys.map((k2) => process.env[k2]);
195376
- const uniqueValues = new Set(values);
195377
- if (uniqueValues.size > 1) {
195378
- log.warning(
195379
- `env var conflict: ${keys.join(", ")} have different values. using uppercase ${upperKey}.`
195380
- );
195381
- }
195382
- const preferredKey = keys.find((k2) => k2 === upperKey) || keys[0];
195383
- const preferredValue = process.env[preferredKey];
195384
- for (const key of keys) {
195385
- delete process.env[key];
195386
- }
195387
- process.env[upperKey] = preferredValue;
195388
- }
195389
- for (const key of Object.keys(process.env)) {
195390
- if (!isSensitiveEnvName(key)) continue;
195391
- const value2 = process.env[key];
195392
- if (typeof value2 !== "string" || value2.length === 0) continue;
195393
- const sanitized = sanitizeSecret(key, value2);
195394
- if (sanitized !== null) process.env[key] = sanitized;
195395
- }
195396
- }
195397
-
195398
195710
  // utils/overrides.ts
195399
195711
  var DENIED_OVERRIDE_NAMES = /* @__PURE__ */ new Set([
195400
195712
  "GITHUB_TOKEN",
@@ -195965,7 +196277,6 @@ ${ctx.error}` : ctx.error;
195965
196277
  workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
195966
196278
  customParts,
195967
196279
  model: ctx.toolState.model,
195968
- fallbackFrom: ctx.toolState.modelFallback?.from,
195969
196280
  clamped: ctx.toolState.modelClamped,
195970
196281
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
195971
196282
  oss: ctx.toolState.oss,
@@ -196349,7 +196660,8 @@ async function fetchRunContext(params) {
196349
196660
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
196350
196661
  try {
196351
196662
  const headers = {
196352
- Authorization: `Bearer ${params.token}`
196663
+ Authorization: `Bearer ${params.token}`,
196664
+ "X-Pullfrog-Credential-Pools": "1"
196353
196665
  };
196354
196666
  if (params.oidcToken) {
196355
196667
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
@@ -197646,16 +197958,20 @@ async function main() {
197646
197958
  const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
197647
197959
  const opencodeCliPath = await agents.opencode.install();
197648
197960
  captureBaselineModels(opencodeCliPath);
197961
+ const injected = [];
197649
197962
  if (runContext.dbSecrets) {
197650
197963
  for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
197651
197964
  if (!process.env[key]) {
197652
197965
  const sanitized = sanitizeSecret(key, value2);
197653
- if (sanitized !== null) process.env[key] = sanitized;
197966
+ if (sanitized === null) continue;
197967
+ process.env[key] = sanitized;
197968
+ injected.push(key);
197654
197969
  }
197655
197970
  }
197656
197971
  const count = Object.keys(runContext.dbSecrets).length;
197657
197972
  if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`);
197658
197973
  }
197974
+ await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
197659
197975
  installCodexAuth();
197660
197976
  installXaiAuth();
197661
197977
  captureAuthorizedModels(opencodeCliPath);
@@ -197729,13 +198045,23 @@ async function main() {
197729
198045
  }
197730
198046
  const gitAuthServer = __using(_stack2, await startGitAuthServer(tmpdir3), true);
197731
198047
  setGitAuthServer(gitAuthServer);
198048
+ let resolvedModel = resolveModel({ slug: payload.model });
198049
+ if (!resolvedModel && !payload.proxyModel && runContext.credentialAccess?.candidates.length) {
198050
+ resolvedModel = resolvePoolModel({
198051
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
198052
+ });
198053
+ }
198054
+ let poolSelected = !payload.proxyModel && await selectConfiguredCredential({
198055
+ ctx: runContext,
198056
+ model: resolvedModel
198057
+ });
197732
198058
  const access = decideModelAccess({
197733
198059
  modelExplicit: payload.modelExplicit ?? false,
197734
198060
  model: payload.model,
197735
198061
  oss: runContext.oss,
197736
198062
  proxyActive: !!payload.proxyModel,
197737
198063
  subsidyTarget: runContext.proxyModel,
197738
- resolvedModel: resolveModel({ slug: payload.model }),
198064
+ resolvedModel,
197739
198065
  authorized: getAuthorizedModels()
197740
198066
  });
197741
198067
  if (access.kind === "error") {
@@ -197749,15 +198075,19 @@ async function main() {
197749
198075
  );
197750
198076
  }
197751
198077
  if (access.kind === "proxy") payload.proxyModel = access.target;
197752
- if (access.kind === "byok") payload.proxyModel = void 0;
198078
+ if (access.kind === "byok") {
198079
+ payload.proxyModel = void 0;
198080
+ if (!poolSelected)
198081
+ poolSelected = await selectConfiguredCredential({
198082
+ ctx: runContext,
198083
+ model: resolvedModel
198084
+ });
198085
+ }
197753
198086
  if (runContext.oss && payload.proxyModel) {
197754
198087
  payload.effort = ossEffortFloor({ payload });
197755
198088
  }
197756
- const configuredModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
197757
- const credentials = payload.proxyModel ? { kind: "ok" } : await checkConfiguredCredentials({
197758
- model: configuredModel,
197759
- authorized: getAuthorizedModels()
197760
- });
198089
+ if (payload.proxyModel) resolvedModel = void 0;
198090
+ const credentials = payload.proxyModel || poolSelected ? { kind: "ok" } : await checkConfiguredCredentials({ model: resolvedModel });
197761
198091
  if (credentials.kind === "dead") {
197762
198092
  throw new Error(
197763
198093
  buildRejectedCredentialError({
@@ -197769,13 +198099,6 @@ async function main() {
197769
198099
  })
197770
198100
  );
197771
198101
  }
197772
- if (credentials.kind === "fellBack" && configuredModel) {
197773
- log.warning(
197774
- `\xBB ${credentials.credential} was rejected by its provider \u2014 running ${credentials.replacement} instead of ${configuredModel}`
197775
- );
197776
- toolState.modelFallback = { from: configuredModel };
197777
- }
197778
- const resolvedModel = credentials.kind === "fellBack" ? credentials.replacement : configuredModel;
197779
198102
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
197780
198103
  let agent2 = resolveAgent({
197781
198104
  model: resolvedModel,
@@ -198085,12 +198408,12 @@ ${instructions.user}` : null,
198085
198408
  if (payload.timeout === TIMEOUT_DISABLED) {
198086
198409
  result = await Promise.race([agentPromise, activityTimeout.promise]);
198087
198410
  } else {
198088
- const usable = resolveTimeoutMs(payload.timeout);
198089
- if (payload.timeout && usable === null) {
198411
+ const usable2 = resolveTimeoutMs(payload.timeout);
198412
+ if (payload.timeout && usable2 === null) {
198090
198413
  log.warning(`invalid timeout "${payload.timeout}" (use --notimeout to disable), using 1h`);
198091
198414
  }
198092
- const timeoutMs = usable ?? 36e5;
198093
- const actualTimeout = usable !== null ? payload.timeout : "1h";
198415
+ const timeoutMs = usable2 ?? 36e5;
198416
+ const actualTimeout = usable2 !== null ? payload.timeout : "1h";
198094
198417
  let timeoutId;
198095
198418
  const timeoutPromise = new Promise((_4, reject) => {
198096
198419
  timeoutId = setTimeout(() => {