pullfrog 0.1.64 → 0.1.66

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.
@@ -22,3 +22,5 @@ export { createLeapingProgressComment, deleteProgressCommentApi, getProgressComm
22
22
  export type { RunStatusCheckConclusion, RunStatusCheckOctokit, } from "../utils/runStatusCheck.ts";
23
23
  export { APPROVAL_CHECK_NAME, createRunStatusCheck, finalizeRunStatusCheck, RUN_STATUS_CHECK_NAME, runStatusCheckNeedsFinalizing, } from "../utils/runStatusCheck.ts";
24
24
  export { isValidTimeString, parseTimeString, TIMEOUT_DISABLED, } from "../utils/time.ts";
25
+ export type { XaiAuthBody } from "../utils/xaiOAuth.ts";
26
+ export { parseXaiAuthBody, refreshXaiAuthBody, stringifyXaiAuthBody, } from "../utils/xaiOAuth.ts";
package/dist/internal.js CHANGED
@@ -185,7 +185,12 @@ var providers = {
185
185
  displayName: "O3",
186
186
  resolve: "openai/o3",
187
187
  effort: ["low", "medium", "high"],
188
- openRouterResolve: "openrouter/openai/o3"
188
+ openRouterResolve: "openrouter/openai/o3",
189
+ // OpenRouter publishes a reasoning TOGGLE for o3 where direct OpenAI
190
+ // publishes a ladder, so the route genuinely has no rungs. `[]` rather
191
+ // than omitting the field: an absent openRouterEffort falls through to
192
+ // `effort` and would send a rung this route rejects.
193
+ openRouterEffort: []
189
194
  }
190
195
  }
191
196
  }),
