pullfrog 0.1.90 → 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/cli.mjs CHANGED
@@ -84177,7 +84177,7 @@ var require_Element = __commonJS({
84177
84177
  var FilteredElementList = require_FilteredElementList();
84178
84178
  var DOMException2 = require_DOMException();
84179
84179
  var DOMTokenList = require_DOMTokenList();
84180
- var select2 = require_select();
84180
+ var select3 = require_select();
84181
84181
  var ContainerNode = require_ContainerNode();
84182
84182
  var ChildNode = require_ChildNode();
84183
84183
  var NonDocumentTypeChildNode = require_NonDocumentTypeChildNode();
@@ -84938,7 +84938,7 @@ var require_Element = __commonJS({
84938
84938
  this.className = v;
84939
84939
  } },
84940
84940
  matches: { value: function(selector) {
84941
- return select2.matches(this, selector);
84941
+ return select3.matches(this, selector);
84942
84942
  } },
84943
84943
  closest: { value: function(selector) {
84944
84944
  var el = this;
@@ -84951,10 +84951,10 @@ var require_Element = __commonJS({
84951
84951
  return null;
84952
84952
  } },
84953
84953
  querySelector: { value: function(selector) {
84954
- return select2(selector, this)[0];
84954
+ return select3(selector, this)[0];
84955
84955
  } },
84956
84956
  querySelectorAll: { value: function(selector) {
84957
- var nodes = select2(selector, this);
84957
+ var nodes = select3(selector, this);
84958
84958
  return nodes.item ? nodes : new NodeList(nodes);
84959
84959
  } }
84960
84960
  });
@@ -85482,7 +85482,7 @@ var require_DocumentFragment = __commonJS({
85482
85482
  var NodeList = require_NodeList();
85483
85483
  var ContainerNode = require_ContainerNode();
85484
85484
  var Element = require_Element();
85485
- var select2 = require_select();
85485
+ var select3 = require_select();
85486
85486
  var utils = require_utils8();
85487
85487
  function DocumentFragment(doc) {
85488
85488
  ContainerNode.call(this);
@@ -85511,7 +85511,7 @@ var require_DocumentFragment = __commonJS({
85511
85511
  context.isHTML = true;
85512
85512
  context.getElementsByTagName = Element.prototype.getElementsByTagName;
85513
85513
  context.nextElement = Object.getOwnPropertyDescriptor(Element.prototype, "firstElementChild").get;
85514
- var nodes = select2(selector, context);
85514
+ var nodes = select3(selector, context);
85515
85515
  return nodes.item ? nodes : new NodeList(nodes);
85516
85516
  } },
85517
85517
  // Utility methods
@@ -88626,7 +88626,7 @@ var require_Document = __commonJS({
88626
88626
  var NodeIterator = require_NodeIterator();
88627
88627
  var NodeFilter = require_NodeFilter();
88628
88628
  var URL2 = require_URL();
88629
- var select2 = require_select();
88629
+ var select3 = require_select();
88630
88630
  var events = require_events3();
88631
88631
  var xml = require_xmlnames();
88632
88632
  var html = require_htmlelts();
@@ -89187,10 +89187,10 @@ var require_Document = __commonJS({
89187
89187
  return this._templateDocCache;
89188
89188
  } },
89189
89189
  querySelector: { value: function(selector) {
89190
- return select2(selector, this)[0];
89190
+ return select3(selector, this)[0];
89191
89191
  } },
89192
89192
  querySelectorAll: { value: function(selector) {
89193
- var nodes = select2(selector, this);
89193
+ var nodes = select3(selector, this);
89194
89194
  return nodes.item ? nodes : new NodeList(nodes);
89195
89195
  } }
89196
89196
  });
@@ -107103,7 +107103,7 @@ var import_picocolors = __toESM(require_picocolors(), 1);
107103
107103
  // package.json
107104
107104
  var package_default = {
107105
107105
  name: "pullfrog",
107106
- version: "0.1.90",
107106
+ version: "0.1.91",
107107
107107
  cliContractVersion: "1",
107108
107108
  type: "module",
107109
107109
  bin: {
@@ -107478,7 +107478,10 @@ var subscriptionSchema = external_exports.object({
107478
107478
  owner: external_exports.string(),
107479
107479
  inherited: external_exports.boolean(),
107480
107480
  shared: external_exports.boolean(),
107481
- registeredAt: external_exports.string()
107481
+ registeredAt: external_exports.string(),
107482
+ /** the provider's last verdict on the current value; null = usable or never checked. */
107483
+ status: external_exports.enum(["rejected", "exhausted"]).nullish(),
107484
+ resetAt: external_exports.string().nullish()
107482
107485
  });
107483
107486
  var subscriptionListSchema = external_exports.object({
107484
107487
  subscriptions: external_exports.array(subscriptionSchema),
@@ -110796,10 +110799,12 @@ async function preflightClaudeSubscription(params) {
110796
110799
  if (res.status !== 401 && res.status !== 403 && res.status !== 429)
110797
110800
  return { usable: true, status: res.status };
110798
110801
  const body = await res.text().catch(() => "");
110802
+ const reset = Number(res.headers.get("anthropic-ratelimit-unified-reset"));
110799
110803
  return {
110800
110804
  usable: false,
110801
110805
  status: res.status,
110802
- reason: `${res.status}: ${extractApiErrorMessage(body)}`
110806
+ reason: `${res.status}: ${extractApiErrorMessage(body)}`,
110807
+ resetAt: res.status === 429 && reset > 0 ? new Date(reset * 1e3) : void 0
110803
110808
  };
110804
110809
  }
110805
110810
  function extractApiErrorMessage(body) {
@@ -113524,6 +113529,17 @@ import { join as join6 } from "node:path";
113524
113529
  var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
113525
113530
  var XAI_AUTH_ENV = "GROK_AUTH_JSON";
113526
113531
  var installedSubscriptionPaths = /* @__PURE__ */ new Map();
113532
+ function clearInstalledSubscription(name) {
113533
+ const path4 = installedSubscriptionPaths.get(name);
113534
+ if (!path4 || !existsSync5(path4)) return;
113535
+ const value2 = JSON.parse(readFileSync3(path4, "utf8"));
113536
+ if (!value2 || typeof value2 !== "object") return;
113537
+ if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
113538
+ if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
113539
+ writeFileSync5(path4, `${JSON.stringify(value2)}
113540
+ `, { mode: 384 });
113541
+ installedSubscriptionPaths.delete(name);
113542
+ }
113527
113543
  var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
113528
113544
  function isRejectedChain(body) {
113529
113545
  if (!body.refresh_rejected_at) return false;
@@ -113854,8 +113870,8 @@ async function writeBackEntry(apiToken, entry) {
113854
113870
  }
113855
113871
  try {
113856
113872
  const poolState = core2.getState("credential_receipts");
113857
- const receipts = external_exports.record(external_exports.string(), external_exports.string()).parse(JSON.parse(poolState || "{}"));
113858
- const receipt = receipts[entry.secretName];
113873
+ const receipts2 = external_exports.record(external_exports.string(), external_exports.string()).parse(JSON.parse(poolState || "{}"));
113874
+ const receipt = receipts2[entry.secretName];
113859
113875
  if (poolState && !receipt) return;
113860
113876
  const response = await apiFetch({
113861
113877
  path: receipt ? "/api/runtime/credentials" : "/api/runtime/secret",
@@ -120200,6 +120216,9 @@ function captureAuthorizedModels(cliPath) {
120200
120216
  }
120201
120217
  log2.debug(`\xBB opencode authorized: ${authorized.size} models`);
120202
120218
  }
120219
+ function authorizeModel(model) {
120220
+ getAuthorizedModels().add(model);
120221
+ }
120203
120222
  function getAuthorizedModels() {
120204
120223
  if (!authorized) {
120205
120224
  throw new Error("getAuthorizedModels called before captureAuthorizedModels");
@@ -134319,8 +134338,8 @@ function liftWireOnlyMaterial(message, kind) {
134319
134338
  };
134320
134339
  }
134321
134340
  function codecResultValidator(codec2, method) {
134322
- const probe = codec2.validateResult(method, void 0);
134323
- if (!probe.ok && probe.reason === "not-in-era") return void 0;
134341
+ const probe2 = codec2.validateResult(method, void 0);
134342
+ if (!probe2.ok && probe2.reason === "not-in-era") return void 0;
134324
134343
  return { "~standard": {
134325
134344
  version: 1,
134326
134345
  vendor: "mcp-wire-codec",
@@ -148696,8 +148715,8 @@ function liftWireOnlyMaterial2(message, kind) {
148696
148715
  };
148697
148716
  }
148698
148717
  function codecResultValidator2(codec2, method) {
148699
- const probe = codec2.validateResult(method, void 0);
148700
- if (!probe.ok && probe.reason === "not-in-era") return void 0;
148718
+ const probe2 = codec2.validateResult(method, void 0);
148719
+ if (!probe2.ok && probe2.reason === "not-in-era") return void 0;
148701
148720
  return { "~standard": {
148702
148721
  version: 1,
148703
148722
  vendor: "mcp-wire-codec",
@@ -191876,10 +191895,10 @@ function CommitChangesTool(ctx) {
191876
191895
  }
191877
191896
  const requested = new Set(params.files);
191878
191897
  const known = new Set(changes.map((c2) => c2.path));
191879
- const unknown4 = [...requested].filter((p) => !known.has(p));
191880
- if (unknown4.length > 0) {
191898
+ const unknown5 = [...requested].filter((p) => !known.has(p));
191899
+ if (unknown5.length > 0) {
191881
191900
  throw new Error(
191882
- `no detected change at: ${unknown4.join(", ")} \u2014 run git status to list changed paths.`
191901
+ `no detected change at: ${unknown5.join(", ")} \u2014 run git status to list changed paths.`
191883
191902
  );
191884
191903
  }
191885
191904
  changes = changes.filter((c2) => requested.has(c2.path));
@@ -192272,15 +192291,6 @@ function PushTagsTool(ctx) {
192272
192291
  // utils/buildPullfrogFooter.ts
192273
192292
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
192274
192293
  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>`;
192275
- function providerDisplayName(slug2) {
192276
- try {
192277
- const key = getModelProvider(slug2);
192278
- const meta3 = providers[key];
192279
- return meta3?.displayName ?? key;
192280
- } catch {
192281
- return slug2;
192282
- }
192283
- }
192284
192294
  function formatModelLabel(params) {
192285
192295
  const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
192286
192296
  // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
@@ -192295,9 +192305,6 @@ function formatModelLabel(params) {
192295
192305
  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)`;
192296
192306
  }
192297
192307
  const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
192298
- if (params.fallbackFrom) {
192299
- return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
192300
- }
192301
192308
  if (params.clamped?.reason === "trial") {
192302
192309
  return `${base} (model usage covered by Pullfrog)`;
192303
192310
  }
@@ -192338,7 +192345,6 @@ function buildPullfrogFooter(params) {
192338
192345
  parts.push(
192339
192346
  `Using ${formatModelLabel({
192340
192347
  model: params.model,
192341
- fallbackFrom: params.fallbackFrom,
192342
192348
  clamped: params.clamped,
192343
192349
  unselectedProxyDefault: params.unselectedProxyDefault,
192344
192350
  oss: params.oss
@@ -192538,7 +192544,6 @@ function buildCommentFooter(ctx, customParts) {
192538
192544
  } : void 0,
192539
192545
  customParts,
192540
192546
  model: ctx.toolState.model,
192541
- fallbackFrom: ctx.toolState.modelFallback?.from,
192542
192547
  clamped: ctx.toolState.modelClamped,
192543
192548
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
192544
192549
  oss: ctx.oss,
@@ -193528,7 +193533,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
193528
193533
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
193529
193534
  customParts,
193530
193535
  model: ctx.toolState.model,
193531
- fallbackFrom: ctx.toolState.modelFallback?.from,
193532
193536
  clamped: ctx.toolState.modelClamped,
193533
193537
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
193534
193538
  oss: ctx.oss,
@@ -195904,7 +195908,6 @@ function buildPrBodyWithFooter(ctx, body) {
195904
195908
  triggeredBy: true,
195905
195909
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
195906
195910
  model: ctx.toolState.model,
195907
- fallbackFrom: ctx.toolState.modelFallback?.from,
195908
195911
  clamped: ctx.toolState.modelClamped,
195909
195912
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
195910
195913
  oss: ctx.oss,
@@ -198053,17 +198056,17 @@ async function verifyCredential(params) {
198053
198056
  const value2 = params.value.trim();
198054
198057
  if (value2.length === 0) return "dead";
198055
198058
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") return verifyClaudeSubscription(value2);
198056
- const probe = PROBES[params.envVar];
198057
- if (!probe) return "unknown";
198058
- const request2 = probe.request(value2);
198059
+ const probe2 = PROBES[params.envVar];
198060
+ if (!probe2) return "unknown";
198061
+ const request2 = probe2.request(value2);
198059
198062
  try {
198060
198063
  const response = await fetch(request2.url, {
198061
198064
  headers: request2.headers,
198062
198065
  signal: AbortSignal.timeout(5e3)
198063
198066
  });
198064
198067
  if (response.ok) return "alive";
198065
- if (probe.deadWhen) {
198066
- return probe.deadWhen(response.status, await response.text()) ? "dead" : "unknown";
198068
+ if (probe2.deadWhen) {
198069
+ return probe2.deadWhen(response.status, await response.text()) ? "dead" : "unknown";
198067
198070
  }
198068
198071
  return response.status === 401 ? "dead" : "unknown";
198069
198072
  } catch {
@@ -198094,14 +198097,6 @@ async function checkOne(params) {
198094
198097
  const verdict = await verifyCredential({ envVar: params.envVar, value: value2 });
198095
198098
  return verdict === "dead" ? { severity: "dead", reason: void 0 } : null;
198096
198099
  }
198097
- function pickReplacement(params) {
198098
- const candidates = modelAliases.filter(
198099
- (alias) => !alias.hidden && !alias.fallback && params.authorized.has(alias.resolve) && // a model with no known env vars is routable through credentials we can't
198100
- // see (Codex auth lands as an on-disk auth.json), so it stays a candidate.
198101
- !envVarsFor(alias.resolve).some((envVar) => params.deadVars.has(envVar))
198102
- );
198103
- return (candidates.find((alias) => alias.preferred) ?? candidates[0])?.resolve;
198104
- }
198105
198100
  async function checkConfiguredCredentials(params) {
198106
198101
  if (!params.model) return { kind: "ok" };
198107
198102
  const present = envVarsFor(params.model).filter(hasEnvVar4);
@@ -198127,12 +198122,353 @@ async function checkConfiguredCredentials(params) {
198127
198122
  failed.forEach(drop);
198128
198123
  const first = failed[0];
198129
198124
  if (!first) return { kind: "ok" };
198130
- const outcome = { credential: first.envVar, reason: first.failure.reason };
198131
- const replacement = pickReplacement({
198132
- authorized: params.authorized,
198133
- deadVars: new Set(failed.map((entry) => entry.envVar))
198125
+ return { kind: "dead", credential: first.envVar, reason: first.failure.reason };
198126
+ }
198127
+
198128
+ // utils/normalizeEnv.ts
198129
+ function hasControlCharacter(value2) {
198130
+ return [...value2].some((ch) => {
198131
+ const code = ch.charCodeAt(0);
198132
+ return code < 32 || code === 127;
198133
+ });
198134
+ }
198135
+ function sanitizeSecret(key, value2) {
198136
+ const trimmed = value2.trim();
198137
+ if (trimmed.length === 0) {
198138
+ log2.warning(
198139
+ `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
198140
+ );
198141
+ return null;
198142
+ }
198143
+ if (key.endsWith("_API_KEY") && hasControlCharacter(trimmed)) {
198144
+ log2.warning(
198145
+ `\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.`
198146
+ );
198147
+ return null;
198148
+ }
198149
+ if (trimmed !== value2) {
198150
+ log2.warning(
198151
+ `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
198152
+ );
198153
+ }
198154
+ maskSecret(trimmed);
198155
+ return trimmed;
198156
+ }
198157
+ function normalizeEnv() {
198158
+ const upperKeys = /* @__PURE__ */ new Map();
198159
+ for (const key of Object.keys(process.env)) {
198160
+ const upper2 = key.toUpperCase();
198161
+ const existing = upperKeys.get(upper2) || [];
198162
+ existing.push(key);
198163
+ upperKeys.set(upper2, existing);
198164
+ }
198165
+ for (const [upperKey, keys] of upperKeys) {
198166
+ if (keys.length === 1) {
198167
+ const key = keys[0];
198168
+ if (key !== upperKey) {
198169
+ process.env[upperKey] = process.env[key];
198170
+ delete process.env[key];
198171
+ }
198172
+ continue;
198173
+ }
198174
+ const values = keys.map((k2) => process.env[k2]);
198175
+ const uniqueValues = new Set(values);
198176
+ if (uniqueValues.size > 1) {
198177
+ log2.warning(
198178
+ `env var conflict: ${keys.join(", ")} have different values. using uppercase ${upperKey}.`
198179
+ );
198180
+ }
198181
+ const preferredKey = keys.find((k2) => k2 === upperKey) || keys[0];
198182
+ const preferredValue = process.env[preferredKey];
198183
+ for (const key of keys) {
198184
+ delete process.env[key];
198185
+ }
198186
+ process.env[upperKey] = preferredValue;
198187
+ }
198188
+ for (const key of Object.keys(process.env)) {
198189
+ if (!isSensitiveEnvName(key)) continue;
198190
+ const value2 = process.env[key];
198191
+ if (typeof value2 !== "string" || value2.length === 0) continue;
198192
+ const sanitized = sanitizeSecret(key, value2);
198193
+ if (sanitized !== null) process.env[key] = sanitized;
198194
+ }
198195
+ }
198196
+
198197
+ // utils/subscriptionProbe.ts
198198
+ var identitySchema = external_exports.object({
198199
+ email: external_exports.string().email().optional(),
198200
+ sub: external_exports.string().optional(),
198201
+ user_id: external_exports.string().optional(),
198202
+ account_id: external_exports.string().optional(),
198203
+ email_verified: external_exports.boolean().optional()
198204
+ });
198205
+ var codexWindowSchema = external_exports.object({ used_percent: external_exports.number().optional(), reset_at: external_exports.number().optional() }).nullish();
198206
+ var codexUsageSchema = external_exports.object({
198207
+ rate_limit: external_exports.object({
198208
+ allowed: external_exports.boolean(),
198209
+ limit_reached: external_exports.boolean(),
198210
+ primary_window: codexWindowSchema,
198211
+ secondary_window: codexWindowSchema
198212
+ }).optional()
198213
+ });
198214
+ var unknown4 = { status: "unknown" };
198215
+ async function readSubscriptionEndpoint(input) {
198216
+ const headers = new Headers({ authorization: `Bearer ${input.token}` });
198217
+ if (input.accountId) headers.set("ChatGPT-Account-Id", input.accountId);
198218
+ if (input.grokBilling) headers.set("x-xai-token-auth", "xai-grok-cli");
198219
+ return fetch(input.url, {
198220
+ headers,
198221
+ redirect: "error",
198222
+ signal: AbortSignal.timeout(1e4)
198223
+ }).catch(() => null);
198224
+ }
198225
+ async function probeSubscription(input) {
198226
+ if (input.name === "CLAUDE_CODE_OAUTH_TOKEN") {
198227
+ const result = await preflightClaudeSubscription({ token: input.value, model: input.model });
198228
+ if (result.usable)
198229
+ return result.status !== void 0 && result.status < 300 ? { status: "usable" } : unknown4;
198230
+ return result.status === 429 ? { status: "exhausted", detail: result.reason, resetAt: result.resetAt } : { status: "rejected", detail: result.reason };
198231
+ }
198232
+ const codex2 = input.name === "CODEX_AUTH_JSON" ? parseCodexAuthBody(input.value) : null;
198233
+ const grok = input.name === "GROK_AUTH_JSON" ? parseXaiAuthBody(input.value) : null;
198234
+ const auth2 = codex2 ?? grok;
198235
+ if (!auth2) return { status: "rejected", detail: "the stored credential is malformed" };
198236
+ if (auth2.refresh_rejected_at)
198237
+ return { status: "rejected", detail: `refresh rejected at ${auth2.refresh_rejected_at}` };
198238
+ if (grok && input.model)
198239
+ return probeInference({
198240
+ name: "XAI_API_KEY",
198241
+ value: grok.tokens.access_token,
198242
+ model: input.model
198243
+ });
198244
+ const response = await readSubscriptionEndpoint({
198245
+ url: codex2 ? "https://chatgpt.com/backend-api/wham/usage" : "https://cli-chat-proxy.grok.com/v1/billing?format=credits",
198246
+ token: auth2.tokens.access_token,
198247
+ accountId: codex2?.tokens.account_id,
198248
+ grokBilling: !!grok
198249
+ });
198250
+ if (response?.status === 401) return { status: "rejected", detail: "rejected with 401" };
198251
+ if (!response?.ok) return unknown4;
198252
+ const body = await response.json().catch(() => null);
198253
+ if (!codex2) return unknown4;
198254
+ const parsed2 = codexUsageSchema.safeParse(body);
198255
+ const limit = parsed2.success ? parsed2.data.rate_limit : void 0;
198256
+ if (!limit) return unknown4;
198257
+ if (limit.allowed && !limit.limit_reached) return { status: "usable" };
198258
+ const windows = [limit.primary_window, limit.secondary_window];
198259
+ const full = windows.filter((window2) => (window2?.used_percent ?? 0) >= 100);
198260
+ const resets = (full.length ? full : windows).flatMap(
198261
+ (window2) => window2?.reset_at ? [window2.reset_at] : []
198262
+ );
198263
+ return {
198264
+ status: "exhausted",
198265
+ detail: "the ChatGPT plan's usage limit is reached",
198266
+ resetAt: resets.length ? new Date(Math.max(...resets) * 1e3) : void 0
198267
+ };
198268
+ }
198269
+ async function probeInference(input) {
198270
+ const anthropic = input.name === "ANTHROPIC_API_KEY";
198271
+ const openai = input.name === "OPENAI_API_KEY";
198272
+ if (!anthropic && !openai && input.name !== "XAI_API_KEY") {
198273
+ const result = await verifyCredential({ envVar: input.name, value: input.value });
198274
+ return result === "dead" ? { status: "rejected", detail: "rejected by its provider" } : result === "alive" ? { status: "usable" } : unknown4;
198275
+ }
198276
+ const headers = new Headers({ "content-type": "application/json" });
198277
+ if (anthropic) {
198278
+ headers.set("x-api-key", input.value);
198279
+ headers.set("anthropic-version", "2023-06-01");
198280
+ } else headers.set("authorization", `Bearer ${input.value}`);
198281
+ const response = await fetch(
198282
+ anthropic ? "https://api.anthropic.com/v1/messages" : openai ? "https://api.openai.com/v1/responses" : "https://api.x.ai/v1/chat/completions",
198283
+ {
198284
+ method: "POST",
198285
+ headers,
198286
+ body: JSON.stringify(
198287
+ openai ? { model: input.model, input: "Reply OK.", max_output_tokens: 16, store: false } : {
198288
+ model: input.model,
198289
+ messages: [{ role: "user", content: "Reply OK." }],
198290
+ max_tokens: 1
198291
+ }
198292
+ ),
198293
+ redirect: "error",
198294
+ signal: AbortSignal.timeout(1e4)
198295
+ }
198296
+ ).catch(() => null);
198297
+ if (response?.status === 400 && input.name === "XAI_API_KEY") {
198298
+ const error49 = external_exports.object({ error: external_exports.string() }).safeParse(await response.json().catch(() => null));
198299
+ return error49.success && error49.data.error.startsWith("Incorrect API key provided.") ? { status: "rejected", detail: "rejected with 400: incorrect API key" } : unknown4;
198300
+ }
198301
+ if (response?.status === 429 && openai) {
198302
+ const error49 = external_exports.object({ error: external_exports.object({ code: external_exports.string() }) }).safeParse(await response.json().catch(() => null));
198303
+ return error49.success && error49.data.error.code === "insufficient_quota" ? { status: "exhausted", detail: "refused with 429: insufficient quota", resetAt: void 0 } : unknown4;
198304
+ }
198305
+ await response?.body?.cancel();
198306
+ if (!response) return unknown4;
198307
+ if (response.ok) return { status: "usable" };
198308
+ if (response.status === 401 || response.status === 403)
198309
+ return { status: "rejected", detail: `rejected with ${response.status}` };
198310
+ if (response.status === 402)
198311
+ return { status: "exhausted", detail: "refused with 402", resetAt: void 0 };
198312
+ return unknown4;
198313
+ }
198314
+
198315
+ // utils/credentialPool.ts
198316
+ var receipts = {};
198317
+ var workflowCredentials = {};
198318
+ function saveReceipts() {
198319
+ for (const receipt of Object.values(receipts)) maskSecret(receipt);
198320
+ saveSecretState("credential_receipts", JSON.stringify(receipts));
198321
+ }
198322
+ var select2 = op(
198323
+ async (input) => {
198324
+ const response = await apiFetch({
198325
+ path: "/api/runtime/credentials",
198326
+ method: "POST",
198327
+ headers: {
198328
+ authorization: `Bearer ${input.access.token}`,
198329
+ "content-type": "application/json"
198330
+ },
198331
+ body: JSON.stringify({ id: input.candidate.id }),
198332
+ signal: AbortSignal.timeout(35e3)
198333
+ });
198334
+ if (response.status === 404) return null;
198335
+ if (!response.ok)
198336
+ throw new Error("could not load a configured credential; no fallback was charged", {
198337
+ cause: response.status
198338
+ });
198339
+ return selectedCredentialSchema.parse(await response.json());
198340
+ },
198341
+ {
198342
+ retries: [250, 1e3],
198343
+ bail: (error49) => error49 instanceof Error && typeof error49.cause === "number" && error49.cause >= 400 && error49.cause < 500
198344
+ }
198345
+ );
198346
+ function subscriptionForModel(model) {
198347
+ if (!model.includes("/")) return null;
198348
+ const provider2 = getModelProvider(model);
198349
+ return provider2 === "anthropic" ? "CLAUDE_CODE_OAUTH_TOKEN" : provider2 === "openai" ? "CODEX_AUTH_JSON" : provider2 === "xai" ? "GROK_AUTH_JSON" : null;
198350
+ }
198351
+ async function initializeCredentialPool(input) {
198352
+ const access = input.ctx.credentialAccess;
198353
+ if (!access) return;
198354
+ const model = discoveryModel(input.slug);
198355
+ maskSecret(access.token);
198356
+ const names = /* @__PURE__ */ new Set([
198357
+ ...access.candidates.map((candidate) => candidate.name),
198358
+ ...subscriptionNameSchema.options,
198359
+ "ANTHROPIC_API_KEY",
198360
+ "OPENAI_API_KEY",
198361
+ "XAI_API_KEY"
198362
+ ]);
198363
+ for (const name of names) {
198364
+ const value2 = process.env[name];
198365
+ if (value2 && !input.injected.includes(name)) workflowCredentials[name] = value2;
198366
+ }
198367
+ for (const name of subscriptionNameSchema.options) {
198368
+ if (model && subscriptionForModel(model) !== name) continue;
198369
+ if (workflowCredentials[name]) continue;
198370
+ const candidate = access.candidates.find((item) => item.name === name);
198371
+ if (!candidate) continue;
198372
+ const selected = await select2({ access, candidate }).catch(() => {
198373
+ log2.warning(`\xBB could not load ${name} for model discovery; selection will retry if needed`);
198374
+ return null;
198375
+ });
198376
+ if (!selected) continue;
198377
+ const value2 = sanitizeSecret(name, selected.value);
198378
+ if (!value2) continue;
198379
+ process.env[name] = value2;
198380
+ receipts[name] = selected.receipt;
198381
+ }
198382
+ saveReceipts();
198383
+ }
198384
+ function discoveryModel(slug2) {
198385
+ const value2 = process.env.PULLFROG_MODEL?.trim() || slug2?.trim();
198386
+ if (!value2) return void 0;
198387
+ return resolveCliModel(value2) ?? (value2.includes("/") ? value2 : void 0);
198388
+ }
198389
+ function resolvePoolModel(input) {
198390
+ const agent2 = resolveAgent({
198391
+ model: void 0,
198392
+ proxyModel: void 0,
198393
+ codexAgent: input.codexAgent
198134
198394
  });
198135
- return replacement ? { kind: "fellBack", ...outcome, replacement } : { kind: "dead", ...outcome };
198395
+ if (agent2.name === "opencode") return autoSelectModel();
198396
+ const provider2 = agent2.name === "claude" ? "anthropic" : "openai";
198397
+ return modelAliases.find(
198398
+ (alias) => alias.provider === provider2 && alias.preferred && !alias.hidden && !alias.fallback && !alias.routing
198399
+ )?.resolve;
198400
+ }
198401
+ async function selectConfiguredCredential(input) {
198402
+ const access = input.ctx.credentialAccess;
198403
+ const model = input.model;
198404
+ if (!access || !model?.includes("/") || getProviderGatewayUrl(model)) return false;
198405
+ const subscription = subscriptionForModel(model);
198406
+ const names = getModelEnvVars(model).filter((name) => name !== subscription);
198407
+ if (subscription) names.unshift(subscription);
198408
+ const candidates = access.candidates.filter((candidate) => names.includes(candidate.name));
198409
+ if (!candidates.length) return false;
198410
+ const refused = [];
198411
+ for (const name of names.filter((item) => workflowCredentials[item])) {
198412
+ const value2 = workflowCredentials[name];
198413
+ const verdict = await probe({ name, value: value2, model });
198414
+ if (usable(verdict)) {
198415
+ activate({ names, name, value: value2, receipt: void 0, model });
198416
+ return true;
198417
+ }
198418
+ refused.push(`${name} from the workflow: ${verdict.detail}`);
198419
+ log2.info(`\xBB ${name} from the workflow unavailable; trying the next credential`);
198420
+ }
198421
+ for (const candidate of candidates) {
198422
+ const selected = await select2({ access, candidate });
198423
+ if (!selected) continue;
198424
+ const verdict = await probe({ ...selected, model });
198425
+ await reportVerdict({ receipt: selected.receipt, verdict });
198426
+ if (usable(verdict)) {
198427
+ activate({ names, ...selected, model });
198428
+ log2.info(`\xBB selected ${candidate.name} from ${candidate.source} scope`);
198429
+ return true;
198430
+ }
198431
+ refused.push(`${candidate.name} from ${candidate.source} scope: ${verdict.detail}`);
198432
+ log2.info(
198433
+ `\xBB ${candidate.name} from ${candidate.source} scope unavailable; trying the next credential`
198434
+ );
198435
+ }
198436
+ throw new Error(
198437
+ `all configured credentials for ${model} were rejected or exhausted (${refused.join("; ")}); no other model or provider was selected`
198438
+ );
198439
+ }
198440
+ async function probe(input) {
198441
+ const subscription = subscriptionNameSchema.safeParse(input.name);
198442
+ const model = stripProviderPrefix(input.model);
198443
+ return subscription.success ? probeSubscription({ name: subscription.data, value: input.value, model }) : probeInference({ ...input, model });
198444
+ }
198445
+ function usable(verdict) {
198446
+ return verdict.status !== "rejected" && verdict.status !== "exhausted";
198447
+ }
198448
+ async function reportVerdict(input) {
198449
+ if (input.verdict.status === "unknown") return;
198450
+ await apiFetch({
198451
+ path: "/api/runtime/credentials/status",
198452
+ method: "POST",
198453
+ headers: { authorization: `Bearer ${input.receipt}`, "content-type": "application/json" },
198454
+ body: JSON.stringify(input.verdict),
198455
+ signal: AbortSignal.timeout(5e3)
198456
+ }).catch(() => log2.debug("\xBB could not record a credential verdict"));
198457
+ }
198458
+ function activate(input) {
198459
+ for (const name of input.names) {
198460
+ delete process.env[name];
198461
+ delete receipts[name];
198462
+ if (name === "CODEX_AUTH_JSON" || name === "GROK_AUTH_JSON") clearInstalledSubscription(name);
198463
+ }
198464
+ const value2 = sanitizeSecret(input.name, input.value);
198465
+ if (!value2) throw new Error("configured credential was empty");
198466
+ process.env[input.name] = value2;
198467
+ if (input.receipt) receipts[input.name] = input.receipt;
198468
+ installCodexAuth();
198469
+ installXaiAuth();
198470
+ saveReceipts();
198471
+ authorizeModel(input.model);
198136
198472
  }
198137
198473
 
198138
198474
  // utils/gitAuthServer.ts
@@ -198838,75 +199174,6 @@ function buildModelAccessError(input) {
198838
199174
  return [headline, "", why, "", cta].join("\n");
198839
199175
  }
198840
199176
 
198841
- // utils/normalizeEnv.ts
198842
- function hasControlCharacter(value2) {
198843
- return [...value2].some((ch) => {
198844
- const code = ch.charCodeAt(0);
198845
- return code < 32 || code === 127;
198846
- });
198847
- }
198848
- function sanitizeSecret(key, value2) {
198849
- const trimmed = value2.trim();
198850
- if (trimmed.length === 0) {
198851
- log2.warning(
198852
- `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
198853
- );
198854
- return null;
198855
- }
198856
- if (key.endsWith("_API_KEY") && hasControlCharacter(trimmed)) {
198857
- log2.warning(
198858
- `\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.`
198859
- );
198860
- return null;
198861
- }
198862
- if (trimmed !== value2) {
198863
- log2.warning(
198864
- `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
198865
- );
198866
- }
198867
- maskSecret(trimmed);
198868
- return trimmed;
198869
- }
198870
- function normalizeEnv() {
198871
- const upperKeys = /* @__PURE__ */ new Map();
198872
- for (const key of Object.keys(process.env)) {
198873
- const upper2 = key.toUpperCase();
198874
- const existing = upperKeys.get(upper2) || [];
198875
- existing.push(key);
198876
- upperKeys.set(upper2, existing);
198877
- }
198878
- for (const [upperKey, keys] of upperKeys) {
198879
- if (keys.length === 1) {
198880
- const key = keys[0];
198881
- if (key !== upperKey) {
198882
- process.env[upperKey] = process.env[key];
198883
- delete process.env[key];
198884
- }
198885
- continue;
198886
- }
198887
- const values = keys.map((k2) => process.env[k2]);
198888
- const uniqueValues = new Set(values);
198889
- if (uniqueValues.size > 1) {
198890
- log2.warning(
198891
- `env var conflict: ${keys.join(", ")} have different values. using uppercase ${upperKey}.`
198892
- );
198893
- }
198894
- const preferredKey = keys.find((k2) => k2 === upperKey) || keys[0];
198895
- const preferredValue = process.env[preferredKey];
198896
- for (const key of keys) {
198897
- delete process.env[key];
198898
- }
198899
- process.env[upperKey] = preferredValue;
198900
- }
198901
- for (const key of Object.keys(process.env)) {
198902
- if (!isSensitiveEnvName(key)) continue;
198903
- const value2 = process.env[key];
198904
- if (typeof value2 !== "string" || value2.length === 0) continue;
198905
- const sanitized = sanitizeSecret(key, value2);
198906
- if (sanitized !== null) process.env[key] = sanitized;
198907
- }
198908
- }
198909
-
198910
199177
  // utils/overrides.ts
198911
199178
  var DENIED_OVERRIDE_NAMES = /* @__PURE__ */ new Set([
198912
199179
  "GITHUB_TOKEN",
@@ -199477,7 +199744,6 @@ ${ctx.error}` : ctx.error;
199477
199744
  workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
199478
199745
  customParts,
199479
199746
  model: ctx.toolState.model,
199480
- fallbackFrom: ctx.toolState.modelFallback?.from,
199481
199747
  clamped: ctx.toolState.modelClamped,
199482
199748
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
199483
199749
  oss: ctx.toolState.oss,
@@ -199861,7 +200127,8 @@ async function fetchRunContext(params) {
199861
200127
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
199862
200128
  try {
199863
200129
  const headers = {
199864
- Authorization: `Bearer ${params.token}`
200130
+ Authorization: `Bearer ${params.token}`,
200131
+ "X-Pullfrog-Credential-Pools": "1"
199865
200132
  };
199866
200133
  if (params.oidcToken) {
199867
200134
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
@@ -201158,16 +201425,20 @@ async function main() {
201158
201425
  const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
201159
201426
  const opencodeCliPath = await agents.opencode.install();
201160
201427
  captureBaselineModels(opencodeCliPath);
201428
+ const injected = [];
201161
201429
  if (runContext.dbSecrets) {
201162
201430
  for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
201163
201431
  if (!process.env[key]) {
201164
201432
  const sanitized = sanitizeSecret(key, value2);
201165
- if (sanitized !== null) process.env[key] = sanitized;
201433
+ if (sanitized === null) continue;
201434
+ process.env[key] = sanitized;
201435
+ injected.push(key);
201166
201436
  }
201167
201437
  }
201168
201438
  const count = Object.keys(runContext.dbSecrets).length;
201169
201439
  if (count > 0) log2.info(`\xBB ${count} db secret(s) loaded`);
201170
201440
  }
201441
+ await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
201171
201442
  installCodexAuth();
201172
201443
  installXaiAuth();
201173
201444
  captureAuthorizedModels(opencodeCliPath);
@@ -201241,13 +201512,23 @@ async function main() {
201241
201512
  }
201242
201513
  const gitAuthServer = __using(_stack2, await startGitAuthServer(tmpdir4), true);
201243
201514
  setGitAuthServer(gitAuthServer);
201515
+ let resolvedModel = resolveModel({ slug: payload.model });
201516
+ if (!resolvedModel && !payload.proxyModel && runContext.credentialAccess?.candidates.length) {
201517
+ resolvedModel = resolvePoolModel({
201518
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
201519
+ });
201520
+ }
201521
+ let poolSelected = !payload.proxyModel && await selectConfiguredCredential({
201522
+ ctx: runContext,
201523
+ model: resolvedModel
201524
+ });
201244
201525
  const access = decideModelAccess({
201245
201526
  modelExplicit: payload.modelExplicit ?? false,
201246
201527
  model: payload.model,
201247
201528
  oss: runContext.oss,
201248
201529
  proxyActive: !!payload.proxyModel,
201249
201530
  subsidyTarget: runContext.proxyModel,
201250
- resolvedModel: resolveModel({ slug: payload.model }),
201531
+ resolvedModel,
201251
201532
  authorized: getAuthorizedModels()
201252
201533
  });
201253
201534
  if (access.kind === "error") {
@@ -201261,15 +201542,19 @@ async function main() {
201261
201542
  );
201262
201543
  }
201263
201544
  if (access.kind === "proxy") payload.proxyModel = access.target;
201264
- if (access.kind === "byok") payload.proxyModel = void 0;
201545
+ if (access.kind === "byok") {
201546
+ payload.proxyModel = void 0;
201547
+ if (!poolSelected)
201548
+ poolSelected = await selectConfiguredCredential({
201549
+ ctx: runContext,
201550
+ model: resolvedModel
201551
+ });
201552
+ }
201265
201553
  if (runContext.oss && payload.proxyModel) {
201266
201554
  payload.effort = ossEffortFloor({ payload });
201267
201555
  }
201268
- const configuredModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
201269
- const credentials = payload.proxyModel ? { kind: "ok" } : await checkConfiguredCredentials({
201270
- model: configuredModel,
201271
- authorized: getAuthorizedModels()
201272
- });
201556
+ if (payload.proxyModel) resolvedModel = void 0;
201557
+ const credentials = payload.proxyModel || poolSelected ? { kind: "ok" } : await checkConfiguredCredentials({ model: resolvedModel });
201273
201558
  if (credentials.kind === "dead") {
201274
201559
  throw new Error(
201275
201560
  buildRejectedCredentialError({
@@ -201281,13 +201566,6 @@ async function main() {
201281
201566
  })
201282
201567
  );
201283
201568
  }
201284
- if (credentials.kind === "fellBack" && configuredModel) {
201285
- log2.warning(
201286
- `\xBB ${credentials.credential} was rejected by its provider \u2014 running ${credentials.replacement} instead of ${configuredModel}`
201287
- );
201288
- toolState.modelFallback = { from: configuredModel };
201289
- }
201290
- const resolvedModel = credentials.kind === "fellBack" ? credentials.replacement : configuredModel;
201291
201569
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
201292
201570
  let agent2 = resolveAgent({
201293
201571
  model: resolvedModel,
@@ -201597,12 +201875,12 @@ ${instructions.user}` : null,
201597
201875
  if (payload.timeout === TIMEOUT_DISABLED) {
201598
201876
  result = await Promise.race([agentPromise, activityTimeout.promise]);
201599
201877
  } else {
201600
- const usable = resolveTimeoutMs(payload.timeout);
201601
- if (payload.timeout && usable === null) {
201878
+ const usable2 = resolveTimeoutMs(payload.timeout);
201879
+ if (payload.timeout && usable2 === null) {
201602
201880
  log2.warning(`invalid timeout "${payload.timeout}" (use --notimeout to disable), using 1h`);
201603
201881
  }
201604
- const timeoutMs = usable ?? 36e5;
201605
- const actualTimeout = usable !== null ? payload.timeout : "1h";
201882
+ const timeoutMs = usable2 ?? 36e5;
201883
+ const actualTimeout = usable2 !== null ? payload.timeout : "1h";
201606
201884
  let timeoutId;
201607
201885
  const timeoutPromise = new Promise((_4, reject) => {
201608
201886
  timeoutId = setTimeout(() => {
@@ -202230,7 +202508,7 @@ function resolveTarget2(repo) {
202230
202508
  return { owner: match3[1], repo: match3[2] };
202231
202509
  }
202232
202510
  function cliVersion() {
202233
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.90");
202511
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.91");
202234
202512
  if (!parts) return "0.0.0";
202235
202513
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
202236
202514
  }
@@ -202397,7 +202675,7 @@ async function runCli8(input) {
202397
202675
  }
202398
202676
 
202399
202677
  // cli.ts
202400
- var VERSION10 = "0.1.90";
202678
+ var VERSION10 = "0.1.91";
202401
202679
  var bin = basename2(process.argv[1] || "");
202402
202680
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
202403
202681
  var rawArgs = process.argv.slice(2);