pullfrog 0.1.65 → 0.1.67
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/README.md +1 -0
- package/dist/cli.mjs +10320 -5269
- package/dist/external.d.ts +7 -0
- package/dist/index.js +9891 -5130
- package/dist/internal/index.d.ts +2 -0
- package/dist/internal.js +267 -25
- package/dist/models.d.ts +2 -0
- package/dist/utils/apiKeys.d.ts +15 -0
- package/dist/utils/codexHome.d.ts +21 -0
- package/dist/utils/codexOAuth.d.ts +7 -20
- package/dist/utils/codexRefreshDetect.d.ts +44 -0
- package/dist/utils/github.d.ts +6 -0
- package/dist/utils/oauthShared.d.ts +39 -0
- package/dist/utils/payload.d.ts +2 -0
- package/dist/utils/token.d.ts +6 -0
- package/dist/utils/xaiOAuth.d.ts +74 -0
- package/package.json +1 -1
package/dist/internal/index.d.ts
CHANGED
|
@@ -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
|
@@ -221,6 +221,11 @@ var providers = {
|
|
|
221
221
|
xai: provider({
|
|
222
222
|
displayName: "xAI",
|
|
223
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"],
|
|
224
229
|
models: {
|
|
225
230
|
grok: {
|
|
226
231
|
displayName: "Grok",
|
|
@@ -333,6 +338,69 @@ var providers = {
|
|
|
333
338
|
}
|
|
334
339
|
}
|
|
335
340
|
}),
|
|
341
|
+
// the same models as `moonshotai` above, billed against a Kimi membership
|
|
342
|
+
// instead of a Moonshot API balance — a separate endpoint, separate model
|
|
343
|
+
// ids and a separate key, which is why it is a provider rather than a second
|
|
344
|
+
// `envVars` entry. no provider injection is needed: models.dev carries
|
|
345
|
+
// `kimi-for-coding` (api `https://api.kimi.com/coding/v1`, npm
|
|
346
|
+
// `@ai-sdk/anthropic`) and opencode's env loop enables it from a bare
|
|
347
|
+
// `KIMI_API_KEY`, verified against our pinned opencode-ai@1.18.5 — all four
|
|
348
|
+
// ids list in `opencode models` and a run reaches Kimi's own auth check.
|
|
349
|
+
//
|
|
350
|
+
// NO `openRouterResolve` ON PURPOSE. OpenRouter cannot serve a Kimi
|
|
351
|
+
// membership, so a Router path here would silently bill the wallet for
|
|
352
|
+
// Moonshot pay-as-you-go under a slug picked for the opposite reason. The
|
|
353
|
+
// absence is load-bearing: `isCardGatedModel` reads it as "not Router-
|
|
354
|
+
// resolvable", `decideModelAccess` answers an explicit pick with the
|
|
355
|
+
// `router` copy ("add your own provider key"), and run-context clamps a
|
|
356
|
+
// keyless Router pick to the default subsidy model with `noRouterPath`.
|
|
357
|
+
"kimi-for-coding": provider({
|
|
358
|
+
// "Kimi Code" is the product name on kimi.com/code and in the console the
|
|
359
|
+
// key is minted from. The provider KEY has to stay `kimi-for-coding` — it
|
|
360
|
+
// is models.dev's id, so it is what `enabled_providers` and every
|
|
361
|
+
// `resolve` prefix must spell.
|
|
362
|
+
displayName: "Kimi Code",
|
|
363
|
+
envVars: ["KIMI_API_KEY"],
|
|
364
|
+
models: {
|
|
365
|
+
// the tier each model needs is in `description` because Kimi answers an
|
|
366
|
+
// out-of-tier request with 401 — indistinguishable from a bad key by
|
|
367
|
+
// status alone, and only `isKimiPlanTierError` tells them apart after the
|
|
368
|
+
// fact. saying it in the picker is the half that stops the run happening.
|
|
369
|
+
"kimi-k3": {
|
|
370
|
+
displayName: "Kimi K3",
|
|
371
|
+
description: "1M context, but only on Allegretto and above \xB7 Moderato caps it at 256K",
|
|
372
|
+
resolve: "kimi-for-coding/k3",
|
|
373
|
+
effort: ["low", "high", "max"],
|
|
374
|
+
subagentModel: "kimi-k2"
|
|
375
|
+
},
|
|
376
|
+
// `preferred` sits on the 256K variant rather than the 1M flagship, which
|
|
377
|
+
// inverts the usual "top tier wins" rule for a reason specific to Kimi:
|
|
378
|
+
// below Allegretto the two are the SAME 256K ceiling and `k3` charges
|
|
379
|
+
// roughly double the quota for it, so on the median tier the flagship is
|
|
380
|
+
// strictly dominated rather than merely pricier. This flag seeds the
|
|
381
|
+
// onboarding pick (`getRecommendedSlug`) and wins key-only auto-select
|
|
382
|
+
// (`autoSelectModel`), which are exactly the two moments nobody has
|
|
383
|
+
// chosen yet.
|
|
384
|
+
"kimi-k3-256k": {
|
|
385
|
+
displayName: "Kimi K3 256K",
|
|
386
|
+
description: "Same model at 256K for ~half the quota \xB7 Moderato or above",
|
|
387
|
+
resolve: "kimi-for-coding/k3-256k",
|
|
388
|
+
effort: ["low", "high", "max"],
|
|
389
|
+
preferred: true,
|
|
390
|
+
subagentModel: "kimi-k2"
|
|
391
|
+
},
|
|
392
|
+
"kimi-k2": {
|
|
393
|
+
displayName: "Kimi K2",
|
|
394
|
+
description: "Included in every Kimi membership tier",
|
|
395
|
+
resolve: "kimi-for-coding/kimi-for-coding"
|
|
396
|
+
},
|
|
397
|
+
"kimi-k2-highspeed": {
|
|
398
|
+
displayName: "Kimi K2 HighSpeed",
|
|
399
|
+
description: "~6x output speed for ~3x quota \xB7 needs Allegretto or above",
|
|
400
|
+
resolve: "kimi-for-coding/kimi-for-coding-highspeed"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}),
|
|
336
404
|
opencode: provider({
|
|
337
405
|
displayName: "OpenCode Zen",
|
|
338
406
|
envVars: ["OPENCODE_API_KEY"],
|
|
@@ -464,7 +532,13 @@ var providers = {
|
|
|
464
532
|
},
|
|
465
533
|
"kimi-k2": {
|
|
466
534
|
displayName: "Kimi K2",
|
|
467
|
-
|
|
535
|
+
// k2.7-code is UNDEPLOYED on Zen — still listed in /zen/v1/models, but
|
|
536
|
+
// the endpoint answers 400 `[NOT_FOUND] Model not found, inaccessible,
|
|
537
|
+
// and/or not deployed` (measured 2026-08-28; k2.6 200 on the same key).
|
|
538
|
+
// opencode-go is no escape: its own kimi-k2 resolves to the SAME model
|
|
539
|
+
// id. k2.6 is the newest build Zen will actually serve, and the mirror
|
|
540
|
+
// guard permits the step down because k2.7-code is in ZEN_UNDEPLOYED.
|
|
541
|
+
resolve: "opencode/kimi-k2.6",
|
|
468
542
|
openRouterResolve: "openrouter/moonshotai/kimi-k2.7-code"
|
|
469
543
|
},
|
|
470
544
|
// slug pins the m2 line for DB stability; resolve tracks the current m2.7.
|
|
@@ -879,6 +953,80 @@ var providers = {
|
|
|
879
953
|
openRouterResolve: "openrouter/minimax/minimax-m3"
|
|
880
954
|
}
|
|
881
955
|
}
|
|
956
|
+
}),
|
|
957
|
+
vercel: provider({
|
|
958
|
+
// Vercel AI Gateway — one key serves every model it fronts. model IDs use
|
|
959
|
+
// the models.dev `vercel` catalog's own naming (dotted versions, e.g.
|
|
960
|
+
// `anthropic/claude-opus-5`, `zai/glm-5.3`), and effort ladders mirror that
|
|
961
|
+
// catalog's entries, not the upstream vendor's. deliberately no
|
|
962
|
+
// `openRouterResolve`: a gateway pick is BYOK-only — silently rerouting it
|
|
963
|
+
// over the Router proxy would bill the wallet for traffic the user pointed
|
|
964
|
+
// at their own gateway.
|
|
965
|
+
displayName: "Vercel AI Gateway",
|
|
966
|
+
envVars: ["AI_GATEWAY_API_KEY"],
|
|
967
|
+
models: {
|
|
968
|
+
"claude-opus": {
|
|
969
|
+
displayName: "Claude Opus",
|
|
970
|
+
resolve: "vercel/anthropic/claude-opus-5",
|
|
971
|
+
effort: ["low", "medium", "high", "xhigh", "max"],
|
|
972
|
+
preferred: true,
|
|
973
|
+
subagentModel: "claude-sonnet"
|
|
974
|
+
},
|
|
975
|
+
"claude-sonnet": {
|
|
976
|
+
displayName: "Claude Sonnet",
|
|
977
|
+
resolve: "vercel/anthropic/claude-sonnet-5",
|
|
978
|
+
effort: ["low", "medium", "high", "xhigh"]
|
|
979
|
+
},
|
|
980
|
+
"claude-haiku": {
|
|
981
|
+
displayName: "Claude Haiku",
|
|
982
|
+
resolve: "vercel/anthropic/claude-haiku-4.5"
|
|
983
|
+
},
|
|
984
|
+
"gpt-sol": {
|
|
985
|
+
displayName: "GPT Sol",
|
|
986
|
+
resolve: "vercel/openai/gpt-5.6-sol",
|
|
987
|
+
effort: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
988
|
+
subagentModel: "gpt-terra"
|
|
989
|
+
},
|
|
990
|
+
"gpt-terra": {
|
|
991
|
+
displayName: "GPT Terra",
|
|
992
|
+
resolve: "vercel/openai/gpt-5.6-terra",
|
|
993
|
+
effort: ["none", "low", "medium", "high", "xhigh", "max"]
|
|
994
|
+
},
|
|
995
|
+
"gpt-luna": {
|
|
996
|
+
displayName: "GPT Luna",
|
|
997
|
+
resolve: "vercel/openai/gpt-5.6-luna",
|
|
998
|
+
effort: ["none", "low", "medium", "high", "xhigh", "max"]
|
|
999
|
+
},
|
|
1000
|
+
"gemini-pro": {
|
|
1001
|
+
displayName: "Gemini Pro",
|
|
1002
|
+
resolve: "vercel/google/gemini-3.1-pro-preview",
|
|
1003
|
+
effort: ["low", "medium", "high"]
|
|
1004
|
+
},
|
|
1005
|
+
"gemini-flash": {
|
|
1006
|
+
displayName: "Gemini Flash",
|
|
1007
|
+
resolve: "vercel/google/gemini-3.6-flash",
|
|
1008
|
+
effort: ["minimal", "low", "medium", "high"]
|
|
1009
|
+
},
|
|
1010
|
+
"deepseek-pro": {
|
|
1011
|
+
displayName: "DeepSeek Pro",
|
|
1012
|
+
resolve: "vercel/deepseek/deepseek-v4-pro-0813",
|
|
1013
|
+
effort: ["high", "xhigh"]
|
|
1014
|
+
},
|
|
1015
|
+
"deepseek-flash": {
|
|
1016
|
+
displayName: "DeepSeek Flash",
|
|
1017
|
+
resolve: "vercel/deepseek/deepseek-v4-flash",
|
|
1018
|
+
effort: ["high", "xhigh"]
|
|
1019
|
+
},
|
|
1020
|
+
glm: {
|
|
1021
|
+
displayName: "GLM",
|
|
1022
|
+
resolve: "vercel/zai/glm-5.3",
|
|
1023
|
+
effort: ["low", "high", "max"]
|
|
1024
|
+
},
|
|
1025
|
+
"kimi-k3": {
|
|
1026
|
+
displayName: "Kimi K3",
|
|
1027
|
+
resolve: "vercel/moonshotai/kimi-k3"
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
882
1030
|
})
|
|
883
1031
|
};
|
|
884
1032
|
function parseModel(slug) {
|
|
@@ -1541,17 +1689,48 @@ function stripExistingFooter(body) {
|
|
|
1541
1689
|
return body.substring(0, dividerIndex).trimEnd();
|
|
1542
1690
|
}
|
|
1543
1691
|
|
|
1544
|
-
// utils/
|
|
1545
|
-
var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
1546
|
-
var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
1692
|
+
// utils/oauthShared.ts
|
|
1547
1693
|
var OAuthInvalidGrantError = class extends Error {
|
|
1548
1694
|
status;
|
|
1549
|
-
|
|
1550
|
-
|
|
1695
|
+
/** whether the provider said this refresh chain is permanently unusable, so
|
|
1696
|
+
* the rotation core may latch it out of use until the user re-mints. */
|
|
1697
|
+
chainIsDead;
|
|
1698
|
+
constructor(provider2, status, body, chainIsDead) {
|
|
1699
|
+
super(`${provider2} token refresh failed: ${status} ${body}`);
|
|
1551
1700
|
this.name = "OAuthInvalidGrantError";
|
|
1552
1701
|
this.status = status;
|
|
1702
|
+
this.chainIsDead = chainIsDead;
|
|
1553
1703
|
}
|
|
1554
1704
|
};
|
|
1705
|
+
function parseOAuthErrorBody(body) {
|
|
1706
|
+
try {
|
|
1707
|
+
const parsed = JSON.parse(body);
|
|
1708
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
1709
|
+
} catch {
|
|
1710
|
+
}
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
function decodeJwtExpMs(token) {
|
|
1714
|
+
const parts = token.split(".");
|
|
1715
|
+
if (parts.length !== 3) return null;
|
|
1716
|
+
let payload;
|
|
1717
|
+
try {
|
|
1718
|
+
payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
1719
|
+
} catch {
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
|
|
1723
|
+
return payload.exp * 1e3;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// utils/codexOAuth.ts
|
|
1727
|
+
var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
1728
|
+
var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
1729
|
+
function codexChainIsDead(body) {
|
|
1730
|
+
const err = parseOAuthErrorBody(body)?.error;
|
|
1731
|
+
if (!err || typeof err !== "object") return false;
|
|
1732
|
+
return "code" in err && err.code === "token_expired";
|
|
1733
|
+
}
|
|
1555
1734
|
async function refreshCodexAuthBody(body) {
|
|
1556
1735
|
const response = await fetch(CODEX_OAUTH_TOKEN_URL, {
|
|
1557
1736
|
method: "POST",
|
|
@@ -1566,7 +1745,7 @@ async function refreshCodexAuthBody(body) {
|
|
|
1566
1745
|
if (!response.ok) {
|
|
1567
1746
|
const text = await response.text().catch(() => "");
|
|
1568
1747
|
if (response.status >= 400 && response.status < 500) {
|
|
1569
|
-
throw new OAuthInvalidGrantError(response.status, text);
|
|
1748
|
+
throw new OAuthInvalidGrantError("Codex", response.status, text, codexChainIsDead(text));
|
|
1570
1749
|
}
|
|
1571
1750
|
throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
|
|
1572
1751
|
}
|
|
@@ -1584,18 +1763,6 @@ async function refreshCodexAuthBody(body) {
|
|
|
1584
1763
|
last_refresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
1585
1764
|
};
|
|
1586
1765
|
}
|
|
1587
|
-
function decodeJwtExpMs(token) {
|
|
1588
|
-
const parts = token.split(".");
|
|
1589
|
-
if (parts.length !== 3) return null;
|
|
1590
|
-
let payload;
|
|
1591
|
-
try {
|
|
1592
|
-
payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
1593
|
-
} catch {
|
|
1594
|
-
return null;
|
|
1595
|
-
}
|
|
1596
|
-
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
|
|
1597
|
-
return payload.exp * 1e3;
|
|
1598
|
-
}
|
|
1599
1766
|
function parseCodexAuthBody(raw) {
|
|
1600
1767
|
let parsed;
|
|
1601
1768
|
try {
|
|
@@ -1869,7 +2036,12 @@ function disableCheckLine(owner, repo) {
|
|
|
1869
2036
|
const url = `https://pullfrog.com/console/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}#auto-review-prs`;
|
|
1870
2037
|
return `
|
|
1871
2038
|
|
|
1872
|
-
|
|
2039
|
+
This check reports run status only and gates nothing unless you required it in branch protection. [Turn off the run status check \u2192](${url})`;
|
|
2040
|
+
}
|
|
2041
|
+
function logsLine(detailsUrl) {
|
|
2042
|
+
return detailsUrl ? `
|
|
2043
|
+
|
|
2044
|
+
[View the run logs \u2192](${detailsUrl})` : "";
|
|
1873
2045
|
}
|
|
1874
2046
|
var TERMINAL_OUTPUT = {
|
|
1875
2047
|
success: {
|
|
@@ -1878,7 +2050,7 @@ var TERMINAL_OUTPUT = {
|
|
|
1878
2050
|
},
|
|
1879
2051
|
failure: {
|
|
1880
2052
|
title: "Pullfrog run failed",
|
|
1881
|
-
summary: "The Pullfrog run failed.
|
|
2053
|
+
summary: "The Pullfrog run failed."
|
|
1882
2054
|
},
|
|
1883
2055
|
cancelled: {
|
|
1884
2056
|
title: "Pullfrog run cancelled",
|
|
@@ -1886,11 +2058,11 @@ var TERMINAL_OUTPUT = {
|
|
|
1886
2058
|
},
|
|
1887
2059
|
timed_out: {
|
|
1888
2060
|
title: "Pullfrog run timed out",
|
|
1889
|
-
summary: "The Pullfrog run exceeded its timeout.
|
|
2061
|
+
summary: "The Pullfrog run exceeded its timeout."
|
|
1890
2062
|
},
|
|
1891
2063
|
action_required: {
|
|
1892
2064
|
title: "Pullfrog run needs attention",
|
|
1893
|
-
summary: "The Pullfrog run stopped and needs attention.
|
|
2065
|
+
summary: "The Pullfrog run stopped and needs attention."
|
|
1894
2066
|
},
|
|
1895
2067
|
neutral: {
|
|
1896
2068
|
title: "Pullfrog run finished",
|
|
@@ -1906,8 +2078,11 @@ function terminalOutput(params) {
|
|
|
1906
2078
|
const review = params.reviewUrl ? `
|
|
1907
2079
|
|
|
1908
2080
|
[View the review Pullfrog posted \u2192](${params.reviewUrl})` : "";
|
|
1909
|
-
|
|
1910
|
-
return {
|
|
2081
|
+
if (params.conclusion === "success") return { title: base.title, summary: base.summary + review };
|
|
2082
|
+
return {
|
|
2083
|
+
title: base.title,
|
|
2084
|
+
summary: base.summary + logsLine(params.detailsUrl) + review + disableCheckLine(params.owner, params.repo)
|
|
2085
|
+
};
|
|
1911
2086
|
}
|
|
1912
2087
|
async function createRunStatusCheck(params) {
|
|
1913
2088
|
const existing = await params.octokit.rest.checks.listForRef({
|
|
@@ -1944,6 +2119,7 @@ async function finalizeRunStatusCheck(params) {
|
|
|
1944
2119
|
conclusion: params.conclusion,
|
|
1945
2120
|
owner: params.owner,
|
|
1946
2121
|
repo: params.repo,
|
|
2122
|
+
detailsUrl: params.detailsUrl,
|
|
1947
2123
|
reviewUrl: params.reviewUrl
|
|
1948
2124
|
})
|
|
1949
2125
|
};
|
|
@@ -1977,6 +2153,69 @@ function parseTimeString(input) {
|
|
|
1977
2153
|
function isValidTimeString(input) {
|
|
1978
2154
|
return parseTimeString(input) !== null;
|
|
1979
2155
|
}
|
|
2156
|
+
|
|
2157
|
+
// utils/xaiOAuth.ts
|
|
2158
|
+
var XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
2159
|
+
var XAI_OAUTH_TOKEN_URL = "https://auth.x.ai/oauth2/token";
|
|
2160
|
+
function xaiChainIsDead(body) {
|
|
2161
|
+
return parseOAuthErrorBody(body)?.error === "invalid_grant";
|
|
2162
|
+
}
|
|
2163
|
+
async function refreshXaiAuthBody(body) {
|
|
2164
|
+
const response = await fetch(XAI_OAUTH_TOKEN_URL, {
|
|
2165
|
+
method: "POST",
|
|
2166
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2167
|
+
body: new URLSearchParams({
|
|
2168
|
+
grant_type: "refresh_token",
|
|
2169
|
+
refresh_token: body.tokens.refresh_token,
|
|
2170
|
+
client_id: XAI_OAUTH_CLIENT_ID
|
|
2171
|
+
}).toString(),
|
|
2172
|
+
signal: AbortSignal.timeout(1e4)
|
|
2173
|
+
});
|
|
2174
|
+
if (!response.ok) {
|
|
2175
|
+
const text = await response.text().catch(() => "");
|
|
2176
|
+
if (response.status >= 400 && response.status < 500) {
|
|
2177
|
+
throw new OAuthInvalidGrantError("Grok", response.status, text, xaiChainIsDead(text));
|
|
2178
|
+
}
|
|
2179
|
+
throw new Error(`Grok token refresh failed: ${response.status} ${text}`);
|
|
2180
|
+
}
|
|
2181
|
+
const tokens = await response.json();
|
|
2182
|
+
return {
|
|
2183
|
+
auth_mode: "grok",
|
|
2184
|
+
tokens: {
|
|
2185
|
+
access_token: tokens.access_token,
|
|
2186
|
+
// xAI rotates on every use, but tolerate a server that echoes nothing
|
|
2187
|
+
// rather than writing an empty refresh token that bricks the chain.
|
|
2188
|
+
refresh_token: tokens.refresh_token || body.tokens.refresh_token
|
|
2189
|
+
},
|
|
2190
|
+
last_refresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
function parseXaiAuthBody(raw) {
|
|
2194
|
+
let parsed;
|
|
2195
|
+
try {
|
|
2196
|
+
parsed = JSON.parse(raw);
|
|
2197
|
+
} catch {
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2201
|
+
const v = parsed;
|
|
2202
|
+
if (v.auth_mode !== "grok") return null;
|
|
2203
|
+
const tokens = v.tokens;
|
|
2204
|
+
if (!tokens || typeof tokens !== "object") return null;
|
|
2205
|
+
const t = tokens;
|
|
2206
|
+
if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
|
|
2207
|
+
if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
|
|
2208
|
+
return {
|
|
2209
|
+
auth_mode: "grok",
|
|
2210
|
+
...typeof v.refresh_rejected_at === "string" ? { refresh_rejected_at: v.refresh_rejected_at } : {},
|
|
2211
|
+
tokens: { access_token: t.access_token, refresh_token: t.refresh_token },
|
|
2212
|
+
...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function stringifyXaiAuthBody(body) {
|
|
2216
|
+
return `${JSON.stringify(body, null, 2)}
|
|
2217
|
+
`;
|
|
2218
|
+
}
|
|
1980
2219
|
export {
|
|
1981
2220
|
APPROVAL_CHECK_NAME,
|
|
1982
2221
|
AUTO_EFFICIENT,
|
|
@@ -2024,9 +2263,11 @@ export {
|
|
|
2024
2263
|
parseEffortPosition,
|
|
2025
2264
|
parseModel,
|
|
2026
2265
|
parseTimeString,
|
|
2266
|
+
parseXaiAuthBody,
|
|
2027
2267
|
providers,
|
|
2028
2268
|
pullfrogMcpName,
|
|
2029
2269
|
refreshCodexAuthBody,
|
|
2270
|
+
refreshXaiAuthBody,
|
|
2030
2271
|
resolveAutoTier,
|
|
2031
2272
|
resolveCliModel,
|
|
2032
2273
|
resolveDisplayAlias,
|
|
@@ -2038,6 +2279,7 @@ export {
|
|
|
2038
2279
|
rungLabel,
|
|
2039
2280
|
rungPosition,
|
|
2040
2281
|
stringifyCodexAuthBody,
|
|
2282
|
+
stringifyXaiAuthBody,
|
|
2041
2283
|
stripExistingFooter,
|
|
2042
2284
|
truncateAtLineBoundary,
|
|
2043
2285
|
updateProgressComment,
|
package/dist/models.d.ts
CHANGED
|
@@ -120,6 +120,7 @@ export declare const providers: {
|
|
|
120
120
|
xai: ProviderConfig;
|
|
121
121
|
deepseek: ProviderConfig;
|
|
122
122
|
moonshotai: ProviderConfig;
|
|
123
|
+
"kimi-for-coding": ProviderConfig;
|
|
123
124
|
opencode: ProviderConfig;
|
|
124
125
|
"opencode-go": ProviderConfig;
|
|
125
126
|
bedrock: ProviderConfig;
|
|
@@ -127,6 +128,7 @@ export declare const providers: {
|
|
|
127
128
|
azure: ProviderConfig;
|
|
128
129
|
"openai-compatible": ProviderConfig;
|
|
129
130
|
openrouter: ProviderConfig;
|
|
131
|
+
vercel: ProviderConfig;
|
|
130
132
|
};
|
|
131
133
|
export type ModelProvider = keyof typeof providers;
|
|
132
134
|
export declare function parseModel(slug: string): {
|
package/dist/utils/apiKeys.d.ts
CHANGED
|
@@ -135,6 +135,21 @@ export declare function isMalformedKeyError(text: string): boolean;
|
|
|
135
135
|
* user controls fixes it, so it gets its own copy instead of the re-auth CTA.
|
|
136
136
|
*/
|
|
137
137
|
export declare function isClaudeSubscriptionDisabledError(text: string): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Kimi Code entitlement denial — the key is valid and the membership is live,
|
|
140
|
+
* but the tier doesn't include the model or the context window the run asked
|
|
141
|
+
* for (`k3` and `k3-256k` need Moderato, `kimi-for-coding-highspeed` and K3's
|
|
142
|
+
* 1M window need Allegretto). Kimi answers all of them with **401**, the same
|
|
143
|
+
* status as a revoked key, so nothing but the sentence tells them apart — and
|
|
144
|
+
* without that the run renders "rotate the key in your provider dashboard",
|
|
145
|
+
* which names three things that will not fix it.
|
|
146
|
+
*
|
|
147
|
+
* Both published shapes, parameterised over the clause that varies rather than
|
|
148
|
+
* the model id inside it, per `isOAuthCredentialExpiredError`. Listed in
|
|
149
|
+
* `isApiKeyAuthError` below so the branch is reachable whatever `type` Kimi
|
|
150
|
+
* puts in the payload. See wiki/kimi-code.md.
|
|
151
|
+
*/
|
|
152
|
+
export declare function isKimiPlanTierError(text: string): boolean;
|
|
138
153
|
export declare function isClaudeSessionLimitError(text: string): boolean;
|
|
139
154
|
/**
|
|
140
155
|
* The provider rejected a credential we checked before the agent started, and
|
|
@@ -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
|
|
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
|
|
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
|
|
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;
|
package/dist/utils/github.d.ts
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic OAuth primitives shared by the Codex and Grok chains.
|
|
3
|
+
* Pure stdlib (Buffer) so both the action runtime and the Next.js server can
|
|
4
|
+
* import it via pullfrog/internal.
|
|
5
|
+
*
|
|
6
|
+
* Both chains mint JWT access tokens whose `exp` we read as a freshness hint,
|
|
7
|
+
* and both rotate their refresh token on every use — so both need the same
|
|
8
|
+
* "the provider rejected this permanently" signal.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Thrown when an OAuth provider rejects a refresh token (4xx).
|
|
12
|
+
*
|
|
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.
|
|
20
|
+
*/
|
|
21
|
+
export declare class OAuthInvalidGrantError extends Error {
|
|
22
|
+
readonly status: number;
|
|
23
|
+
/** whether the provider said this refresh chain is permanently unusable, so
|
|
24
|
+
* the rotation core may latch it out of use until the user re-mints. */
|
|
25
|
+
readonly chainIsDead: boolean;
|
|
26
|
+
constructor(provider: string, status: number, body: string, chainIsDead: boolean);
|
|
27
|
+
}
|
|
28
|
+
/** parse a token-endpoint error body, or null when it is not a JSON object.
|
|
29
|
+
* Each provider's refresh reads its own discriminator out of the result. */
|
|
30
|
+
export declare function parseOAuthErrorBody(body: string): Record<string, unknown> | null;
|
|
31
|
+
/** decode a JWT payload's `exp` claim and return it in ms since epoch.
|
|
32
|
+
* returns null if the token isn't a parseable JWT or has no `exp` claim —
|
|
33
|
+
* caller falls back to "treat as expired".
|
|
34
|
+
*
|
|
35
|
+
* We don't verify the signature (we'd need the issuer's JWKS); we're only
|
|
36
|
+
* using the claim as a freshness hint. The real auth check happens at the
|
|
37
|
+
* provider when the token is used, so trusting a forged JWT here would just
|
|
38
|
+
* delay the inevitable 401. No security boundary at this decode step. */
|
|
39
|
+
export declare function decodeJwtExpMs(token: string): number | null;
|
package/dist/utils/payload.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const JsonPayload: import("arktype/internal/variants/object.ts").
|
|
|
19
19
|
write: string[];
|
|
20
20
|
unavailable?: string[];
|
|
21
21
|
} | undefined;
|
|
22
|
+
xrepoGrant?: string | undefined;
|
|
22
23
|
timeout?: string | undefined;
|
|
23
24
|
progressComment?: {
|
|
24
25
|
id: string;
|
|
@@ -66,6 +67,7 @@ export declare function resolvePayload(resolvedPromptInput: ResolvedPromptInput,
|
|
|
66
67
|
write: string[];
|
|
67
68
|
unavailable?: string[];
|
|
68
69
|
} | undefined;
|
|
70
|
+
xrepoGrant: string | undefined;
|
|
69
71
|
timeout: string | undefined;
|
|
70
72
|
cwd: string | undefined;
|
|
71
73
|
progressComment: {
|
package/dist/utils/token.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ type ResolveTokensParams = {
|
|
|
29
29
|
push: PushPermission;
|
|
30
30
|
authorPermission: AuthorPermission | undefined;
|
|
31
31
|
xrepo?: XrepoConfig | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* handle to the cross-repo grant the server persisted for this run. required
|
|
34
|
+
* alongside `xrepo`: the mint endpoint refuses a `repos` list without one,
|
|
35
|
+
* because the runner's list is attacker-controlled on a manual dispatch.
|
|
36
|
+
*/
|
|
37
|
+
xrepoGrant?: string | undefined;
|
|
32
38
|
/**
|
|
33
39
|
* OIDC credentials stashed by main.ts before the restricted-mode env wipe —
|
|
34
40
|
* the mid-run MCP token refresh mints from this snapshot (#891). null when
|