pullfrog 0.1.79 → 0.1.80

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
@@ -108850,10 +108850,10 @@ function decodeJwtExpMs(token) {
108850
108850
  // utils/codexOAuth.ts
108851
108851
  var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
108852
108852
  var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
108853
- function codexChainIsDead(body) {
108853
+ function codexChainIsDead(status, body) {
108854
+ if (status !== 401) return false;
108854
108855
  const err = parseOAuthErrorBody(body)?.error;
108855
- if (!err || typeof err !== "object") return false;
108856
- return "code" in err && err.code === "token_expired";
108856
+ return !(err && typeof err === "object" && "code" in err && err.code === "invalid_client");
108857
108857
  }
108858
108858
  async function refreshCodexAuthBody(body) {
108859
108859
  const response = await fetch(CODEX_OAUTH_TOKEN_URL, {
@@ -108869,7 +108869,12 @@ async function refreshCodexAuthBody(body) {
108869
108869
  if (!response.ok) {
108870
108870
  const text = await response.text().catch(() => "");
108871
108871
  if (response.status >= 400 && response.status < 500) {
108872
- throw new OAuthInvalidGrantError("Codex", response.status, text, codexChainIsDead(text));
108872
+ throw new OAuthInvalidGrantError(
108873
+ "Codex",
108874
+ response.status,
108875
+ text,
108876
+ codexChainIsDead(response.status, text)
108877
+ );
108873
108878
  }
108874
108879
  throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
108875
108880
  }
@@ -123000,7 +123005,7 @@ var import_picocolors = __toESM(require_picocolors(), 1);
123000
123005
  // package.json
123001
123006
  var package_default = {
123002
123007
  name: "pullfrog",
123003
- version: "0.1.79",
123008
+ version: "0.1.80",
123004
123009
  cliContractVersion: "1",
123005
123010
  type: "module",
123006
123011
  bin: {
@@ -123124,6 +123129,27 @@ var PULLFROG_API_URL = (process.env.PULLFROG_API_URL || "https://pullfrog.com").
123124
123129
  /\/+$/,
123125
123130
  ""
123126
123131
  );
123132
+ var CODEX_AUTH_SECRET = "CODEX_AUTH_JSON";
123133
+ var CLAUDE_OAUTH_SECRET = "CLAUDE_CODE_OAUTH_TOKEN";
123134
+ var GROK_AUTH_SECRET = "GROK_AUTH_JSON";
123135
+ var SUBSCRIPTION_SECRET_NAMES = [CODEX_AUTH_SECRET, CLAUDE_OAUTH_SECRET, GROK_AUTH_SECRET];
123136
+ function shadowRefusal(params) {
123137
+ const repos = params.overrides.filter((override) => override.name === params.name).map((override) => `${params.owner}/${override.repo}`);
123138
+ if (!repos.length) return null;
123139
+ const bin2 = process.env.PULLFROG_BIN_NAME || "pullfrog";
123140
+ return [
123141
+ `${import_picocolors.default.cyan(params.name)} is already set on ${repos.map((repo) => import_picocolors.default.cyan(repo)).join(", ")}.`,
123142
+ `a repo's own copy takes precedence, so saving to ${import_picocolors.default.cyan(params.owner)} would not change`,
123143
+ `what runs there read.`,
123144
+ ``,
123145
+ `delete the repo copies, then run this command again:`,
123146
+ ...repos.map(
123147
+ (repo) => ` ${import_picocolors.default.cyan(`npx ${bin2} secret delete ${params.name} --repo ${repo}`)}`
123148
+ ),
123149
+ ``,
123150
+ `${import_picocolors.default.dim("or:")} pass ${import_picocolors.default.cyan("--repo OWNER/REPO")} to replace one repo's own copy instead.`
123151
+ ].join("\n");
123152
+ }
123127
123153
  var activeSpin = null;
123128
123154
  function setActiveSpin(spin) {
123129
123155
  activeSpin = spin;
@@ -123360,6 +123386,9 @@ var secretNamesSchema = external_exports.object({
123360
123386
  target: external_exports.string(),
123361
123387
  secrets: external_exports.array(external_exports.string()),
123362
123388
  inherited: external_exports.array(external_exports.string()),
123389
+ // `.default([])` so a CLI newer than its server — a preview deployment, a self-hosted
123390
+ // install — keeps working: every command parses this shape, not just `auth`.
123391
+ overrides: external_exports.array(external_exports.object({ name: external_exports.string(), repo: external_exports.string() })).default([]),
123363
123392
  writable: external_exports.boolean()
123364
123393
  });
123365
123394
  async function runCli(input2) {
@@ -123429,6 +123458,10 @@ use auth codex, auth claude or auth grok for subscription credentials.`);
123429
123458
  throw new Error("--file requires exactly one secret name");
123430
123459
  if (file3 === void 0 && !process.stdin.isTTY)
123431
123460
  throw new Error("use --file PATH or --file - without a terminal");
123461
+ for (const name of names.filter((secret) => SUBSCRIPTION_SECRET_NAMES.includes(secret))) {
123462
+ const refusal = shadowRefusal({ overrides: data.overrides, owner: target.owner, name });
123463
+ if (refusal) throw new Error(refusal);
123464
+ }
123432
123465
  const secrets = [];
123433
123466
  for (const name of names) {
123434
123467
  const value2 = file3 === void 0 ? await password({
@@ -123445,9 +123478,6 @@ use auth codex, auth claude or auth grok for subscription credentials.`);
123445
123478
  }
123446
123479
 
123447
123480
  // commands/auth.ts
123448
- var CODEX_AUTH_SECRET = "CODEX_AUTH_JSON";
123449
- var CLAUDE_OAUTH_SECRET = "CLAUDE_CODE_OAUTH_TOKEN";
123450
- var GROK_AUTH_SECRET = "GROK_AUTH_JSON";
123451
123481
  var CLAUDE_OAUTH_TOKEN_PREFIX = "sk-ant-oat";
123452
123482
  function stripAnsi(s) {
123453
123483
  return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
@@ -123561,6 +123591,16 @@ async function runCodex(params) {
123561
123591
  if (parsed2._.length) throw new Error("unexpected auth argument");
123562
123592
  await runCodexAuth(parsed2);
123563
123593
  }
123594
+ function refuseWhenRepoCopiesShadow(params) {
123595
+ const refusal = shadowRefusal({
123596
+ overrides: params.access.overrides,
123597
+ owner: params.owner,
123598
+ name: params.name
123599
+ });
123600
+ if (!refusal) return;
123601
+ log.warn(refusal);
123602
+ bail("nothing saved.");
123603
+ }
123564
123604
  async function runCodexAuth(parsed2) {
123565
123605
  intro(import_picocolors2.default.bgGreen(import_picocolors2.default.black(" pullfrog auth codex ")));
123566
123606
  const spin = spinner();
@@ -123601,6 +123641,7 @@ async function runCodexAuth(parsed2) {
123601
123641
  bail(
123602
123642
  scope2 === "repo" ? "repo admin required to change secrets" : "org owner required to change secrets"
123603
123643
  );
123644
+ refuseWhenRepoCopiesShadow({ access, owner: remote.owner, name: CODEX_AUTH_SECRET });
123604
123645
  if (access.secrets.includes(CODEX_AUTH_SECRET)) {
123605
123646
  const overwrite = await select({
123606
123647
  message: `${import_picocolors2.default.cyan(CODEX_AUTH_SECRET)} is already configured \u2014 overwrite?`,
@@ -123777,6 +123818,7 @@ async function runClaudeAuth(parsed2) {
123777
123818
  bail(
123778
123819
  scope2 === "repo" ? "repo admin required to change secrets" : "org owner required to change secrets"
123779
123820
  );
123821
+ refuseWhenRepoCopiesShadow({ access, owner: remote.owner, name: CLAUDE_OAUTH_SECRET });
123780
123822
  if (access.secrets.includes(CLAUDE_OAUTH_SECRET)) {
123781
123823
  const overwrite = await select({
123782
123824
  message: `${import_picocolors2.default.cyan(CLAUDE_OAUTH_SECRET)} is already configured \u2014 overwrite?`,
@@ -123911,6 +123953,7 @@ async function runGrokAuth(parsed2) {
123911
123953
  bail(
123912
123954
  scope2 === "repo" ? "repo admin required to change secrets" : "org owner required to change secrets"
123913
123955
  );
123956
+ refuseWhenRepoCopiesShadow({ access, owner: remote.owner, name: GROK_AUTH_SECRET });
123914
123957
  if (access.secrets.includes(GROK_AUTH_SECRET)) {
123915
123958
  const overwrite = await select({
123916
123959
  message: `${import_picocolors2.default.cyan(GROK_AUTH_SECRET)} is already configured \u2014 overwrite?`,
@@ -215186,6 +215229,115 @@ function subagentDeniedToolNames(ctx, outputSchema) {
215186
215229
  return names;
215187
215230
  }
215188
215231
 
215232
+ // utils/providerDashboards.ts
215233
+ var PROVIDER_DASHBOARDS = {
215234
+ anthropic: {
215235
+ name: "Anthropic",
215236
+ billing: "https://platform.claude.com/settings/billing",
215237
+ usage: "https://platform.claude.com/usage",
215238
+ keys: "https://platform.claude.com/settings/keys"
215239
+ },
215240
+ "claude-plan": {
215241
+ name: "Claude Pro/Max",
215242
+ credential: "CLAUDE_CODE_OAUTH_TOKEN",
215243
+ billing: "https://claude.ai/settings/billing",
215244
+ usage: "https://claude.ai/settings/usage"
215245
+ },
215246
+ openai: {
215247
+ name: "OpenAI",
215248
+ billing: "https://platform.openai.com/settings/organization/billing/overview",
215249
+ usage: "https://platform.openai.com/settings/organization/usage",
215250
+ keys: "https://platform.openai.com/api-keys"
215251
+ },
215252
+ "codex-plan": {
215253
+ name: "ChatGPT/Codex",
215254
+ credential: "CODEX_AUTH_JSON",
215255
+ billing: "https://chatgpt.com/#settings/Subscription",
215256
+ usage: "https://chatgpt.com/codex/settings/usage"
215257
+ },
215258
+ google: {
215259
+ name: "Google AI Studio",
215260
+ // AI Studio's "Usage & billing" page is where the Gemini API plan lives;
215261
+ // the Cloud billing console is a level up and needs a project picked.
215262
+ billing: "https://aistudio.google.com/usage",
215263
+ keys: "https://aistudio.google.com/apikey"
215264
+ },
215265
+ vertex: {
215266
+ name: "Google Vertex AI",
215267
+ billing: "https://console.cloud.google.com/billing",
215268
+ usage: "https://console.cloud.google.com/vertex-ai"
215269
+ },
215270
+ xai: {
215271
+ name: "xAI",
215272
+ billing: "https://console.x.ai/team/default/billing",
215273
+ usage: "https://console.x.ai/team/default/usage",
215274
+ keys: "https://console.x.ai/team/default/api-keys"
215275
+ },
215276
+ "grok-plan": {
215277
+ name: "SuperGrok",
215278
+ credential: "GROK_AUTH_JSON",
215279
+ billing: "https://grok.com/?_s=billing"
215280
+ },
215281
+ deepseek: {
215282
+ name: "DeepSeek",
215283
+ billing: "https://platform.deepseek.com/top_up",
215284
+ usage: "https://platform.deepseek.com/usage",
215285
+ keys: "https://platform.deepseek.com/api_keys"
215286
+ },
215287
+ moonshotai: {
215288
+ name: "Moonshot (Kimi API)",
215289
+ billing: "https://platform.kimi.ai/console/pay",
215290
+ usage: "https://platform.kimi.ai/console/account",
215291
+ keys: "https://platform.kimi.ai/console/api-keys"
215292
+ },
215293
+ "kimi-for-coding": {
215294
+ name: "Kimi Code",
215295
+ billing: "https://www.kimi.com/membership/pricing",
215296
+ keys: "https://www.kimi.com/code/console"
215297
+ },
215298
+ meta: {
215299
+ name: "Meta Model API",
215300
+ billing: "https://dev.meta.ai/billing",
215301
+ usage: "https://dev.meta.ai/usage",
215302
+ keys: "https://dev.meta.ai/api-keys"
215303
+ },
215304
+ opencode: {
215305
+ name: "OpenCode Zen",
215306
+ billing: "https://opencode.ai/zen",
215307
+ keys: "https://opencode.ai/zen"
215308
+ },
215309
+ // Go is a separate plan on the same key, with its own usage cap that resets
215310
+ // on its own (wiki/opencode-silent-stall.md). Its page is the plan page; no
215311
+ // per-window usage page is documented.
215312
+ "opencode-go": {
215313
+ name: "OpenCode Go",
215314
+ billing: "https://opencode.ai/go",
215315
+ keys: "https://opencode.ai/zen"
215316
+ },
215317
+ openrouter: {
215318
+ name: "OpenRouter",
215319
+ billing: "https://openrouter.ai/settings/credits",
215320
+ usage: "https://openrouter.ai/activity",
215321
+ keys: "https://openrouter.ai/workspaces/default/keys"
215322
+ },
215323
+ vercel: {
215324
+ name: "Vercel AI Gateway",
215325
+ // `/d?to=` resolves `[team]` to whichever team the visitor is signed into.
215326
+ billing: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway",
215327
+ keys: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys"
215328
+ },
215329
+ bedrock: {
215330
+ name: "Amazon Bedrock",
215331
+ billing: "https://console.aws.amazon.com/billing/home",
215332
+ keys: "https://us-east-1.console.aws.amazon.com/bedrock/home#/api-keys"
215333
+ },
215334
+ // Azure OpenAI keys are per-resource, so there is no stable keys page to name.
215335
+ azure: {
215336
+ name: "Azure OpenAI",
215337
+ billing: "https://portal.azure.com/#view/Microsoft_Azure_CostManagement/Menu/~/overview"
215338
+ }
215339
+ };
215340
+
215189
215341
  // utils/apiKeys.ts
215190
215342
  var MISSING_KEY_MARKER = "no API key found";
215191
215343
  var BYOK_SLUG_MARKER = "env var is required when the model is set to";
@@ -215510,14 +215662,22 @@ function buildRejectedCredentialError(params) {
215510
215662
  ].join("\n");
215511
215663
  }
215512
215664
  const where = params.inPullfrogStore ? `[Update it in Pullfrog \u2192](${settingsUrl})` : `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)`;
215665
+ const keysUrl = providerKeysUrl(params.credential);
215666
+ const issue4 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
215513
215667
  return [
215514
215668
  `**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
215515
215669
  "",
215516
215670
  `**To fix:** issue a new key in your provider dashboard and update the copy Pullfrog uses${params.inPullfrogStore ? " in the console" : " in your repo's GitHub Actions secrets"}.`,
215517
215671
  "",
215518
- `${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
215672
+ `${issue4}${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
215519
215673
  ].join("\n");
215520
215674
  }
215675
+ function providerKeysUrl(credential) {
215676
+ for (const [id, provider2] of Object.entries(providers)) {
215677
+ if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id]?.keys;
215678
+ }
215679
+ return void 0;
215680
+ }
215521
215681
  function formatApiKeyErrorSummary(params) {
215522
215682
  if (params.raw.includes(MISSING_KEY_MARKER)) {
215523
215683
  if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
@@ -215709,23 +215869,35 @@ function billingConsoleUrl(owner) {
215709
215869
  return `https://pullfrog.com/console/${encodeURIComponent(owner)}#billing`;
215710
215870
  }
215711
215871
  function commercialPaywallBody(params) {
215712
- if (params.reason === "commercial") {
215713
- return [
215714
- `**Pullfrog needs Pro on ${params.ownerLogin}, so this run didn't start.**`,
215715
- "",
215716
- "Pro is $30/month per organization and covers private repositories. Public repos and personal accounts keep running free.",
215717
- "",
215718
- `[Upgrade to Pro \u2192](${params.url})`
215719
- ].join("\n");
215872
+ const pitch = "Pro is required on private organization repos like this one: a flat $30/month per organization for unlimited runs, members, and repos.";
215873
+ switch (params.reason) {
215874
+ case "commercial":
215875
+ return [
215876
+ `**${params.ownerLogin}'s Pullfrog trial has expired.**`,
215877
+ "",
215878
+ pitch,
215879
+ "",
215880
+ `[Upgrade now \u2192](${params.url})`
215881
+ ].join("\n");
215882
+ case "subscription_ended":
215883
+ return [
215884
+ `**${params.ownerLogin}'s Pullfrog Pro subscription has ended.**`,
215885
+ "",
215886
+ pitch,
215887
+ "",
215888
+ `[Resubscribe \u2192](${params.url})`
215889
+ ].join("\n");
215890
+ case "subscription_unpaid":
215891
+ return [
215892
+ `**Pullfrog paused runs on ${params.ownerLogin}: the Pro renewal failed.**`,
215893
+ "",
215894
+ "Update the card on file to resume runs.",
215895
+ "",
215896
+ `[Update billing \u2192](${params.url})`
215897
+ ].join("\n");
215898
+ default:
215899
+ return params.reason;
215720
215900
  }
215721
- params.reason;
215722
- return [
215723
- `**Pullfrog paused runs on ${params.ownerLogin}: the Pro renewal failed.**`,
215724
- "",
215725
- "Update the card on file to resume runs.",
215726
- "",
215727
- `[Update billing \u2192](${params.url})`
215728
- ].join("\n");
215729
215901
  }
215730
215902
  function formatCommercialGateSummary(params) {
215731
215903
  return commercialPaywallBody({
@@ -215777,6 +215949,12 @@ function formatBillingErrorSummary(error109, owner) {
215777
215949
  ownerLogin: owner
215778
215950
  });
215779
215951
  }
215952
+ if (error109.code === "subscription_ended") {
215953
+ return formatCommercialGateSummary({
215954
+ reason: "subscription_ended",
215955
+ ownerLogin: owner
215956
+ });
215957
+ }
215780
215958
  if (error109.code === "subscription_unpaid") {
215781
215959
  return formatCommercialGateSummary({
215782
215960
  reason: "subscription_unpaid",
@@ -217689,8 +217867,9 @@ async function fetchRunContext(params) {
217689
217867
  clearTimeout(timeoutId);
217690
217868
  if (response.status === 402) {
217691
217869
  const body = await response.json().catch(() => null);
217692
- const reason = typeof body === "object" && body !== null && "reason" in body && body.reason === "subscription_unpaid" ? "subscription_unpaid" : "commercial";
217693
- return { ...defaultRunContext, commercialRefused: reason };
217870
+ const reason = typeof body === "object" && body !== null && "reason" in body ? body.reason : null;
217871
+ const commercialRefused = reason === "subscription_unpaid" || reason === "subscription_ended" ? reason : "commercial";
217872
+ return { ...defaultRunContext, commercialRefused };
217694
217873
  }
217695
217874
  if (!response.ok) {
217696
217875
  return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
@@ -217930,15 +218109,9 @@ function formatMinimalFailureComment(repo) {
217930
218109
  const url5 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
217931
218110
  return `**Run failed.** [View the logs \u2192](${url5})`;
217932
218111
  }
217933
- var PROVIDER_BILLING_URLS = {
217934
- deepseek: "https://platform.deepseek.com/top_up",
217935
- anthropic: "https://console.anthropic.com/settings/billing",
217936
- openai: "https://platform.openai.com/account/billing",
217937
- google: "https://aistudio.google.com/usage",
217938
- opencode: "https://opencode.ai/zen",
217939
- xai: "https://console.x.ai/team/default/billing",
217940
- openrouter: "https://openrouter.ai/settings/credits"
217941
- };
218112
+ var PROVIDER_BILLING_URLS = Object.fromEntries(
218113
+ Object.entries(PROVIDER_DASHBOARDS).map(([id, dashboard]) => [id, dashboard.billing])
218114
+ );
217942
218115
  function detectProviderId(message) {
217943
218116
  const harnessId = extractProviderId(message);
217944
218117
  if (harnessId) return harnessId;
@@ -217949,7 +218122,6 @@ function detectProviderId(message) {
217949
218122
  }
217950
218123
  return null;
217951
218124
  }
217952
- var OPENROUTER_KEYS_URL = "https://openrouter.ai/workspaces/default/keys";
217953
218125
  var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
217954
218126
  function formatProviderBillingExhausted(input2) {
217955
218127
  const providerId = detectProviderId(input2.errorMessage);
@@ -217960,7 +218132,7 @@ function formatProviderBillingExhausted(input2) {
217960
218132
  "",
217961
218133
  "OpenRouter refused the request against the key's own spending ceiling, not your account balance \u2014 raising or removing that limit is the fix, and topping up credits alone may not be.",
217962
218134
  "",
217963
- `[Raise the key's limit \u2192](${OPENROUTER_KEYS_URL}) \xB7 [Add credits \u2192](${PROVIDER_BILLING_URLS.openrouter})`,
218135
+ `[Raise the key's limit \u2192](${PROVIDER_DASHBOARDS.openrouter.keys}) \xB7 [Add credits \u2192](${PROVIDER_BILLING_URLS.openrouter})`,
217964
218136
  "",
217965
218137
  `\`\`\`
217966
218138
  ${input2.errorMessage}
@@ -218894,7 +219066,11 @@ async function main() {
218894
219066
  authorPermission: void 0,
218895
219067
  oidc: oidcCredentials
218896
219068
  }), true);
218897
- const errorMessage = runContext.commercialRefused === "subscription_unpaid" ? "Pro renewal failed for this organization" : "Pro plan required for this organization";
219069
+ const errorMessage = {
219070
+ commercial: "Pro trial expired for this organization",
219071
+ subscription_ended: "Pro subscription ended for this organization",
219072
+ subscription_unpaid: "Pro renewal failed for this organization"
219073
+ }[runContext.commercialRefused];
218898
219074
  log2.error(errorMessage);
218899
219075
  const body = formatCommercialGateSummary({
218900
219076
  reason: runContext.commercialRefused,
@@ -219985,7 +220161,7 @@ function resolveTarget2(repo) {
219985
220161
  return { owner: match3[1], repo: match3[2] };
219986
220162
  }
219987
220163
  function cliVersion() {
219988
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.79");
220164
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.80");
219989
220165
  if (!parts) return "0.0.0";
219990
220166
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
219991
220167
  }
@@ -220152,7 +220328,7 @@ async function runCli8(input2) {
220152
220328
  }
220153
220329
 
220154
220330
  // cli.ts
220155
- var VERSION10 = "0.1.79";
220331
+ var VERSION10 = "0.1.80";
220156
220332
  var bin = basename2(process.argv[1] || "");
220157
220333
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
220158
220334
  var rawArgs = process.argv.slice(2);
package/dist/index.js CHANGED
@@ -109975,7 +109975,7 @@ var import_semver = __toESM(require_semver2(), 1);
109975
109975
  // package.json
109976
109976
  var package_default = {
109977
109977
  name: "pullfrog",
109978
- version: "0.1.79",
109978
+ version: "0.1.80",
109979
109979
  cliContractVersion: "1",
109980
109980
  type: "module",
109981
109981
  bin: {
@@ -198046,6 +198046,115 @@ function subagentDeniedToolNames(ctx, outputSchema) {
198046
198046
  return names;
198047
198047
  }
198048
198048
 
198049
+ // utils/providerDashboards.ts
198050
+ var PROVIDER_DASHBOARDS = {
198051
+ anthropic: {
198052
+ name: "Anthropic",
198053
+ billing: "https://platform.claude.com/settings/billing",
198054
+ usage: "https://platform.claude.com/usage",
198055
+ keys: "https://platform.claude.com/settings/keys"
198056
+ },
198057
+ "claude-plan": {
198058
+ name: "Claude Pro/Max",
198059
+ credential: "CLAUDE_CODE_OAUTH_TOKEN",
198060
+ billing: "https://claude.ai/settings/billing",
198061
+ usage: "https://claude.ai/settings/usage"
198062
+ },
198063
+ openai: {
198064
+ name: "OpenAI",
198065
+ billing: "https://platform.openai.com/settings/organization/billing/overview",
198066
+ usage: "https://platform.openai.com/settings/organization/usage",
198067
+ keys: "https://platform.openai.com/api-keys"
198068
+ },
198069
+ "codex-plan": {
198070
+ name: "ChatGPT/Codex",
198071
+ credential: "CODEX_AUTH_JSON",
198072
+ billing: "https://chatgpt.com/#settings/Subscription",
198073
+ usage: "https://chatgpt.com/codex/settings/usage"
198074
+ },
198075
+ google: {
198076
+ name: "Google AI Studio",
198077
+ // AI Studio's "Usage & billing" page is where the Gemini API plan lives;
198078
+ // the Cloud billing console is a level up and needs a project picked.
198079
+ billing: "https://aistudio.google.com/usage",
198080
+ keys: "https://aistudio.google.com/apikey"
198081
+ },
198082
+ vertex: {
198083
+ name: "Google Vertex AI",
198084
+ billing: "https://console.cloud.google.com/billing",
198085
+ usage: "https://console.cloud.google.com/vertex-ai"
198086
+ },
198087
+ xai: {
198088
+ name: "xAI",
198089
+ billing: "https://console.x.ai/team/default/billing",
198090
+ usage: "https://console.x.ai/team/default/usage",
198091
+ keys: "https://console.x.ai/team/default/api-keys"
198092
+ },
198093
+ "grok-plan": {
198094
+ name: "SuperGrok",
198095
+ credential: "GROK_AUTH_JSON",
198096
+ billing: "https://grok.com/?_s=billing"
198097
+ },
198098
+ deepseek: {
198099
+ name: "DeepSeek",
198100
+ billing: "https://platform.deepseek.com/top_up",
198101
+ usage: "https://platform.deepseek.com/usage",
198102
+ keys: "https://platform.deepseek.com/api_keys"
198103
+ },
198104
+ moonshotai: {
198105
+ name: "Moonshot (Kimi API)",
198106
+ billing: "https://platform.kimi.ai/console/pay",
198107
+ usage: "https://platform.kimi.ai/console/account",
198108
+ keys: "https://platform.kimi.ai/console/api-keys"
198109
+ },
198110
+ "kimi-for-coding": {
198111
+ name: "Kimi Code",
198112
+ billing: "https://www.kimi.com/membership/pricing",
198113
+ keys: "https://www.kimi.com/code/console"
198114
+ },
198115
+ meta: {
198116
+ name: "Meta Model API",
198117
+ billing: "https://dev.meta.ai/billing",
198118
+ usage: "https://dev.meta.ai/usage",
198119
+ keys: "https://dev.meta.ai/api-keys"
198120
+ },
198121
+ opencode: {
198122
+ name: "OpenCode Zen",
198123
+ billing: "https://opencode.ai/zen",
198124
+ keys: "https://opencode.ai/zen"
198125
+ },
198126
+ // Go is a separate plan on the same key, with its own usage cap that resets
198127
+ // on its own (wiki/opencode-silent-stall.md). Its page is the plan page; no
198128
+ // per-window usage page is documented.
198129
+ "opencode-go": {
198130
+ name: "OpenCode Go",
198131
+ billing: "https://opencode.ai/go",
198132
+ keys: "https://opencode.ai/zen"
198133
+ },
198134
+ openrouter: {
198135
+ name: "OpenRouter",
198136
+ billing: "https://openrouter.ai/settings/credits",
198137
+ usage: "https://openrouter.ai/activity",
198138
+ keys: "https://openrouter.ai/workspaces/default/keys"
198139
+ },
198140
+ vercel: {
198141
+ name: "Vercel AI Gateway",
198142
+ // `/d?to=` resolves `[team]` to whichever team the visitor is signed into.
198143
+ billing: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway",
198144
+ keys: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys"
198145
+ },
198146
+ bedrock: {
198147
+ name: "Amazon Bedrock",
198148
+ billing: "https://console.aws.amazon.com/billing/home",
198149
+ keys: "https://us-east-1.console.aws.amazon.com/bedrock/home#/api-keys"
198150
+ },
198151
+ // Azure OpenAI keys are per-resource, so there is no stable keys page to name.
198152
+ azure: {
198153
+ name: "Azure OpenAI",
198154
+ billing: "https://portal.azure.com/#view/Microsoft_Azure_CostManagement/Menu/~/overview"
198155
+ }
198156
+ };
198157
+
198049
198158
  // utils/apiKeys.ts
198050
198159
  var MISSING_KEY_MARKER = "no API key found";
198051
198160
  var BYOK_SLUG_MARKER = "env var is required when the model is set to";
@@ -198370,14 +198479,22 @@ function buildRejectedCredentialError(params) {
198370
198479
  ].join("\n");
198371
198480
  }
198372
198481
  const where = params.inPullfrogStore ? `[Update it in Pullfrog \u2192](${settingsUrl})` : `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)`;
198482
+ const keysUrl = providerKeysUrl(params.credential);
198483
+ const issue3 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
198373
198484
  return [
198374
198485
  `**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
198375
198486
  "",
198376
198487
  `**To fix:** issue a new key in your provider dashboard and update the copy Pullfrog uses${params.inPullfrogStore ? " in the console" : " in your repo's GitHub Actions secrets"}.`,
198377
198488
  "",
198378
- `${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198489
+ `${issue3}${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198379
198490
  ].join("\n");
198380
198491
  }
198492
+ function providerKeysUrl(credential) {
198493
+ for (const [id, provider2] of Object.entries(providers)) {
198494
+ if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id]?.keys;
198495
+ }
198496
+ return void 0;
198497
+ }
198381
198498
  function formatApiKeyErrorSummary(params) {
198382
198499
  if (params.raw.includes(MISSING_KEY_MARKER)) {
198383
198500
  if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
@@ -198569,23 +198686,35 @@ function billingConsoleUrl(owner) {
198569
198686
  return `https://pullfrog.com/console/${encodeURIComponent(owner)}#billing`;
198570
198687
  }
198571
198688
  function commercialPaywallBody(params) {
198572
- if (params.reason === "commercial") {
198573
- return [
198574
- `**Pullfrog needs Pro on ${params.ownerLogin}, so this run didn't start.**`,
198575
- "",
198576
- "Pro is $30/month per organization and covers private repositories. Public repos and personal accounts keep running free.",
198577
- "",
198578
- `[Upgrade to Pro \u2192](${params.url})`
198579
- ].join("\n");
198689
+ const pitch = "Pro is required on private organization repos like this one: a flat $30/month per organization for unlimited runs, members, and repos.";
198690
+ switch (params.reason) {
198691
+ case "commercial":
198692
+ return [
198693
+ `**${params.ownerLogin}'s Pullfrog trial has expired.**`,
198694
+ "",
198695
+ pitch,
198696
+ "",
198697
+ `[Upgrade now \u2192](${params.url})`
198698
+ ].join("\n");
198699
+ case "subscription_ended":
198700
+ return [
198701
+ `**${params.ownerLogin}'s Pullfrog Pro subscription has ended.**`,
198702
+ "",
198703
+ pitch,
198704
+ "",
198705
+ `[Resubscribe \u2192](${params.url})`
198706
+ ].join("\n");
198707
+ case "subscription_unpaid":
198708
+ return [
198709
+ `**Pullfrog paused runs on ${params.ownerLogin}: the Pro renewal failed.**`,
198710
+ "",
198711
+ "Update the card on file to resume runs.",
198712
+ "",
198713
+ `[Update billing \u2192](${params.url})`
198714
+ ].join("\n");
198715
+ default:
198716
+ return params.reason;
198580
198717
  }
198581
- params.reason;
198582
- return [
198583
- `**Pullfrog paused runs on ${params.ownerLogin}: the Pro renewal failed.**`,
198584
- "",
198585
- "Update the card on file to resume runs.",
198586
- "",
198587
- `[Update billing \u2192](${params.url})`
198588
- ].join("\n");
198589
198718
  }
198590
198719
  function formatCommercialGateSummary(params) {
198591
198720
  return commercialPaywallBody({
@@ -198637,6 +198766,12 @@ function formatBillingErrorSummary(error62, owner) {
198637
198766
  ownerLogin: owner
198638
198767
  });
198639
198768
  }
198769
+ if (error62.code === "subscription_ended") {
198770
+ return formatCommercialGateSummary({
198771
+ reason: "subscription_ended",
198772
+ ownerLogin: owner
198773
+ });
198774
+ }
198640
198775
  if (error62.code === "subscription_unpaid") {
198641
198776
  return formatCommercialGateSummary({
198642
198777
  reason: "subscription_unpaid",
@@ -200549,8 +200684,9 @@ async function fetchRunContext(params) {
200549
200684
  clearTimeout(timeoutId);
200550
200685
  if (response.status === 402) {
200551
200686
  const body = await response.json().catch(() => null);
200552
- const reason = typeof body === "object" && body !== null && "reason" in body && body.reason === "subscription_unpaid" ? "subscription_unpaid" : "commercial";
200553
- return { ...defaultRunContext, commercialRefused: reason };
200687
+ const reason = typeof body === "object" && body !== null && "reason" in body ? body.reason : null;
200688
+ const commercialRefused = reason === "subscription_unpaid" || reason === "subscription_ended" ? reason : "commercial";
200689
+ return { ...defaultRunContext, commercialRefused };
200554
200690
  }
200555
200691
  if (!response.ok) {
200556
200692
  return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
@@ -200790,15 +200926,9 @@ function formatMinimalFailureComment(repo) {
200790
200926
  const url4 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
200791
200927
  return `**Run failed.** [View the logs \u2192](${url4})`;
200792
200928
  }
200793
- var PROVIDER_BILLING_URLS = {
200794
- deepseek: "https://platform.deepseek.com/top_up",
200795
- anthropic: "https://console.anthropic.com/settings/billing",
200796
- openai: "https://platform.openai.com/account/billing",
200797
- google: "https://aistudio.google.com/usage",
200798
- opencode: "https://opencode.ai/zen",
200799
- xai: "https://console.x.ai/team/default/billing",
200800
- openrouter: "https://openrouter.ai/settings/credits"
200801
- };
200929
+ var PROVIDER_BILLING_URLS = Object.fromEntries(
200930
+ Object.entries(PROVIDER_DASHBOARDS).map(([id, dashboard]) => [id, dashboard.billing])
200931
+ );
200802
200932
  function detectProviderId(message) {
200803
200933
  const harnessId = extractProviderId(message);
200804
200934
  if (harnessId) return harnessId;
@@ -200809,7 +200939,6 @@ function detectProviderId(message) {
200809
200939
  }
200810
200940
  return null;
200811
200941
  }
200812
- var OPENROUTER_KEYS_URL = "https://openrouter.ai/workspaces/default/keys";
200813
200942
  var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
200814
200943
  function formatProviderBillingExhausted(input2) {
200815
200944
  const providerId = detectProviderId(input2.errorMessage);
@@ -200820,7 +200949,7 @@ function formatProviderBillingExhausted(input2) {
200820
200949
  "",
200821
200950
  "OpenRouter refused the request against the key's own spending ceiling, not your account balance \u2014 raising or removing that limit is the fix, and topping up credits alone may not be.",
200822
200951
  "",
200823
- `[Raise the key's limit \u2192](${OPENROUTER_KEYS_URL}) \xB7 [Add credits \u2192](${PROVIDER_BILLING_URLS.openrouter})`,
200952
+ `[Raise the key's limit \u2192](${PROVIDER_DASHBOARDS.openrouter.keys}) \xB7 [Add credits \u2192](${PROVIDER_BILLING_URLS.openrouter})`,
200824
200953
  "",
200825
200954
  `\`\`\`
200826
200955
  ${input2.errorMessage}
@@ -201754,7 +201883,11 @@ async function main() {
201754
201883
  authorPermission: void 0,
201755
201884
  oidc: oidcCredentials
201756
201885
  }), true);
201757
- const errorMessage = runContext.commercialRefused === "subscription_unpaid" ? "Pro renewal failed for this organization" : "Pro plan required for this organization";
201886
+ const errorMessage = {
201887
+ commercial: "Pro trial expired for this organization",
201888
+ subscription_ended: "Pro subscription ended for this organization",
201889
+ subscription_unpaid: "Pro renewal failed for this organization"
201890
+ }[runContext.commercialRefused];
201758
201891
  log.error(errorMessage);
201759
201892
  const body = formatCommercialGateSummary({
201760
201893
  reason: runContext.commercialRefused,
@@ -9,7 +9,7 @@ export type { AuthorPermission, AutoTier, EffortPosition, ModelAlias, ModelProvi
9
9
  export { AUTO_EFFICIENT, AUTO_INTELLIGENT, AUTO_ROUTER, autoLadder, autoRoutedSlug, DEFAULT_EFFORT_POSITION, DEFAULT_PROXY_MODEL, defaultAutoTier, EFFORT_ALIASES, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoRouted, isAutoSlug, isAutoTier, isCardGatedModel, isEffortPosition, isOssAllowedModel, isRouterTier, ladderRungs, modelAliases, modelHasStoredAuth, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, offeredRungs, ossFundedModelNames, PROVIDER_GATEWAY_URL_ENV, parseEffortPosition, parseModel, providers, pullfrogMcpName, ROUTED_PROVIDERS, ROUTER_LADDER, ROUTER_TIERS, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolveRoutedModel, resolveRung, rungLabel, rungPosition, SUBSIDY_RUNG, } from "../external.ts";
10
10
  export type { Mode } from "../modes.ts";
11
11
  export { modes } from "../modes.ts";
12
- export { commercialPaywallBody } from "../utils/billingErrors.ts";
12
+ export { type CommercialRefusal, commercialPaywallBody } from "../utils/billingErrors.ts";
13
13
  export type { BuildPullfrogFooterParams, WorkflowRunFooterInfo, } from "../utils/buildPullfrogFooter.ts";
14
14
  export { buildPullfrogFooter, PULLFROG_DIVIDER, stripExistingFooter, } from "../utils/buildPullfrogFooter.ts";
15
15
  export type { CodexAuthBody } from "../utils/codexOAuth.ts";
@@ -22,6 +22,7 @@ export { isLeapingIntoActionCommentBody, LEAPING_INTO_ACTION_PREFIX, } from "../
22
22
  export { MAX_LEARNINGS_LENGTH, truncateAtLineBoundary } from "../utils/learningsTruncate.ts";
23
23
  export type { CreateProgressCommentTarget, ProgressComment, ProgressCommentType, } from "../utils/progressComment.ts";
24
24
  export { createLeapingProgressComment, deleteProgressCommentApi, getProgressComment, updateProgressComment, } from "../utils/progressComment.ts";
25
+ export { PROVIDER_DASHBOARDS, type ProviderDashboard } from "../utils/providerDashboards.ts";
25
26
  export type { RunStatusCheckConclusion, RunStatusCheckOctokit, } from "../utils/runStatusCheck.ts";
26
27
  export { APPROVAL_CHECK_NAME, createRunStatusCheck, finalizeRunStatusCheck, RUN_STATUS_CHECK_NAME, runStatusCheckNeedsFinalizing, } from "../utils/runStatusCheck.ts";
27
28
  export { isValidTimeString, parseTimeString, TIMEOUT_DISABLED, } from "../utils/time.ts";
package/dist/internal.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "pullfrog",
11
- version: "0.1.79",
11
+ version: "0.1.80",
12
12
  cliContractVersion: "1",
13
13
  type: "module",
14
14
  bin: {
@@ -16151,23 +16151,35 @@ var modes = computeModes("opencode");
16151
16151
 
16152
16152
  // utils/billingErrors.ts
16153
16153
  function commercialPaywallBody(params) {
16154
- if (params.reason === "commercial") {
16155
- return [
16156
- `**Pullfrog needs Pro on ${params.ownerLogin}, so this run didn't start.**`,
16157
- "",
16158
- "Pro is $30/month per organization and covers private repositories. Public repos and personal accounts keep running free.",
16159
- "",
16160
- `[Upgrade to Pro \u2192](${params.url})`
16161
- ].join("\n");
16162
- }
16163
- params.reason;
16164
- return [
16165
- `**Pullfrog paused runs on ${params.ownerLogin}: the Pro renewal failed.**`,
16166
- "",
16167
- "Update the card on file to resume runs.",
16168
- "",
16169
- `[Update billing \u2192](${params.url})`
16170
- ].join("\n");
16154
+ const pitch = "Pro is required on private organization repos like this one: a flat $30/month per organization for unlimited runs, members, and repos.";
16155
+ switch (params.reason) {
16156
+ case "commercial":
16157
+ return [
16158
+ `**${params.ownerLogin}'s Pullfrog trial has expired.**`,
16159
+ "",
16160
+ pitch,
16161
+ "",
16162
+ `[Upgrade now \u2192](${params.url})`
16163
+ ].join("\n");
16164
+ case "subscription_ended":
16165
+ return [
16166
+ `**${params.ownerLogin}'s Pullfrog Pro subscription has ended.**`,
16167
+ "",
16168
+ pitch,
16169
+ "",
16170
+ `[Resubscribe \u2192](${params.url})`
16171
+ ].join("\n");
16172
+ case "subscription_unpaid":
16173
+ return [
16174
+ `**Pullfrog paused runs on ${params.ownerLogin}: the Pro renewal failed.**`,
16175
+ "",
16176
+ "Update the card on file to resume runs.",
16177
+ "",
16178
+ `[Update billing \u2192](${params.url})`
16179
+ ].join("\n");
16180
+ default:
16181
+ return params.reason;
16182
+ }
16171
16183
  }
16172
16184
 
16173
16185
  // utils/buildPullfrogFooter.ts
@@ -16300,10 +16312,10 @@ function decodeJwtExpMs(token) {
16300
16312
  // utils/codexOAuth.ts
16301
16313
  var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
16302
16314
  var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
16303
- function codexChainIsDead(body) {
16315
+ function codexChainIsDead(status, body) {
16316
+ if (status !== 401) return false;
16304
16317
  const err = parseOAuthErrorBody(body)?.error;
16305
- if (!err || typeof err !== "object") return false;
16306
- return "code" in err && err.code === "token_expired";
16318
+ return !(err && typeof err === "object" && "code" in err && err.code === "invalid_client");
16307
16319
  }
16308
16320
  async function refreshCodexAuthBody(body) {
16309
16321
  const response = await fetch(CODEX_OAUTH_TOKEN_URL, {
@@ -16319,7 +16331,12 @@ async function refreshCodexAuthBody(body) {
16319
16331
  if (!response.ok) {
16320
16332
  const text = await response.text().catch(() => "");
16321
16333
  if (response.status >= 400 && response.status < 500) {
16322
- throw new OAuthInvalidGrantError("Codex", response.status, text, codexChainIsDead(text));
16334
+ throw new OAuthInvalidGrantError(
16335
+ "Codex",
16336
+ response.status,
16337
+ text,
16338
+ codexChainIsDead(response.status, text)
16339
+ );
16323
16340
  }
16324
16341
  throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
16325
16342
  }
@@ -16598,6 +16615,115 @@ async function createLeapingProgressComment(ctx, target, body) {
16598
16615
  };
16599
16616
  }
16600
16617
 
16618
+ // utils/providerDashboards.ts
16619
+ var PROVIDER_DASHBOARDS = {
16620
+ anthropic: {
16621
+ name: "Anthropic",
16622
+ billing: "https://platform.claude.com/settings/billing",
16623
+ usage: "https://platform.claude.com/usage",
16624
+ keys: "https://platform.claude.com/settings/keys"
16625
+ },
16626
+ "claude-plan": {
16627
+ name: "Claude Pro/Max",
16628
+ credential: "CLAUDE_CODE_OAUTH_TOKEN",
16629
+ billing: "https://claude.ai/settings/billing",
16630
+ usage: "https://claude.ai/settings/usage"
16631
+ },
16632
+ openai: {
16633
+ name: "OpenAI",
16634
+ billing: "https://platform.openai.com/settings/organization/billing/overview",
16635
+ usage: "https://platform.openai.com/settings/organization/usage",
16636
+ keys: "https://platform.openai.com/api-keys"
16637
+ },
16638
+ "codex-plan": {
16639
+ name: "ChatGPT/Codex",
16640
+ credential: "CODEX_AUTH_JSON",
16641
+ billing: "https://chatgpt.com/#settings/Subscription",
16642
+ usage: "https://chatgpt.com/codex/settings/usage"
16643
+ },
16644
+ google: {
16645
+ name: "Google AI Studio",
16646
+ // AI Studio's "Usage & billing" page is where the Gemini API plan lives;
16647
+ // the Cloud billing console is a level up and needs a project picked.
16648
+ billing: "https://aistudio.google.com/usage",
16649
+ keys: "https://aistudio.google.com/apikey"
16650
+ },
16651
+ vertex: {
16652
+ name: "Google Vertex AI",
16653
+ billing: "https://console.cloud.google.com/billing",
16654
+ usage: "https://console.cloud.google.com/vertex-ai"
16655
+ },
16656
+ xai: {
16657
+ name: "xAI",
16658
+ billing: "https://console.x.ai/team/default/billing",
16659
+ usage: "https://console.x.ai/team/default/usage",
16660
+ keys: "https://console.x.ai/team/default/api-keys"
16661
+ },
16662
+ "grok-plan": {
16663
+ name: "SuperGrok",
16664
+ credential: "GROK_AUTH_JSON",
16665
+ billing: "https://grok.com/?_s=billing"
16666
+ },
16667
+ deepseek: {
16668
+ name: "DeepSeek",
16669
+ billing: "https://platform.deepseek.com/top_up",
16670
+ usage: "https://platform.deepseek.com/usage",
16671
+ keys: "https://platform.deepseek.com/api_keys"
16672
+ },
16673
+ moonshotai: {
16674
+ name: "Moonshot (Kimi API)",
16675
+ billing: "https://platform.kimi.ai/console/pay",
16676
+ usage: "https://platform.kimi.ai/console/account",
16677
+ keys: "https://platform.kimi.ai/console/api-keys"
16678
+ },
16679
+ "kimi-for-coding": {
16680
+ name: "Kimi Code",
16681
+ billing: "https://www.kimi.com/membership/pricing",
16682
+ keys: "https://www.kimi.com/code/console"
16683
+ },
16684
+ meta: {
16685
+ name: "Meta Model API",
16686
+ billing: "https://dev.meta.ai/billing",
16687
+ usage: "https://dev.meta.ai/usage",
16688
+ keys: "https://dev.meta.ai/api-keys"
16689
+ },
16690
+ opencode: {
16691
+ name: "OpenCode Zen",
16692
+ billing: "https://opencode.ai/zen",
16693
+ keys: "https://opencode.ai/zen"
16694
+ },
16695
+ // Go is a separate plan on the same key, with its own usage cap that resets
16696
+ // on its own (wiki/opencode-silent-stall.md). Its page is the plan page; no
16697
+ // per-window usage page is documented.
16698
+ "opencode-go": {
16699
+ name: "OpenCode Go",
16700
+ billing: "https://opencode.ai/go",
16701
+ keys: "https://opencode.ai/zen"
16702
+ },
16703
+ openrouter: {
16704
+ name: "OpenRouter",
16705
+ billing: "https://openrouter.ai/settings/credits",
16706
+ usage: "https://openrouter.ai/activity",
16707
+ keys: "https://openrouter.ai/workspaces/default/keys"
16708
+ },
16709
+ vercel: {
16710
+ name: "Vercel AI Gateway",
16711
+ // `/d?to=` resolves `[team]` to whichever team the visitor is signed into.
16712
+ billing: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway",
16713
+ keys: "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys"
16714
+ },
16715
+ bedrock: {
16716
+ name: "Amazon Bedrock",
16717
+ billing: "https://console.aws.amazon.com/billing/home",
16718
+ keys: "https://us-east-1.console.aws.amazon.com/bedrock/home#/api-keys"
16719
+ },
16720
+ // Azure OpenAI keys are per-resource, so there is no stable keys page to name.
16721
+ azure: {
16722
+ name: "Azure OpenAI",
16723
+ billing: "https://portal.azure.com/#view/Microsoft_Azure_CostManagement/Menu/~/overview"
16724
+ }
16725
+ };
16726
+
16601
16727
  // utils/runStatusCheck.ts
16602
16728
  var RUN_STATUS_CHECK_NAME = "pullfrog";
16603
16729
  var GITHUB_ACTIONS_APP_SLUG = "github-actions";
@@ -16806,6 +16932,7 @@ export {
16806
16932
  OAuthInvalidGrantError,
16807
16933
  OSS_MODEL_ALLOWLIST,
16808
16934
  OSS_RECOMMENDED_MODEL,
16935
+ PROVIDER_DASHBOARDS,
16809
16936
  PROVIDER_GATEWAY_URL_ENV,
16810
16937
  PULLFROG_DIVIDER,
16811
16938
  ROUTED_PROVIDERS,
@@ -43,6 +43,15 @@ export declare class BillingError extends Error {
43
43
  export declare class TransientError extends Error {
44
44
  constructor(message: string);
45
45
  }
46
+ /**
47
+ * Why the org commercial gate refused a run (billing model v2), resolved by
48
+ * `resolveTeamRunAccess` and carried unchanged through the paywall comment, the
49
+ * run-context 402 and the proxy-token 402. `commercial`: the no-card trial
50
+ * wound down to `paused`. `subscription_ended`: a paid subscription was
51
+ * canceled and lapsed to `paused`. `subscription_unpaid`: the renewal failed
52
+ * dunning.
53
+ */
54
+ export type CommercialRefusal = "commercial" | "subscription_ended" | "subscription_unpaid";
46
55
  /**
47
56
  * Canonical paused-run paywall body (billing model v2). Single source of truth so
48
57
  * the two surfaces that show it can never drift: the trigger-time comment (a run
@@ -51,7 +60,7 @@ export declare class TransientError extends Error {
51
60
  * caller's billing surface (each passes its own, internal plan page vs console).
52
61
  */
53
62
  export declare function commercialPaywallBody(params: {
54
- reason: "commercial" | "subscription_unpaid";
63
+ reason: CommercialRefusal;
55
64
  ownerLogin: string;
56
65
  url: string;
57
66
  }): string;
@@ -65,7 +74,7 @@ export declare function commercialPaywallBody(params: {
65
74
  * production row; see `isSubscriptionHostTrusted` and wiki/billing-model-v2.md.
66
75
  */
67
76
  export declare function formatCommercialGateSummary(params: {
68
- reason: "commercial" | "subscription_unpaid";
77
+ reason: CommercialRefusal;
69
78
  ownerLogin: string;
70
79
  }): string;
71
80
  /**
@@ -21,9 +21,8 @@ export interface CodexAuthBody {
21
21
  };
22
22
  last_refresh?: string;
23
23
  /**
24
- * ISO timestamp of a rejection OpenAI attributed to the token itself
25
- * (`error.code: "token_expired"` — it does not emit RFC 6749's
26
- * `invalid_grant` here). OpenAI rotates the refresh
24
+ * ISO timestamp of a rejection OpenAI attributed to the token itself (a `401`
25
+ * other than `invalid_client` — see `codexChainIsDead`). OpenAI rotates the refresh
27
26
  * token on every use, so such a rejection is PERMANENT — without a latch the
28
27
  * server re-issued the identical doomed refresh on every run (455 futile
29
28
  * round trips in 7 days, one per run, each holding a Postgres row lock across
@@ -11,12 +11,13 @@
11
11
  * Thrown when an OAuth provider rejects a refresh token (4xx).
12
12
  *
13
13
  * `chainIsDead` is decided by the CALLER, because the two providers we talk to
14
- * disagree about how to say it and only the caller knows its own dialect:
15
- * xAI answers RFC 6749 (`{"error":"invalid_grant"}`) while OpenAI nests an
16
- * object and discriminates on `error.code` (`token_expired`). A rejection we
17
- * cannot classify — a CDN error page in front of the token endpoint, say — is
18
- * NOT dead: latching there would retire every customer's working credential
19
- * over a transient edge event.
14
+ * disagree about how to say it and only the caller knows its own dialect, and
15
+ * so does the list of rejections that are NOT dead. xAI answers RFC 6749
16
+ * (`{"error":"invalid_grant"}`), so anything else — an unknown client id, an
17
+ * unsupported grant, a CDN error page — is spared. OpenAI answers a dead chain
18
+ * with a `401` whose body varies and is often empty, so there the `401` itself
19
+ * is the signal and only `invalid_client` is spared. Latching wrongly retires a
20
+ * customer's working credential until they re-mint it.
20
21
  */
21
22
  export declare class OAuthInvalidGrantError extends Error {
22
23
  readonly status: number;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where each provider's own account lives, keyed by the provider id the rest of
3
+ * the runtime already uses (`providers` in `models.ts`, `providerID=` in the
4
+ * opencode log). Both failure surfaces read this one table — the per-run PR
5
+ * comment for its top-up CTA, and the diagnosis agent's link registry — so a
6
+ * host move is fixed in one place. Every catalog provider is here except
7
+ * `openai-compatible`, whose endpoint is the customer's own and has no page.
8
+ *
9
+ * How each path is known (2026-09-14), because "checked" means three different
10
+ * things here. Probed with a discriminating control — the path answers 200 and
11
+ * a junk path on the same host 404s: `platform.claude.com`, `platform.kimi.ai`,
12
+ * `www.kimi.com`, `dev.meta.ai`, `opencode.ai`, the AWS billing host.
13
+ * Taken from the provider's own docs, because the host answers a probe with a
14
+ * Cloudflare challenge or a catch-all login redirect that no junk path can
15
+ * distinguish: OpenAI, xAI, DeepSeek, `claude.ai`, `chatgpt.com`, Google,
16
+ * OpenRouter, Vercel, `grok.com`. Fragment routes (`#…`) never reach a server,
17
+ * so those rest on the docs alone. `console.anthropic.com` and
18
+ * `platform.moonshot.ai` both 301 now, so the entries name the hosts they land
19
+ * on.
20
+ *
21
+ * The subscription rows are not providers in the catalog sense — a Claude,
22
+ * ChatGPT or SuperGrok plan has a usage window and a plan page but no API key —
23
+ * so they carry the `-plan` suffix, no `keys`, and the credential that proves
24
+ * the plan is in play.
25
+ */
26
+ export type ProviderDashboard = {
27
+ /** the name a customer knows the account by */
28
+ name: string;
29
+ /** a plan row only: the stored credential whose presence means the plan is in use */
30
+ credential?: string;
31
+ /** the wallet, the card on file, a spend cap, or the plan itself */
32
+ billing: string;
33
+ /** spend to date, or a plan's limit window and its reset time */
34
+ usage?: string;
35
+ /** where an API key is created or rotated */
36
+ keys?: string;
37
+ };
38
+ export declare const PROVIDER_DASHBOARDS: Record<string, ProviderDashboard>;
@@ -1,5 +1,6 @@
1
1
  import type { PushPermission, ShellPermission } from "../external.ts";
2
2
  import type { RouterTier } from "../models.ts";
3
+ import type { CommercialRefusal } from "./billingErrors.ts";
3
4
  import type { RepoContext } from "./github.ts";
4
5
  export interface Mode {
5
6
  id: string;
@@ -54,15 +55,6 @@ export interface RepoSettings {
54
55
  * `"none"` = no card; `"payg"` = card on file.
55
56
  */
56
57
  export type AccountPlan = "none" | "payg";
57
- /**
58
- * The org commercial gate's refusal (billing model v2). Set when run-context
59
- * returns 402 for a `paused`/`unpaid` org — the backstop for manual re-runs and
60
- * self-configured triggers that never hit reserveRun. main.ts stops before
61
- * installing the agent or loading account secrets and writes actionable copy.
62
- * A forked action can bypass this response, so proxy-token enforces the same
63
- * verdict before issuing a Pullfrog Router key.
64
- */
65
- export type CommercialRefusal = "commercial" | "subscription_unpaid";
66
58
  export interface RunContext {
67
59
  settings: RepoSettings;
68
60
  apiToken: string;
@@ -70,6 +62,14 @@ export interface RunContext {
70
62
  plan: AccountPlan;
71
63
  proxyModel?: string | undefined;
72
64
  dbSecrets?: Record<string, string> | undefined;
65
+ /**
66
+ * The org commercial gate's refusal (billing model v2). Set when run-context
67
+ * returns 402 for a `paused`/`unpaid` org — the backstop for manual re-runs and
68
+ * self-configured triggers that never hit reserveRun. main.ts stops before
69
+ * installing the agent or loading account secrets and writes actionable copy.
70
+ * A forked action can bypass this response, so proxy-token enforces the same
71
+ * verdict before issuing a Pullfrog Router key.
72
+ */
73
73
  commercialRefused?: CommercialRefusal | undefined;
74
74
  /**
75
75
  * the server tried and failed to materialize Pullfrog-stored secrets (or we
@@ -1,7 +1,8 @@
1
1
  import type { Octokit } from "@octokit/rest";
2
2
  import type { RouterTier } from "../models.ts";
3
+ import type { CommercialRefusal } from "./billingErrors.ts";
3
4
  import { type OctokitWithPlugins } from "./github.ts";
4
- import { type AccountPlan, type CommercialRefusal, type RepoSettings } from "./runContext.ts";
5
+ import { type AccountPlan, type RepoSettings } from "./runContext.ts";
5
6
  export interface RunContextData {
6
7
  repo: {
7
8
  owner: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "cliContractVersion": "1",
5
5
  "type": "module",
6
6
  "bin": {