pullfrog 0.1.12 → 0.1.14
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 +95 -40
- package/dist/index.js +94 -39
- package/dist/utils/gitAuth.d.ts +15 -6
- package/dist/utils/gitAuthServer.d.ts +9 -3
- package/dist/utils/providerErrors.d.ts +18 -0
- package/dist/utils/runErrorRenderer.d.ts +23 -8
- package/dist/utils/runLifecycle.d.ts +7 -0
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -144249,7 +144249,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
144249
144249
|
// package.json
|
|
144250
144250
|
var package_default = {
|
|
144251
144251
|
name: "pullfrog",
|
|
144252
|
-
version: "0.1.
|
|
144252
|
+
version: "0.1.14",
|
|
144253
144253
|
type: "module",
|
|
144254
144254
|
bin: {
|
|
144255
144255
|
pullfrog: "dist/cli.mjs",
|
|
@@ -144831,8 +144831,8 @@ async function $git(subcommand, args2, options) {
|
|
|
144831
144831
|
activityTimeout: 0
|
|
144832
144832
|
});
|
|
144833
144833
|
if (result.stderr.includes("askpass-compromised")) {
|
|
144834
|
-
log.info("askpass code was
|
|
144835
|
-
throw new Error("git auth failed \u2014 askpass code was
|
|
144834
|
+
log.info("askpass code was replayed after revoke \u2014 token has been revoked");
|
|
144835
|
+
throw new Error("git auth failed \u2014 askpass code was replayed after revoke, token revoked");
|
|
144836
144836
|
}
|
|
144837
144837
|
if (result.exitCode !== 0) {
|
|
144838
144838
|
const stderr = result.stderr.trim();
|
|
@@ -144849,6 +144849,7 @@ ${stdout}` : stderr || stdout || "(no output)";
|
|
|
144849
144849
|
stderr: result.stderr.trim()
|
|
144850
144850
|
};
|
|
144851
144851
|
} finally {
|
|
144852
|
+
authServer.revoke(code);
|
|
144852
144853
|
try {
|
|
144853
144854
|
unlinkSync(scriptPath);
|
|
144854
144855
|
} catch {
|
|
@@ -145120,7 +145121,13 @@ var TRANSIENT_PATTERNS = [
|
|
|
145120
145121
|
/HTTP 5\d\d/,
|
|
145121
145122
|
/returned error: 5\d\d/i,
|
|
145122
145123
|
/HTTP 429/,
|
|
145123
|
-
/returned error: 429/i
|
|
145124
|
+
/returned error: 429/i,
|
|
145125
|
+
// github installation tokens can 401 for seconds after minting while
|
|
145126
|
+
// replicating (@octokit/auth-app retries the same class). git push
|
|
145127
|
+
// surfaces it as "Invalid username or token", distinct from 403
|
|
145128
|
+
// permission denied — safe to backoff-retry with the same token.
|
|
145129
|
+
/Invalid username or token/,
|
|
145130
|
+
/Authentication failed for 'https:\/\/github\.com\//
|
|
145124
145131
|
];
|
|
145125
145132
|
function classifyPushError(msg) {
|
|
145126
145133
|
if (CONCURRENT_PUSH_PATTERNS.some((p2) => msg.includes(p2))) return "concurrent-push";
|
|
@@ -149153,19 +149160,22 @@ async function installFromNpmTarball(params) {
|
|
|
149153
149160
|
}
|
|
149154
149161
|
|
|
149155
149162
|
// utils/providerErrors.ts
|
|
149163
|
+
var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
|
|
149156
149164
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
149157
149165
|
var PROVIDER_ERROR_PATTERNS = [
|
|
149158
149166
|
// billing-payload patterns come BEFORE bare status-code patterns. providers
|
|
149159
149167
|
// commonly return 401 / 429 for billing/quota exhaustion (OpenCode Zen
|
|
149160
149168
|
// `CreditsError` / `FreeUsageLimitError`, Gemini `RESOURCE_EXHAUSTED` +
|
|
149161
|
-
// "spending cap", Anthropic "Insufficient balance"
|
|
149162
|
-
// and require user-billing action —
|
|
149163
|
-
// rate-limit. status-code patterns
|
|
149164
|
-
// "auth error (401)" / "rate limited (429)"
|
|
149165
|
-
|
|
149166
|
-
{ regex: /\
|
|
149167
|
-
{ regex:
|
|
149168
|
-
{ regex: /
|
|
149169
|
+
// "spending cap", Anthropic "Insufficient balance" / "credit balance is
|
|
149170
|
+
// too low"). these are non-retryable and require user-billing action —
|
|
149171
|
+
// distinct from a transient auth error or rate-limit. status-code patterns
|
|
149172
|
+
// would otherwise win and surface "auth error (401)" / "rate limited (429)"
|
|
149173
|
+
// with no billing hint. see #778, #835.
|
|
149174
|
+
{ regex: /\bCreditsError\b/, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
149175
|
+
{ regex: /\bFreeUsageLimitError\b/, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
149176
|
+
{ regex: /Insufficient balance/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
149177
|
+
{ regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
149178
|
+
{ regex: /spending cap/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
149169
149179
|
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
149170
149180
|
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
149171
149181
|
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
@@ -149236,6 +149246,13 @@ var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_token
|
|
|
149236
149246
|
function isRouterKeylimitExhaustedError(text) {
|
|
149237
149247
|
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
149238
149248
|
}
|
|
149249
|
+
function isProviderBillingExhausted(text) {
|
|
149250
|
+
return findProviderErrorMatch(text)?.label === PROVIDER_BILLING_EXHAUSTED_LABEL;
|
|
149251
|
+
}
|
|
149252
|
+
function extractProviderId(text) {
|
|
149253
|
+
const match3 = text.match(/\bproviderID=([a-z0-9_-]+)/i);
|
|
149254
|
+
return match3 ? match3[1].toLowerCase() : null;
|
|
149255
|
+
}
|
|
149239
149256
|
|
|
149240
149257
|
// utils/skills.ts
|
|
149241
149258
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -151445,8 +151462,7 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
151445
151462
|
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
151446
151463
|
import { createServer as createServer2 } from "node:http";
|
|
151447
151464
|
import { join as join16 } from "node:path";
|
|
151448
|
-
var
|
|
151449
|
-
var TAMPER_WINDOW_MS = 6e4;
|
|
151465
|
+
var REVOKED_TRAP_MS = 6e4;
|
|
151450
151466
|
function revokeGitHubToken(token) {
|
|
151451
151467
|
fetch("https://api.github.com/installation/token", {
|
|
151452
151468
|
method: "DELETE",
|
|
@@ -151477,18 +151493,14 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151477
151493
|
res.writeHead(404).end();
|
|
151478
151494
|
return;
|
|
151479
151495
|
}
|
|
151480
|
-
if (entry.state === "
|
|
151481
|
-
entry.state = "consumed";
|
|
151482
|
-
clearTimeout(entry.timeout);
|
|
151483
|
-
entry.timeout = setTimeout(() => codes.delete(code), TAMPER_WINDOW_MS);
|
|
151484
|
-
entry.timeout.unref();
|
|
151496
|
+
if (entry.state === "active") {
|
|
151485
151497
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
151486
151498
|
res.end(entry.token);
|
|
151487
151499
|
return;
|
|
151488
151500
|
}
|
|
151489
|
-
log.info("askpass code used
|
|
151501
|
+
log.info("askpass code used after revoke \u2014 revoking token");
|
|
151490
151502
|
revokeGitHubToken(entry.token);
|
|
151491
|
-
clearTimeout(entry.timeout);
|
|
151503
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
151492
151504
|
codes.delete(code);
|
|
151493
151505
|
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
151494
151506
|
res.end("compromised");
|
|
@@ -151505,14 +151517,16 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151505
151517
|
log.debug(`git auth server listening on 127.0.0.1:${port}`);
|
|
151506
151518
|
function register4(token) {
|
|
151507
151519
|
const code = randomUUID4();
|
|
151508
|
-
|
|
151509
|
-
codes.delete(code);
|
|
151510
|
-
log.debug(`git auth code expired: ${code.slice(0, 8)}...`);
|
|
151511
|
-
}, CODE_TTL_MS);
|
|
151512
|
-
timeout.unref();
|
|
151513
|
-
codes.set(code, { token, state: "pending", timeout });
|
|
151520
|
+
codes.set(code, { token, state: "active" });
|
|
151514
151521
|
return code;
|
|
151515
151522
|
}
|
|
151523
|
+
function revoke(code) {
|
|
151524
|
+
const entry = codes.get(code);
|
|
151525
|
+
if (!entry) return;
|
|
151526
|
+
entry.state = "revoked";
|
|
151527
|
+
entry.timeout = setTimeout(() => codes.delete(code), REVOKED_TRAP_MS);
|
|
151528
|
+
entry.timeout.unref();
|
|
151529
|
+
}
|
|
151516
151530
|
function writeAskpassScript(code) {
|
|
151517
151531
|
const scriptId = randomUUID4();
|
|
151518
151532
|
const scriptName = `askpass-${scriptId}.js`;
|
|
@@ -151526,17 +151540,15 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151526
151540
|
`if(r.statusCode===409){process.stderr.write("askpass-compromised\\n");process.exit(1)}`,
|
|
151527
151541
|
`if(r.statusCode!==200){process.exit(1)}`,
|
|
151528
151542
|
`var d="";r.on("data",function(c){d+=c});`,
|
|
151529
|
-
`r.on("end",function(){`,
|
|
151530
|
-
`
|
|
151531
|
-
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
151532
|
-
`})}).on("error",function(){process.exit(1)})}`
|
|
151543
|
+
`r.on("end",function(){process.stdout.write(d+"\\n")})`,
|
|
151544
|
+
`}).on("error",function(){process.exit(1)})}`
|
|
151533
151545
|
].join("\n");
|
|
151534
151546
|
writeFileSync12(scriptPath, content, { mode: 448 });
|
|
151535
151547
|
return scriptPath;
|
|
151536
151548
|
}
|
|
151537
151549
|
async function close() {
|
|
151538
151550
|
for (const entry of codes.values()) {
|
|
151539
|
-
clearTimeout(entry.timeout);
|
|
151551
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
151540
151552
|
}
|
|
151541
151553
|
codes.clear();
|
|
151542
151554
|
await new Promise((resolve3) => server.close(() => resolve3()));
|
|
@@ -151545,6 +151557,7 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151545
151557
|
return {
|
|
151546
151558
|
port,
|
|
151547
151559
|
register: register4,
|
|
151560
|
+
revoke,
|
|
151548
151561
|
writeAskpassScript,
|
|
151549
151562
|
close,
|
|
151550
151563
|
[Symbol.asyncDispose]: close
|
|
@@ -156717,6 +156730,36 @@ async function resolveRunContextData(params) {
|
|
|
156717
156730
|
function isProviderModelNotFoundError(message) {
|
|
156718
156731
|
return message.includes("ProviderModelNotFoundError");
|
|
156719
156732
|
}
|
|
156733
|
+
var PROVIDER_BILLING_URLS = {
|
|
156734
|
+
deepseek: "https://platform.deepseek.com/top_up",
|
|
156735
|
+
anthropic: "https://console.anthropic.com/settings/billing",
|
|
156736
|
+
openai: "https://platform.openai.com/account/billing",
|
|
156737
|
+
google: "https://aistudio.google.com/usage",
|
|
156738
|
+
opencode: "https://opencode.ai/zen"
|
|
156739
|
+
};
|
|
156740
|
+
function detectProviderId(message) {
|
|
156741
|
+
const harnessId = extractProviderId(message);
|
|
156742
|
+
if (harnessId) return harnessId;
|
|
156743
|
+
if (/credit balance is too low/i.test(message)) return "anthropic";
|
|
156744
|
+
return null;
|
|
156745
|
+
}
|
|
156746
|
+
function formatProviderBillingExhausted(input) {
|
|
156747
|
+
const providerId = detectProviderId(input.errorMessage);
|
|
156748
|
+
const dashboardUrl = providerId ? PROVIDER_BILLING_URLS[providerId] : void 0;
|
|
156749
|
+
const headline = providerId ? `**Your \`${providerId}\` account is out of credit.**` : "**Your provider account is out of credit.**";
|
|
156750
|
+
const cta = dashboardUrl ? `[Top up \`${providerId}\` \u2192](${dashboardUrl})` : "Top up your provider account, then re-trigger Pullfrog.";
|
|
156751
|
+
return [
|
|
156752
|
+
headline,
|
|
156753
|
+
"",
|
|
156754
|
+
"Pullfrog detected a billing-exhausted response from your provider \u2014 the agent stopped before completing this run.",
|
|
156755
|
+
"",
|
|
156756
|
+
cta,
|
|
156757
|
+
"",
|
|
156758
|
+
`\`\`\`
|
|
156759
|
+
${input.errorMessage}
|
|
156760
|
+
\`\`\``
|
|
156761
|
+
].join("\n");
|
|
156762
|
+
}
|
|
156720
156763
|
function formatProviderModelNotFoundSummary(input) {
|
|
156721
156764
|
return `Pullfrog's free fallback model is no longer available in OpenCode's catalog. Add an API key for your configured model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
|
|
156722
156765
|
|
|
@@ -156736,6 +156779,12 @@ function renderRunError(input) {
|
|
|
156736
156779
|
isHang: true,
|
|
156737
156780
|
errorMessage: input.errorMessage
|
|
156738
156781
|
}) : null;
|
|
156782
|
+
if (isProviderBillingExhausted(input.errorMessage)) {
|
|
156783
|
+
const body = formatProviderBillingExhausted({ errorMessage: input.errorMessage });
|
|
156784
|
+
return { summary: `### \u274C Pullfrog failed
|
|
156785
|
+
|
|
156786
|
+
${body}`, comment: body };
|
|
156787
|
+
}
|
|
156739
156788
|
const apiKeySource = hangBody ?? input.errorMessage;
|
|
156740
156789
|
const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
|
|
156741
156790
|
owner: input.repo.owner,
|
|
@@ -156859,12 +156908,14 @@ async function finalizeSuccessRun(input) {
|
|
|
156859
156908
|
repo: input.repo,
|
|
156860
156909
|
agentDiagnostic: input.toolState.agentDiagnostic
|
|
156861
156910
|
}) : null;
|
|
156862
|
-
if (rendered
|
|
156863
|
-
await reportErrorToComment({
|
|
156864
|
-
|
|
156865
|
-
|
|
156866
|
-
|
|
156867
|
-
)
|
|
156911
|
+
if (rendered) {
|
|
156912
|
+
await reportErrorToComment({
|
|
156913
|
+
toolState: input.toolState,
|
|
156914
|
+
error: rendered.comment,
|
|
156915
|
+
createIfMissing: true
|
|
156916
|
+
}).catch((error49) => {
|
|
156917
|
+
log.debug(`failure error report failed: ${error49}`);
|
|
156918
|
+
});
|
|
156868
156919
|
}
|
|
156869
156920
|
if (input.result.success && input.toolState.progressComment && !input.toolState.finalSummaryWritten) {
|
|
156870
156921
|
await deleteProgressComment(input.toolContext).catch((error49) => {
|
|
@@ -156896,7 +156947,11 @@ async function writeRunErrorOutputs(input) {
|
|
|
156896
156947
|
} catch {
|
|
156897
156948
|
}
|
|
156898
156949
|
try {
|
|
156899
|
-
await reportErrorToComment({
|
|
156950
|
+
await reportErrorToComment({
|
|
156951
|
+
toolState: input.toolState,
|
|
156952
|
+
error: input.rendered.comment,
|
|
156953
|
+
createIfMissing: true
|
|
156954
|
+
});
|
|
156900
156955
|
} catch {
|
|
156901
156956
|
}
|
|
156902
156957
|
}
|
|
@@ -158384,7 +158439,7 @@ async function run2() {
|
|
|
158384
158439
|
}
|
|
158385
158440
|
|
|
158386
158441
|
// cli.ts
|
|
158387
|
-
var VERSION10 = "0.1.
|
|
158442
|
+
var VERSION10 = "0.1.14";
|
|
158388
158443
|
var bin = basename2(process.argv[1] || "");
|
|
158389
158444
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
158390
158445
|
var rawArgs = process.argv.slice(2);
|
package/dist/index.js
CHANGED
|
@@ -142509,7 +142509,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142509
142509
|
// package.json
|
|
142510
142510
|
var package_default = {
|
|
142511
142511
|
name: "pullfrog",
|
|
142512
|
-
version: "0.1.
|
|
142512
|
+
version: "0.1.14",
|
|
142513
142513
|
type: "module",
|
|
142514
142514
|
bin: {
|
|
142515
142515
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143091,8 +143091,8 @@ async function $git(subcommand, args2, options) {
|
|
|
143091
143091
|
activityTimeout: 0
|
|
143092
143092
|
});
|
|
143093
143093
|
if (result.stderr.includes("askpass-compromised")) {
|
|
143094
|
-
log.info("askpass code was
|
|
143095
|
-
throw new Error("git auth failed \u2014 askpass code was
|
|
143094
|
+
log.info("askpass code was replayed after revoke \u2014 token has been revoked");
|
|
143095
|
+
throw new Error("git auth failed \u2014 askpass code was replayed after revoke, token revoked");
|
|
143096
143096
|
}
|
|
143097
143097
|
if (result.exitCode !== 0) {
|
|
143098
143098
|
const stderr = result.stderr.trim();
|
|
@@ -143109,6 +143109,7 @@ ${stdout}` : stderr || stdout || "(no output)";
|
|
|
143109
143109
|
stderr: result.stderr.trim()
|
|
143110
143110
|
};
|
|
143111
143111
|
} finally {
|
|
143112
|
+
authServer.revoke(code);
|
|
143112
143113
|
try {
|
|
143113
143114
|
unlinkSync(scriptPath);
|
|
143114
143115
|
} catch {
|
|
@@ -143380,7 +143381,13 @@ var TRANSIENT_PATTERNS = [
|
|
|
143380
143381
|
/HTTP 5\d\d/,
|
|
143381
143382
|
/returned error: 5\d\d/i,
|
|
143382
143383
|
/HTTP 429/,
|
|
143383
|
-
/returned error: 429/i
|
|
143384
|
+
/returned error: 429/i,
|
|
143385
|
+
// github installation tokens can 401 for seconds after minting while
|
|
143386
|
+
// replicating (@octokit/auth-app retries the same class). git push
|
|
143387
|
+
// surfaces it as "Invalid username or token", distinct from 403
|
|
143388
|
+
// permission denied — safe to backoff-retry with the same token.
|
|
143389
|
+
/Invalid username or token/,
|
|
143390
|
+
/Authentication failed for 'https:\/\/github\.com\//
|
|
143384
143391
|
];
|
|
143385
143392
|
function classifyPushError(msg) {
|
|
143386
143393
|
if (CONCURRENT_PUSH_PATTERNS.some((p) => msg.includes(p))) return "concurrent-push";
|
|
@@ -147413,19 +147420,22 @@ async function installFromNpmTarball(params) {
|
|
|
147413
147420
|
}
|
|
147414
147421
|
|
|
147415
147422
|
// utils/providerErrors.ts
|
|
147423
|
+
var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
|
|
147416
147424
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
147417
147425
|
var PROVIDER_ERROR_PATTERNS = [
|
|
147418
147426
|
// billing-payload patterns come BEFORE bare status-code patterns. providers
|
|
147419
147427
|
// commonly return 401 / 429 for billing/quota exhaustion (OpenCode Zen
|
|
147420
147428
|
// `CreditsError` / `FreeUsageLimitError`, Gemini `RESOURCE_EXHAUSTED` +
|
|
147421
|
-
// "spending cap", Anthropic "Insufficient balance"
|
|
147422
|
-
// and require user-billing action —
|
|
147423
|
-
// rate-limit. status-code patterns
|
|
147424
|
-
// "auth error (401)" / "rate limited (429)"
|
|
147425
|
-
|
|
147426
|
-
{ regex: /\
|
|
147427
|
-
{ regex:
|
|
147428
|
-
{ regex: /
|
|
147429
|
+
// "spending cap", Anthropic "Insufficient balance" / "credit balance is
|
|
147430
|
+
// too low"). these are non-retryable and require user-billing action —
|
|
147431
|
+
// distinct from a transient auth error or rate-limit. status-code patterns
|
|
147432
|
+
// would otherwise win and surface "auth error (401)" / "rate limited (429)"
|
|
147433
|
+
// with no billing hint. see #778, #835.
|
|
147434
|
+
{ regex: /\bCreditsError\b/, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
147435
|
+
{ regex: /\bFreeUsageLimitError\b/, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
147436
|
+
{ regex: /Insufficient balance/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
147437
|
+
{ regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
147438
|
+
{ regex: /spending cap/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
147429
147439
|
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
147430
147440
|
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
147431
147441
|
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
@@ -147496,6 +147506,13 @@ var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_token
|
|
|
147496
147506
|
function isRouterKeylimitExhaustedError(text) {
|
|
147497
147507
|
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
147498
147508
|
}
|
|
147509
|
+
function isProviderBillingExhausted(text) {
|
|
147510
|
+
return findProviderErrorMatch(text)?.label === PROVIDER_BILLING_EXHAUSTED_LABEL;
|
|
147511
|
+
}
|
|
147512
|
+
function extractProviderId(text) {
|
|
147513
|
+
const match3 = text.match(/\bproviderID=([a-z0-9_-]+)/i);
|
|
147514
|
+
return match3 ? match3[1].toLowerCase() : null;
|
|
147515
|
+
}
|
|
147499
147516
|
|
|
147500
147517
|
// utils/skills.ts
|
|
147501
147518
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -149705,8 +149722,7 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
149705
149722
|
import { writeFileSync as writeFileSync11 } from "node:fs";
|
|
149706
149723
|
import { createServer as createServer2 } from "node:http";
|
|
149707
149724
|
import { join as join15 } from "node:path";
|
|
149708
|
-
var
|
|
149709
|
-
var TAMPER_WINDOW_MS = 6e4;
|
|
149725
|
+
var REVOKED_TRAP_MS = 6e4;
|
|
149710
149726
|
function revokeGitHubToken(token) {
|
|
149711
149727
|
fetch("https://api.github.com/installation/token", {
|
|
149712
149728
|
method: "DELETE",
|
|
@@ -149737,18 +149753,14 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149737
149753
|
res.writeHead(404).end();
|
|
149738
149754
|
return;
|
|
149739
149755
|
}
|
|
149740
|
-
if (entry.state === "
|
|
149741
|
-
entry.state = "consumed";
|
|
149742
|
-
clearTimeout(entry.timeout);
|
|
149743
|
-
entry.timeout = setTimeout(() => codes.delete(code), TAMPER_WINDOW_MS);
|
|
149744
|
-
entry.timeout.unref();
|
|
149756
|
+
if (entry.state === "active") {
|
|
149745
149757
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
149746
149758
|
res.end(entry.token);
|
|
149747
149759
|
return;
|
|
149748
149760
|
}
|
|
149749
|
-
log.info("askpass code used
|
|
149761
|
+
log.info("askpass code used after revoke \u2014 revoking token");
|
|
149750
149762
|
revokeGitHubToken(entry.token);
|
|
149751
|
-
clearTimeout(entry.timeout);
|
|
149763
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
149752
149764
|
codes.delete(code);
|
|
149753
149765
|
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
149754
149766
|
res.end("compromised");
|
|
@@ -149765,14 +149777,16 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149765
149777
|
log.debug(`git auth server listening on 127.0.0.1:${port}`);
|
|
149766
149778
|
function register4(token) {
|
|
149767
149779
|
const code = randomUUID4();
|
|
149768
|
-
|
|
149769
|
-
codes.delete(code);
|
|
149770
|
-
log.debug(`git auth code expired: ${code.slice(0, 8)}...`);
|
|
149771
|
-
}, CODE_TTL_MS);
|
|
149772
|
-
timeout.unref();
|
|
149773
|
-
codes.set(code, { token, state: "pending", timeout });
|
|
149780
|
+
codes.set(code, { token, state: "active" });
|
|
149774
149781
|
return code;
|
|
149775
149782
|
}
|
|
149783
|
+
function revoke(code) {
|
|
149784
|
+
const entry = codes.get(code);
|
|
149785
|
+
if (!entry) return;
|
|
149786
|
+
entry.state = "revoked";
|
|
149787
|
+
entry.timeout = setTimeout(() => codes.delete(code), REVOKED_TRAP_MS);
|
|
149788
|
+
entry.timeout.unref();
|
|
149789
|
+
}
|
|
149776
149790
|
function writeAskpassScript(code) {
|
|
149777
149791
|
const scriptId = randomUUID4();
|
|
149778
149792
|
const scriptName = `askpass-${scriptId}.js`;
|
|
@@ -149786,17 +149800,15 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149786
149800
|
`if(r.statusCode===409){process.stderr.write("askpass-compromised\\n");process.exit(1)}`,
|
|
149787
149801
|
`if(r.statusCode!==200){process.exit(1)}`,
|
|
149788
149802
|
`var d="";r.on("data",function(c){d+=c});`,
|
|
149789
|
-
`r.on("end",function(){`,
|
|
149790
|
-
`
|
|
149791
|
-
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
149792
|
-
`})}).on("error",function(){process.exit(1)})}`
|
|
149803
|
+
`r.on("end",function(){process.stdout.write(d+"\\n")})`,
|
|
149804
|
+
`}).on("error",function(){process.exit(1)})}`
|
|
149793
149805
|
].join("\n");
|
|
149794
149806
|
writeFileSync11(scriptPath, content, { mode: 448 });
|
|
149795
149807
|
return scriptPath;
|
|
149796
149808
|
}
|
|
149797
149809
|
async function close() {
|
|
149798
149810
|
for (const entry of codes.values()) {
|
|
149799
|
-
clearTimeout(entry.timeout);
|
|
149811
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
149800
149812
|
}
|
|
149801
149813
|
codes.clear();
|
|
149802
149814
|
await new Promise((resolve3) => server.close(() => resolve3()));
|
|
@@ -149805,6 +149817,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149805
149817
|
return {
|
|
149806
149818
|
port,
|
|
149807
149819
|
register: register4,
|
|
149820
|
+
revoke,
|
|
149808
149821
|
writeAskpassScript,
|
|
149809
149822
|
close,
|
|
149810
149823
|
[Symbol.asyncDispose]: close
|
|
@@ -154977,6 +154990,36 @@ async function resolveRunContextData(params) {
|
|
|
154977
154990
|
function isProviderModelNotFoundError(message) {
|
|
154978
154991
|
return message.includes("ProviderModelNotFoundError");
|
|
154979
154992
|
}
|
|
154993
|
+
var PROVIDER_BILLING_URLS = {
|
|
154994
|
+
deepseek: "https://platform.deepseek.com/top_up",
|
|
154995
|
+
anthropic: "https://console.anthropic.com/settings/billing",
|
|
154996
|
+
openai: "https://platform.openai.com/account/billing",
|
|
154997
|
+
google: "https://aistudio.google.com/usage",
|
|
154998
|
+
opencode: "https://opencode.ai/zen"
|
|
154999
|
+
};
|
|
155000
|
+
function detectProviderId(message) {
|
|
155001
|
+
const harnessId = extractProviderId(message);
|
|
155002
|
+
if (harnessId) return harnessId;
|
|
155003
|
+
if (/credit balance is too low/i.test(message)) return "anthropic";
|
|
155004
|
+
return null;
|
|
155005
|
+
}
|
|
155006
|
+
function formatProviderBillingExhausted(input) {
|
|
155007
|
+
const providerId = detectProviderId(input.errorMessage);
|
|
155008
|
+
const dashboardUrl = providerId ? PROVIDER_BILLING_URLS[providerId] : void 0;
|
|
155009
|
+
const headline = providerId ? `**Your \`${providerId}\` account is out of credit.**` : "**Your provider account is out of credit.**";
|
|
155010
|
+
const cta = dashboardUrl ? `[Top up \`${providerId}\` \u2192](${dashboardUrl})` : "Top up your provider account, then re-trigger Pullfrog.";
|
|
155011
|
+
return [
|
|
155012
|
+
headline,
|
|
155013
|
+
"",
|
|
155014
|
+
"Pullfrog detected a billing-exhausted response from your provider \u2014 the agent stopped before completing this run.",
|
|
155015
|
+
"",
|
|
155016
|
+
cta,
|
|
155017
|
+
"",
|
|
155018
|
+
`\`\`\`
|
|
155019
|
+
${input.errorMessage}
|
|
155020
|
+
\`\`\``
|
|
155021
|
+
].join("\n");
|
|
155022
|
+
}
|
|
154980
155023
|
function formatProviderModelNotFoundSummary(input) {
|
|
154981
155024
|
return `Pullfrog's free fallback model is no longer available in OpenCode's catalog. Add an API key for your configured model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
|
|
154982
155025
|
|
|
@@ -154996,6 +155039,12 @@ function renderRunError(input) {
|
|
|
154996
155039
|
isHang: true,
|
|
154997
155040
|
errorMessage: input.errorMessage
|
|
154998
155041
|
}) : null;
|
|
155042
|
+
if (isProviderBillingExhausted(input.errorMessage)) {
|
|
155043
|
+
const body = formatProviderBillingExhausted({ errorMessage: input.errorMessage });
|
|
155044
|
+
return { summary: `### \u274C Pullfrog failed
|
|
155045
|
+
|
|
155046
|
+
${body}`, comment: body };
|
|
155047
|
+
}
|
|
154999
155048
|
const apiKeySource = hangBody ?? input.errorMessage;
|
|
155000
155049
|
const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
|
|
155001
155050
|
owner: input.repo.owner,
|
|
@@ -155119,12 +155168,14 @@ async function finalizeSuccessRun(input) {
|
|
|
155119
155168
|
repo: input.repo,
|
|
155120
155169
|
agentDiagnostic: input.toolState.agentDiagnostic
|
|
155121
155170
|
}) : null;
|
|
155122
|
-
if (rendered
|
|
155123
|
-
await reportErrorToComment({
|
|
155124
|
-
|
|
155125
|
-
|
|
155126
|
-
|
|
155127
|
-
)
|
|
155171
|
+
if (rendered) {
|
|
155172
|
+
await reportErrorToComment({
|
|
155173
|
+
toolState: input.toolState,
|
|
155174
|
+
error: rendered.comment,
|
|
155175
|
+
createIfMissing: true
|
|
155176
|
+
}).catch((error49) => {
|
|
155177
|
+
log.debug(`failure error report failed: ${error49}`);
|
|
155178
|
+
});
|
|
155128
155179
|
}
|
|
155129
155180
|
if (input.result.success && input.toolState.progressComment && !input.toolState.finalSummaryWritten) {
|
|
155130
155181
|
await deleteProgressComment(input.toolContext).catch((error49) => {
|
|
@@ -155156,7 +155207,11 @@ async function writeRunErrorOutputs(input) {
|
|
|
155156
155207
|
} catch {
|
|
155157
155208
|
}
|
|
155158
155209
|
try {
|
|
155159
|
-
await reportErrorToComment({
|
|
155210
|
+
await reportErrorToComment({
|
|
155211
|
+
toolState: input.toolState,
|
|
155212
|
+
error: input.rendered.comment,
|
|
155213
|
+
createIfMissing: true
|
|
155214
|
+
});
|
|
155160
155215
|
} catch {
|
|
155161
155216
|
}
|
|
155162
155217
|
}
|
package/dist/utils/gitAuth.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* git authentication via GIT_ASKPASS.
|
|
3
3
|
*
|
|
4
|
-
* a localhost HTTP server serves tokens via
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* a localhost HTTP server serves tokens via UUID codes whose lifetime is
|
|
5
|
+
* bounded by the parent $git() invocation: register() makes the code active,
|
|
6
|
+
* the script (and any sibling subprocess — e.g. git-lfs pre-push) can fetch
|
|
7
|
+
* the token any number of times, and $git()'s finally calls revoke() to
|
|
8
|
+
* close the window. each $git() call writes a unique askpass script with
|
|
9
|
+
* the server port+code baked into the file body — no secrets in subprocess
|
|
10
|
+
* env. a replay of a revoked code trips a 409 and revokes the underlying
|
|
11
|
+
* github installation token.
|
|
7
12
|
*
|
|
8
13
|
* see wiki/askpass.md for full security documentation.
|
|
9
14
|
*/
|
|
@@ -35,9 +40,13 @@ export declare function setGitAuthServer(server: GitAuthServer): void;
|
|
|
35
40
|
* a remote and need credentials. working-tree operations (checkout, merge)
|
|
36
41
|
* use $() from shell.ts which has no token.
|
|
37
42
|
*
|
|
38
|
-
* per call: registers a
|
|
39
|
-
* unique askpass script with port+code baked
|
|
40
|
-
*
|
|
43
|
+
* per call: registers a code with the auth server (valid for the lifetime
|
|
44
|
+
* of this invocation), writes a unique askpass script with port+code baked
|
|
45
|
+
* in, spawns git with GIT_ASKPASS pointing to the script. on completion,
|
|
46
|
+
* revokes the code and deletes the script in finally. multiple sibling
|
|
47
|
+
* askpass calls within one invocation (e.g. git itself + git-lfs pre-push)
|
|
48
|
+
* all see a valid code; replay attempts after finally trip a 409 and the
|
|
49
|
+
* server revokes the underlying github token as a tamper signal.
|
|
41
50
|
*
|
|
42
51
|
* @example
|
|
43
52
|
* await $git("fetch", ["origin", "main"], { token });
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ASKPASS-based git authentication server.
|
|
3
3
|
*
|
|
4
|
-
* serves tokens via a localhost HTTP server with
|
|
4
|
+
* serves tokens via a localhost HTTP server with per-$git()-call UUID codes.
|
|
5
5
|
* each $git() call gets a unique askpass script with the port+code baked in.
|
|
6
6
|
* the token never appears in subprocess env — only the script file path.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* lifetime: the code is valid for as long as the $git() invocation is
|
|
9
|
+
* running. multiple askpass calls within one invocation (e.g. git's own
|
|
10
|
+
* fetch/push + a git-lfs pre-push hook that also authenticates) all
|
|
11
|
+
* succeed. $git() calls revoke(code) in finally; subsequent requests for
|
|
12
|
+
* a revoked code trigger immediate token revocation via the GitHub API
|
|
13
|
+
* as a tamper-evidence precaution (an agent replaying the code after the
|
|
14
|
+
* legitimate window has closed is the realistic attack we still catch).
|
|
10
15
|
*/
|
|
11
16
|
export type GitAuthServer = {
|
|
12
17
|
port: number;
|
|
13
18
|
register: (token: string) => string;
|
|
19
|
+
revoke: (code: string) => void;
|
|
14
20
|
writeAskpassScript: (code: string) => string;
|
|
15
21
|
close: () => Promise<void>;
|
|
16
22
|
[Symbol.asyncDispose]: () => Promise<void>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Stable label for the BYOK provider-billing-exhausted classification. */
|
|
2
|
+
export declare const PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
|
|
1
3
|
/**
|
|
2
4
|
* Result of a provider-error scan: the classification label plus a
|
|
3
5
|
* human-readable excerpt centered on the matched line. The excerpt is what
|
|
@@ -11,3 +13,19 @@ export type ProviderErrorMatch = {
|
|
|
11
13
|
export declare function findProviderErrorMatch(text: string): ProviderErrorMatch | null;
|
|
12
14
|
export declare function detectProviderError(text: string): string | null;
|
|
13
15
|
export declare function isRouterKeylimitExhaustedError(text: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* BYOK billing-exhausted: provider rejected the request because the user's
|
|
18
|
+
* provider wallet is empty (DeepSeek "Insufficient Balance", Anthropic
|
|
19
|
+
* "credit balance is too low", OpenCode Zen `CreditsError` /
|
|
20
|
+
* `FreeUsageLimitError`, Gemini "spending cap"). Distinct from
|
|
21
|
+
* `isRouterKeylimitExhaustedError` — that's Pullfrog's Router wallet, this
|
|
22
|
+
* is the user's own provider account.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isProviderBillingExhausted(text: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Extract `providerID=foo` from agent error logs (OpenCode emits this on
|
|
27
|
+
* `provider error detected (...)` lines). Returns the lowercase provider
|
|
28
|
+
* slug, or null when absent. Used to render a provider-specific dashboard
|
|
29
|
+
* link in the BYOK billing-exhausted summary.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractProviderId(text: string): string | null;
|
|
@@ -3,22 +3,37 @@
|
|
|
3
3
|
* pair of user-facing markdown bodies — one for the GitHub Actions job
|
|
4
4
|
* summary tab, one for the PR progress comment.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Classifications, in dispatch order (first match wins; the api-key
|
|
7
|
+
* branch additionally folds in the activity-timeout hang body as a
|
|
8
|
+
* sub-source so a hang masking an api-key error still surfaces the api-key
|
|
9
|
+
* CTA):
|
|
7
10
|
*
|
|
8
11
|
* 1. `BillingError` — either the proxy-token mint already threw one (402
|
|
9
12
|
* handled inline) or the agent runtime surfaced an OpenRouter
|
|
10
13
|
* "key budget exhausted" string mid-run. Both render via
|
|
11
14
|
* `formatBillingErrorSummary` so the user sees actionable copy.
|
|
12
15
|
*
|
|
13
|
-
* 2.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* 2. BYOK provider billing-exhausted (#835) — DeepSeek "Insufficient
|
|
17
|
+
* Balance", Anthropic "credit balance is too low", OpenCode Zen
|
|
18
|
+
* `CreditsError`, Gemini "spending cap". Checked before api-key auth
|
|
19
|
+
* because billing-exhausted responses often carry 401 status codes
|
|
20
|
+
* that `isApiKeyAuthError` would otherwise mis-classify.
|
|
17
21
|
*
|
|
18
|
-
* 3. API-key auth error — `isApiKeyAuthError` sniffs the raw error string
|
|
19
|
-
*
|
|
22
|
+
* 3. API-key auth error — `isApiKeyAuthError` sniffs the raw error string
|
|
23
|
+
* (or the activity-timeout hang body when present, since that's where
|
|
24
|
+
* the underlying provider error often lands); `formatApiKeyErrorSummary`
|
|
25
|
+
* renders provider + console-link copy.
|
|
20
26
|
*
|
|
21
|
-
* 4.
|
|
27
|
+
* 4. ProviderModelNotFoundError — stale free-fallback model id no longer
|
|
28
|
+
* in the OpenCode catalog; renders a nudge to add a BYOK key.
|
|
29
|
+
*
|
|
30
|
+
* 5. Activity-timeout hang — `errorMessage` starts with
|
|
31
|
+
* `"activity timeout"` or `"agent still pending"` AND none of the
|
|
32
|
+
* above matched. The harness keeps structured diagnostic state on
|
|
33
|
+
* `toolState.agentDiagnostic`; `formatAgentHangBody` renders that as
|
|
34
|
+
* a markdown block.
|
|
35
|
+
*
|
|
36
|
+
* 6. Default — a generic `❌ Pullfrog failed` block with the raw error
|
|
22
37
|
* message in a fenced code block. Same body for both surfaces.
|
|
23
38
|
*
|
|
24
39
|
* The hang body and the API-key body diverge between the two surfaces only
|
|
@@ -71,6 +71,13 @@ export declare function finalizeSuccessRun(input: {
|
|
|
71
71
|
*
|
|
72
72
|
* `lastProgressBody` and the usage table are appended to the summary so the
|
|
73
73
|
* partial work the agent did before failing isn't lost.
|
|
74
|
+
*
|
|
75
|
+
* `createIfMissing: true` is symmetric with `finalizeSuccessRun` — silent
|
|
76
|
+
* triggers (IncrementalReview / pull_request_synchronize / auto-label) that
|
|
77
|
+
* throw past `finalizeSuccessRun` (e.g. timeout race kills the agent
|
|
78
|
+
* mid-billing-exhausted-retry) reach this catch path with no progress
|
|
79
|
+
* comment to update, and without `createIfMissing` the terminal error
|
|
80
|
+
* lands only in the GH job summary that most users never open. see #835.
|
|
74
81
|
*/
|
|
75
82
|
export declare function writeRunErrorOutputs(input: {
|
|
76
83
|
rendered: RenderedRunError;
|