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.
- package/dist/cli.mjs +11273 -5812
- package/dist/external.d.ts +7 -0
- package/dist/index.js +10713 -5677
- package/dist/internal/index.d.ts +2 -0
- package/dist/internal.js +267 -36
- package/dist/models.d.ts +2 -1
- package/dist/toolState.d.ts +1 -1
- package/dist/utils/apiKeys.d.ts +30 -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/globals.d.ts +0 -1
- package/dist/utils/learnings.d.ts +1 -5
- package/dist/utils/learningsTruncate.d.ts +2 -2
- 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
|
@@ -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
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// claude-
|
|
358
|
-
//
|
|
359
|
-
// just produces nothing until it is
|
|
360
|
-
// zero-output failures from 2026-08-23.
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1784
|
-
return {
|
|
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
|
package/dist/toolState.d.ts
CHANGED
|
@@ -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
|
|
29
|
+
* harnesses).
|
|
30
30
|
*/
|
|
31
31
|
export type CommentableLines = {
|
|
32
32
|
RIGHT: Set<number>;
|
package/dist/utils/apiKeys.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/utils/globals.d.ts
CHANGED