@@ -216,6 +221,11 @@ var providers = {
216
221
  xai: provider({
217
222
  displayName: "xAI",
218
223
  envVars: ["XAI_API_KEY"],
224
+ // CLI-only, like CODEX_AUTH_JSON: a Grok subscription chain minted by
225
+ // `pullfrog auth grok`. Excluded from every paste/prompt surface because
226
+ // the refresh token rotates on every use, so a hand-pasted blob is stale
227
+ // the moment it is saved. See wiki/grok-auth.md.
228
+ managedCredentials: ["GROK_AUTH_JSON"],
219
229
  models: {
220
230
  grok: {
221
231
  displayName: "Grok",
@@ -350,14 +360,19 @@ var providers = {
350
360
  effort: ["low", "medium", "high", "xhigh", "max"],
351
361
  openRouterResolve: "openrouter/anthropic/claude-opus-5",
352
362
  subagentModel: "claude-sonnet",
353
- // TEMPORARY — clear this when Zen serves opus again. Zen still LISTS
354
- // claude-opus-5 in /zen/v1/models, so the catalog test passes, but the
355
- // endpoint answers 503 `Upstream request failed: Endpoint is
356
- // unavailable.` (measured 5/5; claude-sonnet-5, claude-opus-4-8,
357
- // claude-haiku-4-5 and claude-fable-5 all 200 on the same key). opencode
358
- // retries the 503 above the AI SDK emitting no part.updated, so a run
359
- // just produces nothing until it is killed — metaideas/init logged six
360
- // zero-output failures from 2026-08-23. see wiki/opencode-silent-stall.md
363
+ // TEMPORARY — clear this ONLY when opus completes a run through opencode,
364
+ // never when the endpoint merely answers. Zen LISTS claude-opus-5 in
365
+ // /zen/v1/models, so the catalog test passes; on 2026-08-25 the endpoint
366
+ // itself answered 503 `Upstream request failed: Endpoint is unavailable.`
367
+ // (measured 5/5; claude-sonnet-5, claude-opus-4-8, claude-haiku-4-5 and
368
+ // claude-fable-5 all 200 on the same key). opencode retries above the AI
369
+ // SDK emitting no part.updated, so a run just produces nothing until it is
370
+ // killed — metaideas/init logged six zero-output failures from 2026-08-23.
371
+ // The 503 has since cleared and the model is STILL unusable: re-measured
372
+ // 2026-08-26, direct POST /zen/v1/messages is 10/10 200 at 1.3-4.1s while
373
+ // `opencode run --model opencode/claude-opus-5` on the same trivial prompt
374
+ // emitted nothing for 240s in CI. A raw-endpoint 200 is not runtime
375
+ // availability. see wiki/opencode-silent-stall.md
361
376
  fallback: "opencode/claude-sonnet"
362
377
  },
363
378
  "claude-sonnet": {
@@ -454,7 +469,13 @@ var providers = {
454
469
  },
455
470
  "kimi-k2": {
456
471
  displayName: "Kimi K2",
457
- resolve: "opencode/kimi-k2.7-code",
472
+ // k2.7-code is UNDEPLOYED on Zen — still listed in /zen/v1/models, but
473
+ // the endpoint answers 400 `[NOT_FOUND] Model not found, inaccessible,
474
+ // and/or not deployed` (measured 2026-08-28; k2.6 200 on the same key).
475
+ // opencode-go is no escape: its own kimi-k2 resolves to the SAME model
476
+ // id. k2.6 is the newest build Zen will actually serve, and the mirror
477
+ // guard permits the step down because k2.7-code is in ZEN_UNDEPLOYED.
478
+ resolve: "opencode/kimi-k2.6",
458
479
  openRouterResolve: "openrouter/moonshotai/kimi-k2.7-code"
459
480
  },
460
481
  // slug pins the m2 line for DB stability; resolve tracks the current m2.7.
@@ -488,22 +509,139 @@ var providers = {
488
509
  }
489
510
  }
490
511
  }),
512
+ // OpenCode Go is a separate $10/mo subscription from Zen, served on its own
513
+ // base URL (`https://opencode.ai/zen/go/v1`) but authenticated with the SAME
514
+ // `OPENCODE_API_KEY`. it carries the open-weight coding models plus a couple
515
+ // of frontier ones, and 14 of the ids below are served ONLY here — Zen's
516
+ // `/v1/models` does not list glm-5.3*, qwen3.7/3.8-*, mimo-*, longcat-2.0,
517
+ // hy3 or muse-spark. so for a Go subscriber this provider is not a duplicate
518
+ // route to Zen, it is the only route to most of what they pay for.
519
+ // like `opencode` and `openrouter` this is a ROUTER, not a vendor: slugs and
520
+ // display names mirror the upstream brand tier, and the picker groups them
521
+ // under the upstream vendor.
491
522
  "opencode-go": provider({
492
523
  displayName: "OpenCode Go",
493
524
  envVars: ["OPENCODE_API_KEY"],
494
525
  models: {
526
+ // Z.ai — the plan's flagship coding family, and the only route the
527
+ // catalog offers to GLM at all.
528
+ glm: {
529
+ displayName: "GLM",
530
+ resolve: "opencode-go/glm-5.3",
531
+ effort: ["low", "high", "max"],
532
+ openRouterResolve: "openrouter/z-ai/glm-5.3",
533
+ preferred: true,
534
+ subagentModel: "glm-flash"
535
+ },
536
+ "glm-flash": {
537
+ displayName: "GLM Flash",
538
+ resolve: "opencode-go/glm-5.3-flash",
539
+ effort: ["low", "high", "max"],
540
+ openRouterResolve: "openrouter/z-ai/glm-5.3-flash"
541
+ },
542
+ // legacy alias — the slug pinned a version instead of a brand tier and
543
+ // was already resolving to 5.2 under a "GLM 5.2" label. folds forward to
544
+ // the tier slug; 16 repos and 9 accounts still hold it.
495
545
  "glm-5.1": {
496
546
  displayName: "GLM 5.2",
497
547
  resolve: "opencode-go/glm-5.2",
498
548
  effort: ["high", "max"],
499
549
  openRouterEffort: ["high", "xhigh"],
500
550
  openRouterResolve: "openrouter/z-ai/glm-5.2",
501
- preferred: true
551
+ fallback: "opencode-go/glm"
552
+ },
553
+ // Moonshot — parity with moonshotai/* and openrouter/*.
554
+ "kimi-k3": {
555
+ displayName: "Kimi K3",
556
+ resolve: "opencode-go/kimi-k3",
557
+ // Go publishes a single rung for K3 where the OpenRouter route
558
+ // publishes three, so every position lands on `max` here.
559
+ effort: ["max"],
560
+ openRouterEffort: ["low", "high", "max"],
561
+ openRouterResolve: "openrouter/moonshotai/kimi-k3",
562
+ subagentModel: "kimi-k2"
502
563
  },
503
564
  "kimi-k2": {
504
565
  displayName: "Kimi K2",
505
566
  resolve: "opencode-go/kimi-k2.7-code",
506
567
  openRouterResolve: "openrouter/moonshotai/kimi-k2.7-code"
568
+ },
569
+ // DeepSeek and Muse Spark are deliberately ABSENT even though Go serves
570
+ // them and prices DeepSeek Pro below Zen. each sits behind a per-workspace
571
+ // opt-in that is off by default — measured, the run dies with
572
+ // `RegionError` ("only available hosted in China") and `DataPolicyError`
573
+ // ("collects data used to improve its quality"). that toggle lives on the
574
+ // CUSTOMER's OpenCode workspace, so no change here can satisfy it, and a
575
+ // picker row that fails for almost everyone is worse than none. DeepSeek
576
+ // stays reachable ungated via `deepseek/*`, `opencode/*` and
577
+ // `openrouter/*`; both remain runnable by full specifier once opted in.
578
+ // Alibaba — new vendor family for the catalog; Zen serves neither tier.
579
+ "qwen-max": {
580
+ displayName: "Qwen Max",
581
+ resolve: "opencode-go/qwen3.8-max",
582
+ // both routes publish rungs, and they are different sets rather than
583
+ // different spellings of one — OpenRouter carries a `minimal` and a
584
+ // `high` the Go route does not.
585
+ effort: ["low", "medium", "xhigh"],
586
+ openRouterEffort: ["minimal", "low", "medium", "high", "xhigh"],
587
+ openRouterResolve: "openrouter/qwen/qwen3.8-max"
588
+ },
589
+ "qwen-plus": {
590
+ displayName: "Qwen Plus",
591
+ resolve: "opencode-go/qwen3.7-plus",
592
+ openRouterResolve: "openrouter/qwen/qwen3.7-plus"
593
+ },
594
+ // MiniMax — parity with opencode/* and openrouter/*; the m2 slug pins the
595
+ // line for DB stability while the resolve tracks the current m2.7.
596
+ "minimax-m3": {
597
+ displayName: "MiniMax M3",
598
+ resolve: "opencode-go/minimax-m3",
599
+ openRouterResolve: "openrouter/minimax/minimax-m3"
600
+ },
601
+ "minimax-m2.5": {
602
+ displayName: "MiniMax M2",
603
+ resolve: "opencode-go/minimax-m2.7",
604
+ openRouterResolve: "openrouter/minimax/minimax-m2.7"
605
+ },
606
+ // Xiaomi — Go-only; Zen serves the free `mimo-v2-pro-free` promo instead.
607
+ "mimo-pro": {
608
+ displayName: "MiMo Pro",
609
+ resolve: "opencode-go/mimo-v2.5-pro",
610
+ openRouterResolve: "openrouter/xiaomi/mimo-v2.5-pro"
611
+ },
612
+ // Meituan — Go-only.
613
+ longcat: {
614
+ displayName: "LongCat",
615
+ resolve: "opencode-go/longcat-2.0",
616
+ openRouterResolve: "openrouter/meituan/longcat-2.0"
617
+ },
618
+ // Tencent — Go-only, and the cheapest model on the plan by an order of
619
+ // magnitude (0.0175/0.0725). Hy3 succeeds the Hunyuan 2.0 line, so the
620
+ // generation is part of the product name the way Kimi K2/K3 is.
621
+ hy3: {
622
+ displayName: "Hy3",
623
+ resolve: "opencode-go/hy3",
624
+ effort: ["none", "low", "high"],
625
+ openRouterResolve: "openrouter/tencent/hy3"
626
+ },
627
+ // xAI and OpenAI — the two non-open models on the plan. same list price as
628
+ // Zen, but a Go subscription covers them where Zen meters them.
629
+ // Go is the only route that has retired grok-4.5 (models.dev marks
630
+ // `opencode-go/grok-4.5` deprecated while every other provider still
631
+ // serves it), so this alias LEADS `xai/grok` by a generation. a mirror
632
+ // that leads is safe; it is the trailing case that rots — see
633
+ // wiki/models-catalog.md on `opencode/kimi-k2`.
634
+ grok: {
635
+ displayName: "Grok",
636
+ resolve: "opencode-go/grok-4.6",
637
+ effort: ["low", "medium", "high", "xhigh"],
638
+ openRouterResolve: "openrouter/x-ai/grok-4.6"
639
+ },
640
+ "gpt-luna": {
641
+ displayName: "GPT Luna",
642
+ resolve: "opencode-go/gpt-5.6-luna",
643
+ effort: ["none", "low", "medium", "high", "xhigh", "max"],
644
+ openRouterResolve: "openrouter/openai/gpt-5.6-luna"
507
645
  }
508
646
  }
509
647
  }),
@@ -680,7 +818,6 @@ var providers = {
680
818
  "o4-mini": {
681
819
  displayName: "O4 Mini",
682
820
  resolve: "openrouter/openai/o4-mini",
683
- effort: ["low", "medium", "high"],
684
821
  openRouterResolve: "openrouter/openai/o4-mini"
685
822
  },
686
823
  "gemini-pro": {
@@ -1415,17 +1552,48 @@ function stripExistingFooter(body) {
1415
1552
  return body.substring(0, dividerIndex).trimEnd();
1416
1553
  }
1417
1554
 
1418
- // utils/codexOAuth.ts
1419
- var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1420
- var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
1555
+ // utils/oauthShared.ts
1421
1556
  var OAuthInvalidGrantError = class extends Error {
1422
1557
  status;
1423
- constructor(status, body) {
1424
- super(`Codex token refresh failed: ${status} ${body}`);
1558
+ /** whether the provider said this refresh chain is permanently unusable, so
1559
+ * the rotation core may latch it out of use until the user re-mints. */
1560
+ chainIsDead;
1561
+ constructor(provider2, status, body, chainIsDead) {
1562
+ super(`${provider2} token refresh failed: ${status} ${body}`);
1425
1563
  this.name = "OAuthInvalidGrantError";
1426
1564
  this.status = status;
1565
+ this.chainIsDead = chainIsDead;
1427
1566
  }
1428
1567
  };
1568
+ function parseOAuthErrorBody(body) {
1569
+ try {
1570
+ const parsed = JSON.parse(body);
1571
+ if (parsed && typeof parsed === "object") return parsed;
1572
+ } catch {
1573
+ }
1574
+ return null;
1575
+ }
1576
+ function decodeJwtExpMs(token) {
1577
+ const parts = token.split(".");
1578
+ if (parts.length !== 3) return null;
1579
+ let payload;
1580
+ try {
1581
+ payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1582
+ } catch {
1583
+ return null;
1584
+ }
1585
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
1586
+ return payload.exp * 1e3;
1587
+ }
1588
+
1589
+ // utils/codexOAuth.ts
1590
+ var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1591
+ var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
1592
+ function codexChainIsDead(body) {
1593
+ const err = parseOAuthErrorBody(body)?.error;
1594
+ if (!err || typeof err !== "object") return false;
1595
+ return "code" in err && err.code === "token_expired";
1596
+ }
1429
1597
  async function refreshCodexAuthBody(body) {
1430
1598
  const response = await fetch(CODEX_OAUTH_TOKEN_URL, {
1431
1599
  method: "POST",
@@ -1440,7 +1608,7 @@ async function refreshCodexAuthBody(body) {
1440
1608
  if (!response.ok) {
1441
1609
  const text = await response.text().catch(() => "");
1442
1610
  if (response.status >= 400 && response.status < 500) {
1443
- throw new OAuthInvalidGrantError(response.status, text);
1611
+ throw new OAuthInvalidGrantError("Codex", response.status, text, codexChainIsDead(text));
1444
1612
  }
1445
1613
  throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
1446
1614
  }
@@ -1458,18 +1626,6 @@ async function refreshCodexAuthBody(body) {
1458
1626
  last_refresh: (/* @__PURE__ */ new Date()).toISOString()
1459
1627
  };
1460
1628
  }
1461
- function decodeJwtExpMs(token) {
1462
- const parts = token.split(".");
1463
- if (parts.length !== 3) return null;
1464
- let payload;
1465
- try {
1466
- payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1467
- } catch {
1468
- return null;
1469
- }
1470
- if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
1471
- return payload.exp * 1e3;
1472
- }
1473
1629
  function parseCodexAuthBody(raw) {
1474
1630
  let parsed;
1475
1631
  try {
@@ -1743,7 +1899,12 @@ function disableCheckLine(owner, repo) {
1743
1899
  const url = `https://pullfrog.com/console/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}#auto-review-prs`;
1744
1900
  return `
1745
1901
 
1746
- [Turn off this check \u2192](${url}) \u2014 it reports run status only and gates nothing unless you required it in branch protection.`;
1902
+ This check reports run status only and gates nothing unless you required it in branch protection. [Turn off the run status check \u2192](${url})`;
1903
+ }
1904
+ function logsLine(detailsUrl) {
1905
+ return detailsUrl ? `
1906
+
1907
+ [View the run logs \u2192](${detailsUrl})` : "";
1747
1908
  }
1748
1909
  var TERMINAL_OUTPUT = {
1749
1910
  success: {
@@ -1752,7 +1913,7 @@ var TERMINAL_OUTPUT = {
1752
1913
  },
1753
1914
  failure: {
1754
1915
  title: "Pullfrog run failed",
1755
- summary: "The Pullfrog run failed. See the run logs for details."
1916
+ summary: "The Pullfrog run failed."
1756
1917
  },
1757
1918
  cancelled: {
1758
1919
  title: "Pullfrog run cancelled",
@@ -1760,11 +1921,11 @@ var TERMINAL_OUTPUT = {
1760
1921
  },
1761
1922
  timed_out: {
1762
1923
  title: "Pullfrog run timed out",
1763
- summary: "The Pullfrog run exceeded its timeout. See the run logs for details."
1924
+ summary: "The Pullfrog run exceeded its timeout."
1764
1925
  },
1765
1926
  action_required: {
1766
1927
  title: "Pullfrog run needs attention",
1767
- summary: "The Pullfrog run stopped and needs attention. See the run logs for details."
1928
+ summary: "The Pullfrog run stopped and needs attention."
1768
1929
  },
1769
1930
  neutral: {
1770
1931
  title: "Pullfrog run finished",
@@ -1780,8 +1941,11 @@ function terminalOutput(params) {
1780
1941
  const review = params.reviewUrl ? `
1781
1942
 
1782
1943
  [View the review Pullfrog posted \u2192](${params.reviewUrl})` : "";
1783
- const disable = params.conclusion === "success" ? "" : disableCheckLine(params.owner, params.repo);
1784
- return { title: base.title, summary: base.summary + review + disable };
1944
+ if (params.conclusion === "success") return { title: base.title, summary: base.summary + review };
1945
+ return {
1946
+ title: base.title,
1947
+ summary: base.summary + logsLine(params.detailsUrl) + review + disableCheckLine(params.owner, params.repo)
1948
+ };
1785
1949
  }
1786
1950
  async function createRunStatusCheck(params) {
1787
1951
  const existing = await params.octokit.rest.checks.listForRef({
@@ -1818,6 +1982,7 @@ async function finalizeRunStatusCheck(params) {
1818
1982
  conclusion: params.conclusion,
1819
1983
  owner: params.owner,
1820
1984
  repo: params.repo,
1985
+ detailsUrl: params.detailsUrl,
1821
1986
  reviewUrl: params.reviewUrl
1822
1987
  })
1823
1988
  };
@@ -1851,6 +2016,69 @@ function parseTimeString(input) {
1851
2016
  function isValidTimeString(input) {
1852
2017
  return parseTimeString(input) !== null;
1853
2018
  }
2019
+
2020
+ // utils/xaiOAuth.ts
2021
+ var XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
2022
+ var XAI_OAUTH_TOKEN_URL = "https://auth.x.ai/oauth2/token";
2023
+ function xaiChainIsDead(body) {
2024
+ return parseOAuthErrorBody(body)?.error === "invalid_grant";
2025
+ }
2026
+ async function refreshXaiAuthBody(body) {
2027
+ const response = await fetch(XAI_OAUTH_TOKEN_URL, {
2028
+ method: "POST",
2029
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2030
+ body: new URLSearchParams({
2031
+ grant_type: "refresh_token",
2032
+ refresh_token: body.tokens.refresh_token,
2033
+ client_id: XAI_OAUTH_CLIENT_ID
2034
+ }).toString(),
2035
+ signal: AbortSignal.timeout(1e4)
2036
+ });
2037
+ if (!response.ok) {
2038
+ const text = await response.text().catch(() => "");
2039
+ if (response.status >= 400 && response.status < 500) {
2040
+ throw new OAuthInvalidGrantError("Grok", response.status, text, xaiChainIsDead(text));
2041
+ }
2042
+ throw new Error(`Grok token refresh failed: ${response.status} ${text}`);
2043
+ }
2044
+ const tokens = await response.json();
2045
+ return {
2046
+ auth_mode: "grok",
2047
+ tokens: {
2048
+ access_token: tokens.access_token,
2049
+ // xAI rotates on every use, but tolerate a server that echoes nothing
2050
+ // rather than writing an empty refresh token that bricks the chain.
2051
+ refresh_token: tokens.refresh_token || body.tokens.refresh_token
2052
+ },
2053
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString()
2054
+ };
2055
+ }
2056
+ function parseXaiAuthBody(raw) {
2057
+ let parsed;
2058
+ try {
2059
+ parsed = JSON.parse(raw);
2060
+ } catch {
2061
+ return null;
2062
+ }
2063
+ if (!parsed || typeof parsed !== "object") return null;
2064
+ const v = parsed;
2065
+ if (v.auth_mode !== "grok") return null;
2066
+ const tokens = v.tokens;
2067
+ if (!tokens || typeof tokens !== "object") return null;
2068
+ const t = tokens;
2069
+ if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
2070
+ if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
2071
+ return {
2072
+ auth_mode: "grok",
2073
+ ...typeof v.refresh_rejected_at === "string" ? { refresh_rejected_at: v.refresh_rejected_at } : {},
2074
+ tokens: { access_token: t.access_token, refresh_token: t.refresh_token },
2075
+ ...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
2076
+ };
2077
+ }
2078
+ function stringifyXaiAuthBody(body) {
2079
+ return `${JSON.stringify(body, null, 2)}
2080
+ `;
2081
+ }
1854
2082
  export {
1855
2083
  APPROVAL_CHECK_NAME,
1856
2084
  AUTO_EFFICIENT,
@@ -1898,9 +2126,11 @@ export {
1898
2126
  parseEffortPosition,
1899
2127
  parseModel,
1900
2128
  parseTimeString,
2129
+ parseXaiAuthBody,
1901
2130
  providers,
1902
2131
  pullfrogMcpName,
1903
2132
  refreshCodexAuthBody,
2133
+ refreshXaiAuthBody,
1904
2134
  resolveAutoTier,
1905
2135
  resolveCliModel,
1906
2136
  resolveDisplayAlias,
@@ -1912,6 +2142,7 @@ export {
1912
2142
  rungLabel,
1913
2143
  rungPosition,
1914
2144
  stringifyCodexAuthBody,
2145
+ stringifyXaiAuthBody,
1915
2146
  stripExistingFooter,
1916
2147
  truncateAtLineBoundary,
1917
2148
  updateProgressComment,
package/dist/models.d.ts CHANGED
@@ -68,7 +68,8 @@ export interface ModelAlias {
68
68
  effort: readonly string[] | undefined;
69
69
  /** effort rungs on the OpenRouter route, when they differ from `effort` (the
70
70
  * DeepSeek/GLM top rung is `max` natively but `xhigh` via OpenRouter).
71
- * undefined falls back to `effort`. */
71
+ * undefined falls back to `effort`; `[]` says the OpenRouter route publishes
72
+ * no rungs at all, which undefined cannot express. */
72
73
  openRouterEffort: readonly string[] | undefined;
73
74
  /** hide from selectable lists (UI dropdowns, CLI pickers). does NOT affect
74
75
  * resolution — for that use `fallback`. used to keep a redundant alias out of
@@ -26,7 +26,7 @@ export type StoredPushDest = {
26
26
  * Lives here (not in `mcp/review.ts`) so `ToolState` — which caches
27
27
  * `Map<path, CommentableLines>` per checkout — does not pull the MCP server
28
28
  * graph into every consumer of run state (the action's main loop, agent
29
- * harnesses, cf-worker indexing).
29
+ * harnesses).
30
30
  */
31
31
  export type CommentableLines = {
32
32
  RIGHT: Set<number>;
@@ -1,3 +1,25 @@
1
+ /** shared sentence in the four `resolveSlug` routing-slug throws. */
2
+ export declare const BYOK_SLUG_MARKER = "env var is required when the model is set to";
3
+ /** shared sentence in the four `validate*Setup` throws. */
4
+ export declare const BYOK_CONFIG_MARKER = "selected but required configuration is missing:";
5
+ /**
6
+ * A BYOK provider setup is incomplete, at either of the two points that catch it:
7
+ * `resolveSlug` refusing a routing slug whose backing env var is unset, and the
8
+ * per-provider `validate*Setup` check that runs once the slug resolves. All eight
9
+ * messages already name what is missing, the remedy and a docs page, so the
10
+ * renderer passes them through rather than rewriting them. Before this they
11
+ * matched no branch at all, and `renderRunError` writes `errorMessage` to the job
12
+ * summary but not to the comment — so the answer existed on every one of these
13
+ * runs and the customer's PR comment read `Run failed.`
14
+ *
15
+ * Matched from character zero rather than searched anywhere in the text, because
16
+ * a match publishes the WHOLE message verbatim to a customer's PR and
17
+ * `errorMessage` is not always ours: `postRun` builds one out of a repo's own
18
+ * stop-hook output, which is arbitrary text the customer's script printed. A
19
+ * message that merely CONTAINS one of these errors is something quoting us, and
20
+ * passing it through would republish everything around it.
21
+ */
22
+ export declare function isByokSetupError(text: string): boolean;
1
23
  /**
2
24
  * marker for the distinct "run-context couldn't hand over your stored secrets"
3
25
  * body. surfaced verbatim by `runErrorRenderer` (same contract as
@@ -73,6 +95,14 @@ export declare function validateAgentApiKey(params: {
73
95
  * - org-disabled Claude subscription (#1072): `Your organization has
74
96
  * disabled Claude subscription access for Claude Code`, an entitlement
75
97
  * denial with its own remedy — see isClaudeSubscriptionDisabledError.
98
+ * - OpenAI rejected key: `Incorrect API key provided: sk-proj-***`, OpenAI's
99
+ * own wording for a revoked, rotated or wrong-project key. Absent from this
100
+ * list every such run rendered the generic one-line "Run failed." with no
101
+ * cause at all — measured on two accounts sitting at 100% failure, one of
102
+ * them for a week. Matched on the full phrase rather than "Incorrect API
103
+ * key" alone for the reason the markers above are phrased distinctively:
104
+ * this predicate short-circuits the whole rendering pipeline on a substring
105
+ * of arbitrary agent output.
76
106
  */
77
107
  export declare function isApiKeyAuthError(text: string): boolean;
78
108
  /**
@@ -33,6 +33,27 @@ export interface InstalledCodexAuth {
33
33
  * `process.env.XDG_DATA_HOME` so every opencode subprocess discovers the
34
34
  * auth.json; no refresh, no DB interaction. */
35
35
  export declare function installCodexAuth(): InstalledCodexAuth | null;
36
+ export interface InstalledXaiAuth {
37
+ /** absolute path of the auth.json we wrote — the post-hook diffs it. */
38
+ authPath: string;
39
+ /** value to set as XDG_DATA_HOME for the OpenCode subprocess. */
40
+ xdgDataHome: string;
41
+ /** refresh_token at materialization time. opencode's XaiAuthPlugin rotates
42
+ * in-process on a long run, so the post-hook compares against this to decide
43
+ * whether anything needs writing back. */
44
+ originalRefresh: string;
45
+ }
46
+ /** materialize GROK_AUTH_JSON from env into opencode's auth.json.
47
+ *
48
+ * opencode ships xAI Grok OAuth natively (`XaiAuthPlugin`, added upstream
49
+ * 2026-05-21, present in our pinned 1.18.5) against the same public
50
+ * Grok-CLI OAuth client we mint with, so the stored chain drops straight in.
51
+ * The plugin sends the token to `api.x.ai/v1` — it deliberately sets no
52
+ * baseURL — so there is no CLI proxy and no client-version header in play.
53
+ *
54
+ * returns null when the env var is absent or malformed; caller treats null as
55
+ * "no grok subscription auth, fall through to XAI_API_KEY". */
56
+ export declare function installXaiAuth(): InstalledXaiAuth | null;
36
57
  export interface InstalledCodexHome {
37
58
  /** value to set as CODEX_HOME for the codex subprocess. holds auth.json,
38
59
  * config.toml and the session rollouts the resume path reads. */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pure-stdlib (fetch + Buffer) Codex OAuth refresh + JWT exp decoding.
2
+ * Pure-stdlib (fetch) Codex OAuth refresh.
3
3
  *
4
4
  * Lives here (not in codexAuth.ts) so the Next.js server side can import it
5
5
  * via pullfrog/internal without dragging in node:child_process / spawn /
@@ -9,6 +9,8 @@
9
9
  *
10
10
  * See wiki/codex-auth.md for the end-to-end refresh lifecycle.
11
11
  */
12
+ import { decodeJwtExpMs, OAuthInvalidGrantError } from "./oauthShared.ts";
13
+ export { decodeJwtExpMs, OAuthInvalidGrantError };
12
14
  export interface CodexAuthBody {
13
15
  auth_mode: "chatgpt";
14
16
  tokens: {
@@ -19,8 +21,10 @@ export interface CodexAuthBody {
19
21
  };
20
22
  last_refresh?: string;
21
23
  /**
22
- * ISO timestamp of an `invalid_grant` rejection. OpenAI rotates the refresh
23
- * token on every use, so a rejection is PERMANENT — without a latch the
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
27
+ * token on every use, so such a rejection is PERMANENT — without a latch the
24
28
  * server re-issued the identical doomed refresh on every run (455 futile
25
29
  * round trips in 7 days, one per run, each holding a Postgres row lock across
26
30
  * a 10s external call). Cleared implicitly: `pullfrog auth codex` and
@@ -35,13 +39,6 @@ export interface CodexAuthBody {
35
39
  * refreshed against this client_id. */
36
40
  export declare const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
37
41
  export declare const CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
38
- /** thrown when the OAuth provider rejects the refresh token (4xx). callers
39
- * can distinguish "race-lost / token revoked" from network errors via
40
- * `instanceof OAuthInvalidGrantError`. */
41
- export declare class OAuthInvalidGrantError extends Error {
42
- readonly status: number;
43
- constructor(status: number, body: string);
44
- }
45
42
  /** force one refresh round-trip against the OAuth provider. returns the
46
43
  * rotated Codex-shaped blob (the auth.json body verbatim). does NOT persist
47
44
  * — caller is responsible for writing back to wherever the token lives.
@@ -54,16 +51,6 @@ export declare class OAuthInvalidGrantError extends Error {
54
51
  * always releases and queued callers get a turn instead of timing out on
55
52
  * the tx wrapper. Real OAuth latency is sub-second; 10s is generous. */
56
53
  export declare function refreshCodexAuthBody(body: CodexAuthBody): Promise<CodexAuthBody>;
57
- /** decode the access_token's JWT payload and return its `exp` claim in ms
58
- * since epoch. returns null if the token isn't a parseable JWT or has no
59
- * `exp` claim — caller falls back to "treat as expired".
60
- *
61
- * We don't verify the JWT signature (we'd need OpenAI's JWKS); we're only
62
- * using the claim as a freshness hint. The actual auth check happens
63
- * server-side at OpenAI when the token is used — trusting a fake JWT here
64
- * would just delay the inevitable 401 from OpenAI. No security boundary
65
- * at this decode step. */
66
- export declare function decodeJwtExpMs(token: string): number | null;
67
54
  /** parse + validate a Codex auth.json body from its JSON-string form.
68
55
  * returns null on any shape mismatch — caller treats as "no codex auth". */
69
56
  export declare function parseCodexAuthBody(raw: string): CodexAuthBody | null;
@@ -0,0 +1,44 @@
1
+ /** One provider's post-run writeback plan, handed from the opencode harness to
2
+ * `entryPost.ts` via `core.saveState`. Type-only across that boundary, so it
3
+ * does not violate entryPost's stdlib-only import rule. */
4
+ export interface OAuthWriteback {
5
+ /** Pullfrog secret to PUT the rotated chain back into. */
6
+ secretName: "CODEX_AUTH_JSON" | "GROK_AUTH_JSON";
7
+ /** opencode auth.json provider key this chain lives under. */
8
+ provider: "openai" | "xai";
9
+ authPath: string;
10
+ originalRefresh: string;
11
+ /** codex only — opencode's auth entry has no slot for it. see below. */
12
+ originalIdToken?: string | undefined;
13
+ }
14
+ /** Detect a mid-run Codex OAuth rotation from an on-disk auth.json and render
15
+ * it in the Codex CLI shape the Pullfrog secret store holds. Returns null when
16
+ * the file carries no usable OAuth entry or the refresh token is unchanged from
17
+ * `originalRefresh`. Lives in its own module so `entryPost.ts` can import it
18
+ * without pulling in `codexHome.ts` (which imports `./cli.ts` and node fs
19
+ * helpers).
20
+ *
21
+ * Two on-disk shapes reach here, one per harness:
22
+ * - codex CLI — `{auth_mode, tokens: {id_token, access_token, refresh_token}}`,
23
+ * already the storage shape, so it round-trips verbatim.
24
+ * - opencode — `{openai: {type: "oauth", access, refresh, accountId}}`, which
25
+ * has no slot for `id_token`. The caller passes the pre-run value through
26
+ * `originalIdToken` so the rotated blob keeps it: the codex CLI REFUSES an
27
+ * auth.json without `tokens.id_token` (`missing field 'id_token'`), so
28
+ * dropping it here would silently disqualify the account from ever running
29
+ * on the codex harness. `id_token` is an identity claim that a refresh does
30
+ * not rotate — the server-side `refreshCodexAuthBody` carries the old one
31
+ * forward the same way. */
32
+ export declare function detectCodexRefresh(params: {
33
+ authFileContent: string;
34
+ originalRefresh: string;
35
+ originalIdToken?: string | undefined;
36
+ }): string | null;
37
+ /** Detect a mid-run Grok OAuth rotation and render it in the storage shape.
38
+ * Simpler than the Codex twin: opencode's `xai` entry carries the whole chain,
39
+ * there is no second on-disk shape to reconcile and no identity claim to
40
+ * re-attach. Returns null when the entry is missing or unrotated. */
41
+ export declare function detectXaiRefresh(params: {
42
+ authFileContent: string;
43
+ originalRefresh: string;
44
+ }): string | null;
@@ -42,6 +42,12 @@ export type GitHubAppPermissions = {
42
42
  };
43
43
  type AcquireTokenOptions = {
44
44
  repos?: string[] | undefined;
45
+ /**
46
+ * handle to the server-persisted cross-repo grant, required whenever `repos`
47
+ * names anything beyond the primary. the server bounds the request against
48
+ * the sets IT stored, so this cannot widen the token by itself.
49
+ */
50
+ xrepoGrant?: string | undefined;
45
51
  permissions?: GitHubAppPermissions;
46
52
  /**
47
53
  * stashed OIDC credentials for minting after restricted mode deletes
@@ -1,3 +1,2 @@
1
- export declare const isCloudflareSandbox: boolean;
2
1
  export declare const isGitHubActions: boolean;
3
2
  export declare const isInsideDocker: boolean;