pullfrog 0.1.13 → 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 +88 -39
- package/dist/index.js +87 -38
- 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 {
|
|
@@ -149159,19 +149160,22 @@ async function installFromNpmTarball(params) {
|
|
|
149159
149160
|
}
|
|
149160
149161
|
|
|
149161
149162
|
// utils/providerErrors.ts
|
|
149163
|
+
var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
|
|
149162
149164
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
149163
149165
|
var PROVIDER_ERROR_PATTERNS = [
|
|
149164
149166
|
// billing-payload patterns come BEFORE bare status-code patterns. providers
|
|
149165
149167
|
// commonly return 401 / 429 for billing/quota exhaustion (OpenCode Zen
|
|
149166
149168
|
// `CreditsError` / `FreeUsageLimitError`, Gemini `RESOURCE_EXHAUSTED` +
|
|
149167
|
-
// "spending cap", Anthropic "Insufficient balance"
|
|
149168
|
-
// and require user-billing action —
|
|
149169
|
-
// rate-limit. status-code patterns
|
|
149170
|
-
// "auth error (401)" / "rate limited (429)"
|
|
149171
|
-
|
|
149172
|
-
{ regex: /\
|
|
149173
|
-
{ regex:
|
|
149174
|
-
{ 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 },
|
|
149175
149179
|
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
149176
149180
|
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
149177
149181
|
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
@@ -149242,6 +149246,13 @@ var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_token
|
|
|
149242
149246
|
function isRouterKeylimitExhaustedError(text) {
|
|
149243
149247
|
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
149244
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
|
+
}
|
|
149245
149256
|
|
|
149246
149257
|
// utils/skills.ts
|
|
149247
149258
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -151451,8 +151462,7 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
151451
151462
|
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
151452
151463
|
import { createServer as createServer2 } from "node:http";
|
|
151453
151464
|
import { join as join16 } from "node:path";
|
|
151454
|
-
var
|
|
151455
|
-
var TAMPER_WINDOW_MS = 6e4;
|
|
151465
|
+
var REVOKED_TRAP_MS = 6e4;
|
|
151456
151466
|
function revokeGitHubToken(token) {
|
|
151457
151467
|
fetch("https://api.github.com/installation/token", {
|
|
151458
151468
|
method: "DELETE",
|
|
@@ -151483,18 +151493,14 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151483
151493
|
res.writeHead(404).end();
|
|
151484
151494
|
return;
|
|
151485
151495
|
}
|
|
151486
|
-
if (entry.state === "
|
|
151487
|
-
entry.state = "consumed";
|
|
151488
|
-
clearTimeout(entry.timeout);
|
|
151489
|
-
entry.timeout = setTimeout(() => codes.delete(code), TAMPER_WINDOW_MS);
|
|
151490
|
-
entry.timeout.unref();
|
|
151496
|
+
if (entry.state === "active") {
|
|
151491
151497
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
151492
151498
|
res.end(entry.token);
|
|
151493
151499
|
return;
|
|
151494
151500
|
}
|
|
151495
|
-
log.info("askpass code used
|
|
151501
|
+
log.info("askpass code used after revoke \u2014 revoking token");
|
|
151496
151502
|
revokeGitHubToken(entry.token);
|
|
151497
|
-
clearTimeout(entry.timeout);
|
|
151503
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
151498
151504
|
codes.delete(code);
|
|
151499
151505
|
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
151500
151506
|
res.end("compromised");
|
|
@@ -151511,14 +151517,16 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151511
151517
|
log.debug(`git auth server listening on 127.0.0.1:${port}`);
|
|
151512
151518
|
function register4(token) {
|
|
151513
151519
|
const code = randomUUID4();
|
|
151514
|
-
|
|
151515
|
-
codes.delete(code);
|
|
151516
|
-
log.debug(`git auth code expired: ${code.slice(0, 8)}...`);
|
|
151517
|
-
}, CODE_TTL_MS);
|
|
151518
|
-
timeout.unref();
|
|
151519
|
-
codes.set(code, { token, state: "pending", timeout });
|
|
151520
|
+
codes.set(code, { token, state: "active" });
|
|
151520
151521
|
return code;
|
|
151521
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
|
+
}
|
|
151522
151530
|
function writeAskpassScript(code) {
|
|
151523
151531
|
const scriptId = randomUUID4();
|
|
151524
151532
|
const scriptName = `askpass-${scriptId}.js`;
|
|
@@ -151532,17 +151540,15 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151532
151540
|
`if(r.statusCode===409){process.stderr.write("askpass-compromised\\n");process.exit(1)}`,
|
|
151533
151541
|
`if(r.statusCode!==200){process.exit(1)}`,
|
|
151534
151542
|
`var d="";r.on("data",function(c){d+=c});`,
|
|
151535
|
-
`r.on("end",function(){`,
|
|
151536
|
-
`
|
|
151537
|
-
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
151538
|
-
`})}).on("error",function(){process.exit(1)})}`
|
|
151543
|
+
`r.on("end",function(){process.stdout.write(d+"\\n")})`,
|
|
151544
|
+
`}).on("error",function(){process.exit(1)})}`
|
|
151539
151545
|
].join("\n");
|
|
151540
151546
|
writeFileSync12(scriptPath, content, { mode: 448 });
|
|
151541
151547
|
return scriptPath;
|
|
151542
151548
|
}
|
|
151543
151549
|
async function close() {
|
|
151544
151550
|
for (const entry of codes.values()) {
|
|
151545
|
-
clearTimeout(entry.timeout);
|
|
151551
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
151546
151552
|
}
|
|
151547
151553
|
codes.clear();
|
|
151548
151554
|
await new Promise((resolve3) => server.close(() => resolve3()));
|
|
@@ -151551,6 +151557,7 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
151551
151557
|
return {
|
|
151552
151558
|
port,
|
|
151553
151559
|
register: register4,
|
|
151560
|
+
revoke,
|
|
151554
151561
|
writeAskpassScript,
|
|
151555
151562
|
close,
|
|
151556
151563
|
[Symbol.asyncDispose]: close
|
|
@@ -156723,6 +156730,36 @@ async function resolveRunContextData(params) {
|
|
|
156723
156730
|
function isProviderModelNotFoundError(message) {
|
|
156724
156731
|
return message.includes("ProviderModelNotFoundError");
|
|
156725
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
|
+
}
|
|
156726
156763
|
function formatProviderModelNotFoundSummary(input) {
|
|
156727
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.
|
|
156728
156765
|
|
|
@@ -156742,6 +156779,12 @@ function renderRunError(input) {
|
|
|
156742
156779
|
isHang: true,
|
|
156743
156780
|
errorMessage: input.errorMessage
|
|
156744
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
|
+
}
|
|
156745
156788
|
const apiKeySource = hangBody ?? input.errorMessage;
|
|
156746
156789
|
const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
|
|
156747
156790
|
owner: input.repo.owner,
|
|
@@ -156865,12 +156908,14 @@ async function finalizeSuccessRun(input) {
|
|
|
156865
156908
|
repo: input.repo,
|
|
156866
156909
|
agentDiagnostic: input.toolState.agentDiagnostic
|
|
156867
156910
|
}) : null;
|
|
156868
|
-
if (rendered
|
|
156869
|
-
await reportErrorToComment({
|
|
156870
|
-
|
|
156871
|
-
|
|
156872
|
-
|
|
156873
|
-
)
|
|
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
|
+
});
|
|
156874
156919
|
}
|
|
156875
156920
|
if (input.result.success && input.toolState.progressComment && !input.toolState.finalSummaryWritten) {
|
|
156876
156921
|
await deleteProgressComment(input.toolContext).catch((error49) => {
|
|
@@ -156902,7 +156947,11 @@ async function writeRunErrorOutputs(input) {
|
|
|
156902
156947
|
} catch {
|
|
156903
156948
|
}
|
|
156904
156949
|
try {
|
|
156905
|
-
await reportErrorToComment({
|
|
156950
|
+
await reportErrorToComment({
|
|
156951
|
+
toolState: input.toolState,
|
|
156952
|
+
error: input.rendered.comment,
|
|
156953
|
+
createIfMissing: true
|
|
156954
|
+
});
|
|
156906
156955
|
} catch {
|
|
156907
156956
|
}
|
|
156908
156957
|
}
|
|
@@ -158390,7 +158439,7 @@ async function run2() {
|
|
|
158390
158439
|
}
|
|
158391
158440
|
|
|
158392
158441
|
// cli.ts
|
|
158393
|
-
var VERSION10 = "0.1.
|
|
158442
|
+
var VERSION10 = "0.1.14";
|
|
158394
158443
|
var bin = basename2(process.argv[1] || "");
|
|
158395
158444
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
158396
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 {
|
|
@@ -147419,19 +147420,22 @@ async function installFromNpmTarball(params) {
|
|
|
147419
147420
|
}
|
|
147420
147421
|
|
|
147421
147422
|
// utils/providerErrors.ts
|
|
147423
|
+
var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
|
|
147422
147424
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
147423
147425
|
var PROVIDER_ERROR_PATTERNS = [
|
|
147424
147426
|
// billing-payload patterns come BEFORE bare status-code patterns. providers
|
|
147425
147427
|
// commonly return 401 / 429 for billing/quota exhaustion (OpenCode Zen
|
|
147426
147428
|
// `CreditsError` / `FreeUsageLimitError`, Gemini `RESOURCE_EXHAUSTED` +
|
|
147427
|
-
// "spending cap", Anthropic "Insufficient balance"
|
|
147428
|
-
// and require user-billing action —
|
|
147429
|
-
// rate-limit. status-code patterns
|
|
147430
|
-
// "auth error (401)" / "rate limited (429)"
|
|
147431
|
-
|
|
147432
|
-
{ regex: /\
|
|
147433
|
-
{ regex:
|
|
147434
|
-
{ 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 },
|
|
147435
147439
|
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
147436
147440
|
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
147437
147441
|
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
@@ -147502,6 +147506,13 @@ var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_token
|
|
|
147502
147506
|
function isRouterKeylimitExhaustedError(text) {
|
|
147503
147507
|
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
147504
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
|
+
}
|
|
147505
147516
|
|
|
147506
147517
|
// utils/skills.ts
|
|
147507
147518
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -149711,8 +149722,7 @@ import { randomUUID as randomUUID4 } from "node:crypto";
|
|
|
149711
149722
|
import { writeFileSync as writeFileSync11 } from "node:fs";
|
|
149712
149723
|
import { createServer as createServer2 } from "node:http";
|
|
149713
149724
|
import { join as join15 } from "node:path";
|
|
149714
|
-
var
|
|
149715
|
-
var TAMPER_WINDOW_MS = 6e4;
|
|
149725
|
+
var REVOKED_TRAP_MS = 6e4;
|
|
149716
149726
|
function revokeGitHubToken(token) {
|
|
149717
149727
|
fetch("https://api.github.com/installation/token", {
|
|
149718
149728
|
method: "DELETE",
|
|
@@ -149743,18 +149753,14 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149743
149753
|
res.writeHead(404).end();
|
|
149744
149754
|
return;
|
|
149745
149755
|
}
|
|
149746
|
-
if (entry.state === "
|
|
149747
|
-
entry.state = "consumed";
|
|
149748
|
-
clearTimeout(entry.timeout);
|
|
149749
|
-
entry.timeout = setTimeout(() => codes.delete(code), TAMPER_WINDOW_MS);
|
|
149750
|
-
entry.timeout.unref();
|
|
149756
|
+
if (entry.state === "active") {
|
|
149751
149757
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
149752
149758
|
res.end(entry.token);
|
|
149753
149759
|
return;
|
|
149754
149760
|
}
|
|
149755
|
-
log.info("askpass code used
|
|
149761
|
+
log.info("askpass code used after revoke \u2014 revoking token");
|
|
149756
149762
|
revokeGitHubToken(entry.token);
|
|
149757
|
-
clearTimeout(entry.timeout);
|
|
149763
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
149758
149764
|
codes.delete(code);
|
|
149759
149765
|
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
149760
149766
|
res.end("compromised");
|
|
@@ -149771,14 +149777,16 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149771
149777
|
log.debug(`git auth server listening on 127.0.0.1:${port}`);
|
|
149772
149778
|
function register4(token) {
|
|
149773
149779
|
const code = randomUUID4();
|
|
149774
|
-
|
|
149775
|
-
codes.delete(code);
|
|
149776
|
-
log.debug(`git auth code expired: ${code.slice(0, 8)}...`);
|
|
149777
|
-
}, CODE_TTL_MS);
|
|
149778
|
-
timeout.unref();
|
|
149779
|
-
codes.set(code, { token, state: "pending", timeout });
|
|
149780
|
+
codes.set(code, { token, state: "active" });
|
|
149780
149781
|
return code;
|
|
149781
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
|
+
}
|
|
149782
149790
|
function writeAskpassScript(code) {
|
|
149783
149791
|
const scriptId = randomUUID4();
|
|
149784
149792
|
const scriptName = `askpass-${scriptId}.js`;
|
|
@@ -149792,17 +149800,15 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149792
149800
|
`if(r.statusCode===409){process.stderr.write("askpass-compromised\\n");process.exit(1)}`,
|
|
149793
149801
|
`if(r.statusCode!==200){process.exit(1)}`,
|
|
149794
149802
|
`var d="";r.on("data",function(c){d+=c});`,
|
|
149795
|
-
`r.on("end",function(){`,
|
|
149796
|
-
`
|
|
149797
|
-
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
149798
|
-
`})}).on("error",function(){process.exit(1)})}`
|
|
149803
|
+
`r.on("end",function(){process.stdout.write(d+"\\n")})`,
|
|
149804
|
+
`}).on("error",function(){process.exit(1)})}`
|
|
149799
149805
|
].join("\n");
|
|
149800
149806
|
writeFileSync11(scriptPath, content, { mode: 448 });
|
|
149801
149807
|
return scriptPath;
|
|
149802
149808
|
}
|
|
149803
149809
|
async function close() {
|
|
149804
149810
|
for (const entry of codes.values()) {
|
|
149805
|
-
clearTimeout(entry.timeout);
|
|
149811
|
+
if (entry.timeout) clearTimeout(entry.timeout);
|
|
149806
149812
|
}
|
|
149807
149813
|
codes.clear();
|
|
149808
149814
|
await new Promise((resolve3) => server.close(() => resolve3()));
|
|
@@ -149811,6 +149817,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
149811
149817
|
return {
|
|
149812
149818
|
port,
|
|
149813
149819
|
register: register4,
|
|
149820
|
+
revoke,
|
|
149814
149821
|
writeAskpassScript,
|
|
149815
149822
|
close,
|
|
149816
149823
|
[Symbol.asyncDispose]: close
|
|
@@ -154983,6 +154990,36 @@ async function resolveRunContextData(params) {
|
|
|
154983
154990
|
function isProviderModelNotFoundError(message) {
|
|
154984
154991
|
return message.includes("ProviderModelNotFoundError");
|
|
154985
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
|
+
}
|
|
154986
155023
|
function formatProviderModelNotFoundSummary(input) {
|
|
154987
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.
|
|
154988
155025
|
|
|
@@ -155002,6 +155039,12 @@ function renderRunError(input) {
|
|
|
155002
155039
|
isHang: true,
|
|
155003
155040
|
errorMessage: input.errorMessage
|
|
155004
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
|
+
}
|
|
155005
155048
|
const apiKeySource = hangBody ?? input.errorMessage;
|
|
155006
155049
|
const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
|
|
155007
155050
|
owner: input.repo.owner,
|
|
@@ -155125,12 +155168,14 @@ async function finalizeSuccessRun(input) {
|
|
|
155125
155168
|
repo: input.repo,
|
|
155126
155169
|
agentDiagnostic: input.toolState.agentDiagnostic
|
|
155127
155170
|
}) : null;
|
|
155128
|
-
if (rendered
|
|
155129
|
-
await reportErrorToComment({
|
|
155130
|
-
|
|
155131
|
-
|
|
155132
|
-
|
|
155133
|
-
)
|
|
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
|
+
});
|
|
155134
155179
|
}
|
|
155135
155180
|
if (input.result.success && input.toolState.progressComment && !input.toolState.finalSummaryWritten) {
|
|
155136
155181
|
await deleteProgressComment(input.toolContext).catch((error49) => {
|
|
@@ -155162,7 +155207,11 @@ async function writeRunErrorOutputs(input) {
|
|
|
155162
155207
|
} catch {
|
|
155163
155208
|
}
|
|
155164
155209
|
try {
|
|
155165
|
-
await reportErrorToComment({
|
|
155210
|
+
await reportErrorToComment({
|
|
155211
|
+
toolState: input.toolState,
|
|
155212
|
+
error: input.rendered.comment,
|
|
155213
|
+
createIfMissing: true
|
|
155214
|
+
});
|
|
155166
155215
|
} catch {
|
|
155167
155216
|
}
|
|
155168
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;
|