Share GitHub rate limit tracking between the action and the worker (#326)

* share GitHub rate limit tracking between the action and the worker

The action now counts all GitHub API requests and captures the latest
`x-ratelimit-remaining`/`x-ratelimit-reset` headers via a global
request hook on every Octokit instance.

On exit, the usage summary is written atomically to a path specified
by `PULLFROG_USAGE_SUMMARY_PATH`. The worker sets this env var before
sandbox execution, reads the file afterward, and feeds the data into
the Durable Object's rate limit state.

This closes the visibility gap where the worker had no insight into
API calls made by the sandboxed action process.

* address review: refactor rate limit state, randomize usage summary path

* track actual rate limit cost using x-ratelimit-remaining delta

* refactor usage summary writing to use onExitSignal API

Replace the monolithic registerUsageSummaryHandler with direct use of
onExitSignal in main.ts and a writeGitHubUsageSummaryToFile utility
in github.ts. This keeps exitHandler.ts as a pure signal handler
registry (from #299) and also writes the summary on normal exit.

* tweak

* unify

* deduplicate stuff

* improve error handling

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
This commit is contained in:
pullfrog[bot]
2026-03-03 12:45:36 +00:00
committed by pullfrog[bot]
parent 53970308ee
commit cc46af0d47
6 changed files with 321 additions and 88 deletions
+147 -78
View File
@@ -18198,7 +18198,7 @@ var require_summary = __commonJS({
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
var os_1 = __require("os"); var os_1 = __require("os");
var fs_1 = __require("fs"); var fs_1 = __require("fs");
var { access, appendFile, writeFile } = fs_1.promises; var { access, appendFile, writeFile: writeFile2 } = fs_1.promises;
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY"; exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary"; exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
var Summary = class { var Summary = class {
@@ -18256,7 +18256,7 @@ var require_summary = __commonJS({
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
const filePath = yield this.filePath(); const filePath = yield this.filePath();
const writeFunc = overwrite ? writeFile : appendFile; const writeFunc = overwrite ? writeFile2 : appendFile;
yield writeFunc(filePath, this._buffer, { encoding: "utf8" }); yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
return this.emptyBuffer(); return this.emptyBuffer();
}); });
@@ -26579,13 +26579,13 @@ var init_schemas = __esm({
} }
return propValues; return propValues;
}); });
const isObject4 = isObject; const isObject5 = isObject;
const catchall = def.catchall; const catchall = def.catchall;
let value2; let value2;
inst._zod.parse = (payload, ctx) => { inst._zod.parse = (payload, ctx) => {
value2 ?? (value2 = _normalized.value); value2 ?? (value2 = _normalized.value);
const input = payload.value; const input = payload.value;
if (!isObject4(input)) { if (!isObject5(input)) {
payload.issues.push({ payload.issues.push({
expected: "object", expected: "object",
code: "invalid_type", code: "invalid_type",
@@ -26683,7 +26683,7 @@ var init_schemas = __esm({
return (payload, ctx) => fn2(shape, payload, ctx); return (payload, ctx) => fn2(shape, payload, ctx);
}; };
let fastpass; let fastpass;
const isObject4 = isObject; const isObject5 = isObject;
const jit = !globalConfig.jitless; const jit = !globalConfig.jitless;
const allowsEval3 = allowsEval; const allowsEval3 = allowsEval;
const fastEnabled = jit && allowsEval3.value; const fastEnabled = jit && allowsEval3.value;
@@ -26692,7 +26692,7 @@ var init_schemas = __esm({
inst._zod.parse = (payload, ctx) => { inst._zod.parse = (payload, ctx) => {
value2 ?? (value2 = _normalized.value); value2 ?? (value2 = _normalized.value);
const input = payload.value; const input = payload.value;
if (!isObject4(input)) { if (!isObject5(input)) {
payload.issues.push({ payload.issues.push({
expected: "object", expected: "object",
code: "invalid_type", code: "invalid_type",
@@ -56273,8 +56273,8 @@ var require_snapshot_utils = __commonJS({
var require_snapshot_recorder = __commonJS({ var require_snapshot_recorder = __commonJS({
"node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) { "node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
"use strict"; "use strict";
var { writeFile, readFile, mkdir } = __require("node:fs/promises"); var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises");
var { dirname: dirname3, resolve: resolve3 } = __require("node:path"); var { dirname: dirname4, resolve: resolve3 } = __require("node:path");
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers"); var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
var { InvalidArgumentError, UndiciError } = require_errors4(); var { InvalidArgumentError, UndiciError } = require_errors4();
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils(); var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -56498,12 +56498,12 @@ var require_snapshot_recorder = __commonJS({
throw new InvalidArgumentError("Snapshot path is required"); throw new InvalidArgumentError("Snapshot path is required");
} }
const resolvedPath = resolve3(path3); const resolvedPath = resolve3(path3);
await mkdir(dirname3(resolvedPath), { recursive: true }); await mkdir(dirname4(resolvedPath), { recursive: true });
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({ const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
hash: hash2, hash: hash2,
snapshot: snapshot2 snapshot: snapshot2
})); }));
await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true }); await writeFile2(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
} }
/** /**
* Clears all recorded snapshots * Clears all recorded snapshots
@@ -72140,7 +72140,7 @@ var require_lodash = __commonJS({
var symbolProto = Symbol2 ? Symbol2.prototype : void 0; var symbolProto = Symbol2 ? Symbol2.prototype : void 0;
var symbolToString = symbolProto ? symbolProto.toString : void 0; var symbolToString = symbolProto ? symbolProto.toString : void 0;
function baseIsRegExp(value2) { function baseIsRegExp(value2) {
return isObject4(value2) && objectToString.call(value2) == regexpTag; return isObject5(value2) && objectToString.call(value2) == regexpTag;
} }
function baseSlice(array3, start, end) { function baseSlice(array3, start, end) {
var index = -1, length = array3.length; var index = -1, length = array3.length;
@@ -72174,7 +72174,7 @@ var require_lodash = __commonJS({
end = end === void 0 ? length : end; end = end === void 0 ? length : end;
return !start && end >= length ? array3 : baseSlice(array3, start, end); return !start && end >= length ? array3 : baseSlice(array3, start, end);
} }
function isObject4(value2) { function isObject5(value2) {
var type2 = typeof value2; var type2 = typeof value2;
return !!value2 && (type2 == "object" || type2 == "function"); return !!value2 && (type2 == "object" || type2 == "function");
} }
@@ -72207,9 +72207,9 @@ var require_lodash = __commonJS({
if (isSymbol(value2)) { if (isSymbol(value2)) {
return NAN; return NAN;
} }
if (isObject4(value2)) { if (isObject5(value2)) {
var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2; var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2;
value2 = isObject4(other) ? other + "" : other; value2 = isObject5(other) ? other + "" : other;
} }
if (typeof value2 != "string") { if (typeof value2 != "string") {
return value2 === 0 ? value2 : +value2; return value2 === 0 ? value2 : +value2;
@@ -72223,7 +72223,7 @@ var require_lodash = __commonJS({
} }
function truncate(string6, options) { function truncate(string6, options) {
var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION; var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION;
if (isObject4(options)) { if (isObject5(options)) {
var separator2 = "separator" in options ? options.separator : separator2; var separator2 = "separator" in options ? options.separator : separator2;
length = "length" in options ? toInteger(options.length) : length; length = "length" in options ? toInteger(options.length) : length;
omission = "omission" in options ? baseToString2(options.omission) : omission; omission = "omission" in options ? baseToString2(options.omission) : omission;
@@ -95647,14 +95647,14 @@ var require_turndown_cjs = __commonJS({
} else if (node2.nodeType === 1) { } else if (node2.nodeType === 1) {
replacement = replacementForNode.call(self2, node2); replacement = replacementForNode.call(self2, node2);
} }
return join17(output, replacement); return join18(output, replacement);
}, ""); }, "");
} }
function postProcess(output) { function postProcess(output) {
var self2 = this; var self2 = this;
this.rules.forEach(function(rule) { this.rules.forEach(function(rule) {
if (typeof rule.append === "function") { if (typeof rule.append === "function") {
output = join17(output, rule.append(self2.options)); output = join18(output, rule.append(self2.options));
} }
}); });
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, ""); return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -95666,7 +95666,7 @@ var require_turndown_cjs = __commonJS({
if (whitespace.leading || whitespace.trailing) content = content.trim(); if (whitespace.leading || whitespace.trailing) content = content.trim();
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing; return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
} }
function join17(output, replacement) { function join18(output, replacement) {
var s1 = trimTrailingNewlines(output); var s1 = trimTrailingNewlines(output);
var s2 = trimLeadingNewlines(replacement); var s2 = trimLeadingNewlines(replacement);
var nls = Math.max(output.length - s1.length, replacement.length - s2.length); var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -97612,7 +97612,7 @@ var require_semver2 = __commonJS({
// entry.ts // entry.ts
var core5 = __toESM(require_core(), 1); var core5 = __toESM(require_core(), 1);
import { dirname as dirname2 } from "node:path"; import { dirname as dirname3 } from "node:path";
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
var liftArray = (data) => Array.isArray(data) ? data : [data]; var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -136544,7 +136544,7 @@ function AskQuestionTool(ctx) {
// mcp/checkout.ts // mcp/checkout.ts
import { writeFileSync as writeFileSync2 } from "node:fs"; import { writeFileSync as writeFileSync2 } from "node:fs";
import { join as join2 } from "node:path"; import { join as join3 } from "node:path";
// utils/gitAuth.ts // utils/gitAuth.ts
import { execSync as execSync2, spawnSync } from "node:child_process"; import { execSync as execSync2, spawnSync } from "node:child_process";
@@ -136583,6 +136583,8 @@ function exitWithSignal(signal) {
// utils/github.ts // utils/github.ts
var core2 = __toESM(require_core(), 1); var core2 = __toESM(require_core(), 1);
import { createSign } from "node:crypto"; import { createSign } from "node:crypto";
import { rename, writeFile } from "node:fs/promises";
import { dirname, join as join2 } from "node:path";
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js // node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
var import_light = __toESM(require_light(), 1); var import_light = __toESM(require_light(), 1);
@@ -140291,6 +140293,9 @@ async function retry(fn2, options = {}) {
} }
// utils/github.ts // utils/github.ts
function isObject4(value2) {
return typeof value2 === "object" && value2 !== null;
}
function isOIDCAvailable() { function isOIDCAvailable() {
return Boolean( return Boolean(
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
@@ -140451,9 +140456,35 @@ function parseRepoContext() {
} }
return { owner, name }; return { owner, name };
} }
function emptyResourceUsage() {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null
};
}
var usageByResource = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage()
};
function getGitHubUsageSummary() {
return {
version: 1,
github: {
core: usageByResource.core,
graphql: usageByResource.graphql
}
};
}
async function writeGitHubUsageSummaryToFile(path3) {
const summary2 = getGitHubUsageSummary();
const tmpPath = join2(dirname(path3), `.usage-summary-${process.pid}.tmp`);
await writeFile(tmpPath, JSON.stringify(summary2));
await rename(tmpPath, path3);
}
function createOctokit(token) { function createOctokit(token) {
const OctokitWithPlugins = Octokit2.plugin(throttling); const OctokitWithPlugins = Octokit2.plugin(throttling);
return new OctokitWithPlugins({ const octokit = new OctokitWithPlugins({
auth: token, auth: token,
throttle: { throttle: {
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => { onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
@@ -140464,6 +140495,37 @@ function createOctokit(token) {
} }
} }
}); });
const onResponse = (response) => {
const resource = response.headers["x-ratelimit-resource"];
if (!resource) {
return response;
}
usageByResource[resource] ??= emptyResourceUsage();
const usage = usageByResource[resource];
usage.requestCount++;
const remaining = response.headers["x-ratelimit-remaining"];
const reset = response.headers["x-ratelimit-reset"];
if (remaining !== void 0) {
usage.rateLimitRemaining = Number(remaining);
}
if (reset !== void 0) {
usage.rateLimitResetMs = Number(reset) * 1e3;
}
return response;
};
octokit.hook.wrap("request", async (request2, options) => {
try {
const response = await request2(options);
onResponse(response);
return response;
} catch (error49) {
if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
onResponse(error49.response);
}
throw error49;
}
});
return octokit;
} }
// utils/token.ts // utils/token.ts
@@ -141081,7 +141143,7 @@ ${diffPreview}`);
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context" "PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
); );
} }
const diffPath = join2(tempDir, `pr-${pull_number}.diff`); const diffPath = join3(tempDir, `pr-${pull_number}.diff`);
writeFileSync2(diffPath, formatResult.content); writeFileSync2(diffPath, formatResult.content);
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`); log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
return { return {
@@ -141105,7 +141167,7 @@ ${diffPreview}`);
// mcp/checkSuite.ts // mcp/checkSuite.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs"; import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
import { join as join3 } from "node:path"; import { join as join4 } from "node:path";
var GetCheckSuiteLogs = type({ var GetCheckSuiteLogs = type({
check_suite_id: type.number.describe("the id from check_suite.id") check_suite_id: type.number.describe("the id from check_suite.id")
}); });
@@ -141200,7 +141262,7 @@ function GetCheckSuiteLogsTool(ctx) {
if (!tempDir) { if (!tempDir) {
throw new Error("PULLFROG_TEMP_DIR not set"); throw new Error("PULLFROG_TEMP_DIR not set");
} }
const logsDir = join3(tempDir, "ci-logs"); const logsDir = join4(tempDir, "ci-logs");
mkdirSync2(logsDir, { recursive: true }); mkdirSync2(logsDir, { recursive: true });
const jobResults = []; const jobResults = [];
for (const run2 of failedRuns) { for (const run2 of failedRuns) {
@@ -141219,7 +141281,7 @@ function GetCheckSuiteLogsTool(ctx) {
}); });
const logsUrl = logsResponse.url; const logsUrl = logsResponse.url;
const logsText = await fetch(logsUrl).then((r) => r.text()); const logsText = await fetch(logsUrl).then((r) => r.text());
const logPath = join3(logsDir, `job-${job.id}.log`); const logPath = join4(logsDir, `job-${job.id}.log`);
writeFileSync3(logPath, logsText); writeFileSync3(logPath, logsText);
const analysis = analyzeLog(logsText, 80); const analysis = analyzeLog(logsText, 80);
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? []; const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
@@ -141556,7 +141618,7 @@ function ReplyToReviewCommentTool(ctx) {
// mcp/commitInfo.ts // mcp/commitInfo.ts
import { writeFileSync as writeFileSync4 } from "node:fs"; import { writeFileSync as writeFileSync4 } from "node:fs";
import { join as join4 } from "node:path"; import { join as join5 } from "node:path";
var CommitInfo = type({ var CommitInfo = type({
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch") sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
}); });
@@ -141580,7 +141642,7 @@ function CommitInfoTool(ctx) {
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context" "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
); );
} }
const diffFile = join4(tempDir, `commit-${sha.slice(0, 7)}.diff`); const diffFile = join5(tempDir, `commit-${sha.slice(0, 7)}.diff`);
writeFileSync4(diffFile, formatResult.content); writeFileSync4(diffFile, formatResult.content);
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`); log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
return { return {
@@ -141688,7 +141750,7 @@ import { performance as performance4 } from "node:perf_hooks";
// prep/installNodeDependencies.ts // prep/installNodeDependencies.ts
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs"; import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
import { join as join5 } from "node:path"; import { join as join6 } from "node:path";
// node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs // node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
function dashDashArg(agent2, agentCommand) { function dashDashArg(agent2, agentCommand) {
@@ -142000,7 +142062,7 @@ async function isCommandAvailable(command) {
return result.exitCode === 0; return result.exitCode === 0;
} }
function getPackageManagerFromPackageJson() { function getPackageManagerFromPackageJson() {
const packageJsonPath = join5(process.cwd(), "package.json"); const packageJsonPath = join6(process.cwd(), "package.json");
try { try {
const content = readFileSync2(packageJsonPath, "utf-8"); const content = readFileSync2(packageJsonPath, "utf-8");
const pkg = JSON.parse(content); const pkg = JSON.parse(content);
@@ -142031,7 +142093,7 @@ async function installPackageManager(name, installSpec) {
return result.stderr || `failed to install ${name}`; return result.stderr || `failed to install ${name}`;
} }
if (name === "deno") { if (name === "deno") {
const denoPath = join5(process.env.HOME || "", ".deno", "bin"); const denoPath = join6(process.env.HOME || "", ".deno", "bin");
process.env.PATH = `${denoPath}:${process.env.PATH}`; process.env.PATH = `${denoPath}:${process.env.PATH}`;
} }
log.info(`\xBB installed ${name}`); log.info(`\xBB installed ${name}`);
@@ -142040,7 +142102,7 @@ async function installPackageManager(name, installSpec) {
var installNodeDependencies = { var installNodeDependencies = {
name: "installNodeDependencies", name: "installNodeDependencies",
shouldRun: () => { shouldRun: () => {
const packageJsonPath = join5(process.cwd(), "package.json"); const packageJsonPath = join6(process.cwd(), "package.json");
return existsSync2(packageJsonPath); return existsSync2(packageJsonPath);
}, },
run: async (options) => { run: async (options) => {
@@ -142125,7 +142187,7 @@ ${errorMessage}`]
// prep/installPythonDependencies.ts // prep/installPythonDependencies.ts
import { existsSync as existsSync3 } from "node:fs"; import { existsSync as existsSync3 } from "node:fs";
import { join as join6 } from "node:path"; import { join as join7 } from "node:path";
var PYTHON_CONFIGS = [ var PYTHON_CONFIGS = [
{ {
file: "requirements.txt", file: "requirements.txt",
@@ -142197,11 +142259,11 @@ var installPythonDependencies = {
return false; return false;
} }
const cwd = process.cwd(); const cwd = process.cwd();
return PYTHON_CONFIGS.some((config3) => existsSync3(join6(cwd, config3.file))); return PYTHON_CONFIGS.some((config3) => existsSync3(join7(cwd, config3.file)));
}, },
run: async (options) => { run: async (options) => {
const cwd = process.cwd(); const cwd = process.cwd();
const config3 = PYTHON_CONFIGS.find((c) => existsSync3(join6(cwd, c.file))); const config3 = PYTHON_CONFIGS.find((c) => existsSync3(join7(cwd, c.file)));
if (!config3) { if (!config3) {
return { return {
language: "python", language: "python",
@@ -142446,7 +142508,7 @@ import {
unlinkSync, unlinkSync,
writeFileSync as writeFileSync5 writeFileSync as writeFileSync5
} from "node:fs"; } from "node:fs";
import { dirname, join as join7, resolve } from "node:path"; import { dirname as dirname2, join as join8, resolve } from "node:path";
var FileReadParams = type({ var FileReadParams = type({
path: "string", path: "string",
"offset?": "number", "offset?": "number",
@@ -142478,7 +142540,7 @@ function resolveReadPath(filePath) {
} }
const home = process.env.HOME; const home = process.env.HOME;
if (home) { if (home) {
const cursorProjectsDir = join7(home, ".cursor", "projects"); const cursorProjectsDir = join8(home, ".cursor", "projects");
if (resolved.startsWith(cursorProjectsDir + "/")) { if (resolved.startsWith(cursorProjectsDir + "/")) {
return resolved; return resolved;
} }
@@ -142505,9 +142567,9 @@ function resolveWritePath(filePath, shellPermission) {
throw new Error(`path must be within the repository (symlink escape blocked): ${filePath}`); throw new Error(`path must be within the repository (symlink escape blocked): ${filePath}`);
} }
} else { } else {
let ancestor = dirname(resolved); let ancestor = dirname2(resolved);
while (!existsSync4(ancestor)) { while (!existsSync4(ancestor)) {
const parent = dirname(ancestor); const parent = dirname2(ancestor);
if (parent === ancestor) break; if (parent === ancestor) break;
ancestor = parent; ancestor = parent;
} }
@@ -142566,7 +142628,7 @@ function FileWriteTool(ctx) {
parameters: FileWriteParams, parameters: FileWriteParams,
execute: execute(async (params) => { execute: execute(async (params) => {
const resolved = resolveWritePath(params.path, ctx.payload.shell); const resolved = resolveWritePath(params.path, ctx.payload.shell);
const dir = dirname(resolved); const dir = dirname2(resolved);
mkdirSync3(dir, { recursive: true }); mkdirSync3(dir, { recursive: true });
writeFileSync5(resolved, params.content, "utf-8"); writeFileSync5(resolved, params.content, "utf-8");
return { path: params.path, written: true }; return { path: params.path, written: true };
@@ -143411,7 +143473,7 @@ async function reportReviewNodeId(ctx, reviewNodeId) {
// mcp/reviewComments.ts // mcp/reviewComments.ts
import { writeFileSync as writeFileSync6 } from "node:fs"; import { writeFileSync as writeFileSync6 } from "node:fs";
import { join as join8 } from "node:path"; import { join as join9 } from "node:path";
var REVIEW_THREADS_QUERY = ` var REVIEW_THREADS_QUERY = `
query ($owner: String!, $name: String!, $prNumber: Int!) { query ($owner: String!, $name: String!, $prNumber: Int!) {
repository(owner: $owner, name: $name) { repository(owner: $owner, name: $name) {
@@ -143781,7 +143843,7 @@ function GetReviewCommentsTool(ctx) {
throw new Error("PULLFROG_TEMP_DIR not set"); throw new Error("PULLFROG_TEMP_DIR not set");
} }
const filename = `review-${params.review_id}-threads.md`; const filename = `review-${params.review_id}-threads.md`;
const commentsPath = join8(tempDir, filename); const commentsPath = join9(tempDir, filename);
writeFileSync6(commentsPath, formatted.content); writeFileSync6(commentsPath, formatted.content);
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`); log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
return { return {
@@ -144073,7 +144135,7 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
import { randomUUID as randomUUID3 } from "node:crypto"; import { randomUUID as randomUUID3 } from "node:crypto";
import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs"; import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs";
import { userInfo } from "node:os"; import { userInfo } from "node:os";
import { join as join9 } from "node:path"; import { join as join10 } from "node:path";
var ShellParams = type({ var ShellParams = type({
command: "string", command: "string",
description: "string", description: "string",
@@ -144207,8 +144269,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
if (params.background) { if (params.background) {
const tempDir = getTempDir(); const tempDir = getTempDir();
const handle = `bg-${randomUUID3().slice(0, 8)}`; const handle = `bg-${randomUUID3().slice(0, 8)}`;
const outputPath = join9(tempDir, `${handle}.log`); const outputPath = join10(tempDir, `${handle}.log`);
const pidPath = join9(tempDir, `${handle}.pid`); const pidPath = join10(tempDir, `${handle}.pid`);
const logFd = openSync(outputPath, "a"); const logFd = openSync(outputPath, "a");
let proc2; let proc2;
try { try {
@@ -144834,7 +144896,7 @@ var modes = computeModes();
// agents/claude.ts // agents/claude.ts
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs"; import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
import { join as join11 } from "node:path"; import { join as join12 } from "node:path";
// package.json // package.json
var package_default = { var package_default = {
@@ -144932,13 +144994,13 @@ import { spawnSync as spawnSync4 } from "node:child_process";
import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs"; import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
import { mkdtemp } from "node:fs/promises"; import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join as join10 } from "node:path"; import { join as join11 } from "node:path";
import { pipeline } from "node:stream/promises"; import { pipeline } from "node:stream/promises";
async function installFromNpmTarball(params) { async function installFromNpmTarball(params) {
const tempDir = process.env.PULLFROG_TEMP_DIR; const tempDir = process.env.PULLFROG_TEMP_DIR;
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set"); if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set");
const extractedDir = join10(tempDir, "package"); const extractedDir = join11(tempDir, "package");
const cliPath = join10(extractedDir, params.executablePath); const cliPath = join11(extractedDir, params.executablePath);
if (existsSync5(cliPath)) { if (existsSync5(cliPath)) {
log.debug(`\xBB using cached binary at ${cliPath}`); log.debug(`\xBB using cached binary at ${cliPath}`);
return cliPath; return cliPath;
@@ -144963,7 +145025,7 @@ async function installFromNpmTarball(params) {
} }
} }
log.debug(`\xBB installing ${params.packageName}@${resolvedVersion}...`); log.debug(`\xBB installing ${params.packageName}@${resolvedVersion}...`);
const tarballPath = join10(tempDir, "package.tgz"); const tarballPath = join11(tempDir, "package.tgz");
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org"; const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
let tarballUrl; let tarballUrl;
if (params.packageName.startsWith("@")) { if (params.packageName.startsWith("@")) {
@@ -145037,8 +145099,8 @@ async function fetchWithRetry(url4, headers, errorMessage) {
} }
async function installFromGithub(params) { async function installFromGithub(params) {
const pullfrogTemp = process.env.PULLFROG_TEMP_DIR; const pullfrogTemp = process.env.PULLFROG_TEMP_DIR;
const installDir = pullfrogTemp ? join10(pullfrogTemp, `github-${params.owner}-${params.repo}`) : await mkdtemp(join10(tmpdir(), `${params.owner}-${params.repo}-github-`)); const installDir = pullfrogTemp ? join11(pullfrogTemp, `github-${params.owner}-${params.repo}`) : await mkdtemp(join11(tmpdir(), `${params.owner}-${params.repo}-github-`));
const expectedCliPath = join10(installDir, params.executablePath ?? params.assetName ?? "asset"); const expectedCliPath = join11(installDir, params.executablePath ?? params.assetName ?? "asset");
if (existsSync5(expectedCliPath)) { if (existsSync5(expectedCliPath)) {
log.debug(`\xBB using cached binary at ${expectedCliPath}`); log.debug(`\xBB using cached binary at ${expectedCliPath}`);
return expectedCliPath; return expectedCliPath;
@@ -145062,13 +145124,13 @@ async function installFromGithub(params) {
mkdirSync4(installDir, { recursive: true }); mkdirSync4(installDir, { recursive: true });
const urlPath = new URL(assetUrl).pathname; const urlPath = new URL(assetUrl).pathname;
const fileName2 = urlPath.split("/").pop() || "asset"; const fileName2 = urlPath.split("/").pop() || "asset";
const downloadPath = join10(installDir, fileName2); const downloadPath = join11(installDir, fileName2);
const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset"); const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
if (!assetResponse.body) throw new Error("Response body is null"); if (!assetResponse.body) throw new Error("Response body is null");
const fileStream = createWriteStream(downloadPath); const fileStream = createWriteStream(downloadPath);
await pipeline(assetResponse.body, fileStream); await pipeline(assetResponse.body, fileStream);
log.debug(`\xBB downloaded asset to ${downloadPath}`); log.debug(`\xBB downloaded asset to ${downloadPath}`);
const cliPath = params.executablePath ? join10(installDir, params.executablePath) : downloadPath; const cliPath = params.executablePath ? join11(installDir, params.executablePath) : downloadPath;
if (!existsSync5(cliPath)) { if (!existsSync5(cliPath)) {
throw new Error(`Executable not found at ${cliPath}`); throw new Error(`Executable not found at ${cliPath}`);
} }
@@ -145079,14 +145141,14 @@ async function installFromGithub(params) {
async function installFromDirectTarball(params) { async function installFromDirectTarball(params) {
const tempDir = process.env.PULLFROG_TEMP_DIR; const tempDir = process.env.PULLFROG_TEMP_DIR;
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set"); if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set");
const extractDir = join10(tempDir, "direct-package"); const extractDir = join11(tempDir, "direct-package");
const cliPath = join10(extractDir, params.executablePath); const cliPath = join11(extractDir, params.executablePath);
if (existsSync5(cliPath)) { if (existsSync5(cliPath)) {
log.debug(`\xBB using cached binary at ${cliPath}`); log.debug(`\xBB using cached binary at ${cliPath}`);
return cliPath; return cliPath;
} }
log.info(`\xBB downloading tarball from ${params.url}...`); log.info(`\xBB downloading tarball from ${params.url}...`);
const tarballPath = join10(tempDir, "direct-package.tgz"); const tarballPath = join11(tempDir, "direct-package.tgz");
const response = await fetchWithRetry(params.url, {}, "failed to download tarball"); const response = await fetchWithRetry(params.url, {}, "failed to download tarball");
if (!response.body) throw new Error("response body is null"); if (!response.body) throw new Error("response body is null");
const fileStream = createWriteStream(tarballPath); const fileStream = createWriteStream(tarballPath);
@@ -145198,9 +145260,9 @@ function buildDisallowedTools(ctx) {
return disallowed; return disallowed;
} }
function writeMcpConfig(ctx) { function writeMcpConfig(ctx) {
const configDir = join11(ctx.tmpdir, ".claude"); const configDir = join12(ctx.tmpdir, ".claude");
mkdirSync5(configDir, { recursive: true }); mkdirSync5(configDir, { recursive: true });
const configPath = join11(configDir, "mcp.json"); const configPath = join12(configDir, "mcp.json");
const mcpConfig = { const mcpConfig = {
mcpServers: { mcpServers: {
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl } [ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
@@ -145417,7 +145479,7 @@ var messageHandlers = {
// agents/codex.ts // agents/codex.ts
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "node:fs"; import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
import { join as join12 } from "node:path"; import { join as join13 } from "node:path";
var CODEX_CLI_VERSION = "0.101.0"; var CODEX_CLI_VERSION = "0.101.0";
var PREFERRED_MODEL = "gpt-5.3-codex"; var PREFERRED_MODEL = "gpt-5.3-codex";
var FALLBACK_MODEL = "gpt-5.2-codex"; var FALLBACK_MODEL = "gpt-5.2-codex";
@@ -145457,9 +145519,9 @@ async function resolveModel(apiKey) {
return FALLBACK_MODEL; return FALLBACK_MODEL;
} }
function writeCodexConfig(ctx) { function writeCodexConfig(ctx) {
const codexDir = join12(ctx.tmpdir, ".codex"); const codexDir = join13(ctx.tmpdir, ".codex");
mkdirSync6(codexDir, { recursive: true }); mkdirSync6(codexDir, { recursive: true });
const configPath = join12(codexDir, "config.toml"); const configPath = join13(codexDir, "config.toml");
log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`); log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`);
const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}] const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}]
url = "${ctx.mcpServerUrl}"`]; url = "${ctx.mcpServerUrl}"`];
@@ -145711,7 +145773,7 @@ function createMessageHandlers() {
import { spawn as spawn3 } from "node:child_process"; import { spawn as spawn3 } from "node:child_process";
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync10 } from "node:fs"; import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync10 } from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join as join13 } from "node:path"; import { join as join14 } from "node:path";
import { performance as performance6 } from "node:perf_hooks"; import { performance as performance6 } from "node:perf_hooks";
var CURSOR_CLI_VERSION = "2026.01.28-fd13201"; var CURSOR_CLI_VERSION = "2026.01.28-fd13201";
var cursorEffortModels = { var cursorEffortModels = {
@@ -145741,7 +145803,7 @@ var cursor = agent({
const cliPath = await installCursor(); const cliPath = await installCursor();
configureCursorMcpServers(ctx); configureCursorMcpServers(ctx);
configureCursorTools(ctx); configureCursorTools(ctx);
const projectCliConfigPath = join13(process.cwd(), ".cursor", "cli.json"); const projectCliConfigPath = join14(process.cwd(), ".cursor", "cli.json");
let modelOverride = null; let modelOverride = null;
if (existsSync6(projectCliConfigPath)) { if (existsSync6(projectCliConfigPath)) {
try { try {
@@ -145926,11 +145988,11 @@ var cursor = agent({
} }
}); });
function getCursorConfigDir() { function getCursorConfigDir() {
return join13(homedir(), ".cursor"); return join14(homedir(), ".cursor");
} }
function configureCursorMcpServers(ctx) { function configureCursorMcpServers(ctx) {
const cursorConfigDir = getCursorConfigDir(); const cursorConfigDir = getCursorConfigDir();
const mcpConfigPath = join13(cursorConfigDir, "mcp.json"); const mcpConfigPath = join14(cursorConfigDir, "mcp.json");
mkdirSync7(cursorConfigDir, { recursive: true }); mkdirSync7(cursorConfigDir, { recursive: true });
const mcpServers = { const mcpServers = {
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl } [ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
@@ -145940,7 +146002,7 @@ function configureCursorMcpServers(ctx) {
} }
function configureCursorTools(ctx) { function configureCursorTools(ctx) {
const cursorConfigDir = getCursorConfigDir(); const cursorConfigDir = getCursorConfigDir();
const cliConfigPath = join13(cursorConfigDir, "cli-config.json"); const cliConfigPath = join14(cursorConfigDir, "cli-config.json");
mkdirSync7(cursorConfigDir, { recursive: true }); mkdirSync7(cursorConfigDir, { recursive: true });
const shell = ctx.payload.shell; const shell = ctx.payload.shell;
const deny = []; const deny = [];
@@ -145969,7 +146031,7 @@ function configureCursorTools(ctx) {
// agents/gemini.ts // agents/gemini.ts
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync11 } from "node:fs"; import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync11 } from "node:fs";
import { homedir as homedir2 } from "node:os"; import { homedir as homedir2 } from "node:os";
import { join as join14 } from "node:path"; import { join as join15 } from "node:path";
var geminiEffortConfig = { var geminiEffortConfig = {
// https://ai.google.dev/gemini-api/docs/models // https://ai.google.dev/gemini-api/docs/models
// the docs mention needing to enable preview features for these models but if you // the docs mention needing to enable preview features for these models but if you
@@ -146193,8 +146255,8 @@ function configureGeminiSettings(ctx) {
const thinkingLevel = effortConfig.thinkingLevel; const thinkingLevel = effortConfig.thinkingLevel;
log.info(`\xBB model: ${model} (thinkingLevel: ${thinkingLevel})`); log.info(`\xBB model: ${model} (thinkingLevel: ${thinkingLevel})`);
const realHome = homedir2(); const realHome = homedir2();
const geminiConfigDir = join14(realHome, ".gemini"); const geminiConfigDir = join15(realHome, ".gemini");
const settingsPath = join14(geminiConfigDir, "settings.json"); const settingsPath = join15(geminiConfigDir, "settings.json");
mkdirSync8(geminiConfigDir, { recursive: true }); mkdirSync8(geminiConfigDir, { recursive: true });
let existingSettings = {}; let existingSettings = {};
try { try {
@@ -146241,7 +146303,7 @@ function configureGeminiSettings(ctx) {
// agents/opencode.ts // agents/opencode.ts
import { existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync12 } from "node:fs"; import { existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync12 } from "node:fs";
import { join as join15 } from "node:path"; import { join as join16 } from "node:path";
import { performance as performance7 } from "node:perf_hooks"; import { performance as performance7 } from "node:perf_hooks";
var OPENCODE_CLI_VERSION = "1.1.56"; var OPENCODE_CLI_VERSION = "1.1.56";
var PROVIDER_ERROR_PATTERNS = [ var PROVIDER_ERROR_PATTERNS = [
@@ -146370,7 +146432,7 @@ var opencode = agent({
run: async (ctx) => { run: async (ctx) => {
const cliPath = await installOpencode(); const cliPath = await installOpencode();
const tempHome = ctx.tmpdir; const tempHome = ctx.tmpdir;
const configDir = join15(tempHome, ".config", "opencode"); const configDir = join16(tempHome, ".config", "opencode");
mkdirSync9(configDir, { recursive: true }); mkdirSync9(configDir, { recursive: true });
configureOpenCode(ctx); configureOpenCode(ctx);
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"]; const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
@@ -146388,7 +146450,7 @@ var opencode = agent({
const env2 = { const env2 = {
...process.env, ...process.env,
HOME: tempHome, HOME: tempHome,
XDG_CONFIG_HOME: join15(tempHome, ".config"), XDG_CONFIG_HOME: join16(tempHome, ".config"),
// set GOOGLE_GENERATIVE_AI_API_KEY alias for Google provider compatibility (if not already set) // set GOOGLE_GENERATIVE_AI_API_KEY alias for Google provider compatibility (if not already set)
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
}; };
@@ -146571,10 +146633,10 @@ ${stderrContext}`
} }
}); });
function configureOpenCode(ctx) { function configureOpenCode(ctx) {
const configDir = join15(ctx.tmpdir, ".config", "opencode"); const configDir = join16(ctx.tmpdir, ".config", "opencode");
mkdirSync9(configDir, { recursive: true }); mkdirSync9(configDir, { recursive: true });
const configPath = join15(configDir, "opencode.json"); const configPath = join16(configDir, "opencode.json");
const repoConfigPath = join15(process.cwd(), "opencode.json"); const repoConfigPath = join16(process.cwd(), "opencode.json");
const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath }); const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath });
if (repoConfig?.model) { if (repoConfig?.model) {
log.info(`\xBB repo opencode model configured: ${repoConfig.model}`); log.info(`\xBB repo opencode model configured: ${repoConfig.model}`);
@@ -147641,9 +147703,9 @@ async function resolveRunContextData(params) {
import { execSync as execSync4 } from "node:child_process"; import { execSync as execSync4 } from "node:child_process";
import { mkdtempSync } from "node:fs"; import { mkdtempSync } from "node:fs";
import { tmpdir as tmpdir2 } from "node:os"; import { tmpdir as tmpdir2 } from "node:os";
import { join as join16 } from "node:path"; import { join as join17 } from "node:path";
function createTempDirectory() { function createTempDirectory() {
const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-")); const sharedTempDir = mkdtempSync(join17(tmpdir2(), "pullfrog-"));
process.env.PULLFROG_TEMP_DIR = sharedTempDir; process.env.PULLFROG_TEMP_DIR = sharedTempDir;
log.info(`\xBB created temp dir at ${sharedTempDir}`); log.info(`\xBB created temp dir at ${sharedTempDir}`);
return sharedTempDir; return sharedTempDir;
@@ -147768,6 +147830,10 @@ async function main() {
var _stack2 = []; var _stack2 = [];
try { try {
normalizeEnv(); normalizeEnv();
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
if (usageSummaryPath) {
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
}
const timer = new Timer(); const timer = new Timer();
let activityTimeout = null; let activityTimeout = null;
const resolvedPromptInput = resolvePromptInput(); const resolvedPromptInput = resolvePromptInput();
@@ -147944,6 +148010,9 @@ ${instructions.user}` : null,
}; };
} finally { } finally {
activityTimeout?.stop(); activityTimeout?.stop();
if (usageSummaryPath) {
await writeGitHubUsageSummaryToFile(usageSummaryPath);
}
} }
} catch (_2) { } catch (_2) {
var _error2 = _2, _hasError2 = true; var _error2 = _2, _hasError2 = true;
@@ -147954,7 +148023,7 @@ ${instructions.user}` : null,
} }
// entry.ts // entry.ts
process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`; process.env.PATH = `${dirname3(process.execPath)}:${process.env.PATH}`;
async function run() { async function run() {
try { try {
const result = await main(); const result = await main();
+12 -1
View File
@@ -25699,8 +25699,8 @@ function formatJsonValue(value) {
} }
// utils/github.ts // utils/github.ts
var core2 = __toESM(require_core(), 1);
import { createSign } from "node:crypto"; import { createSign } from "node:crypto";
var core2 = __toESM(require_core(), 1);
// utils/apiUrl.ts // utils/apiUrl.ts
function isLocalUrl(url) { function isLocalUrl(url) {
@@ -25930,6 +25930,17 @@ function parseRepoContext() {
} }
return { owner, name }; return { owner, name };
} }
function emptyResourceUsage() {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null
};
}
var usageByResource = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage()
};
// utils/token.ts // utils/token.ts
async function revokeGitHubInstallationToken(token) { async function revokeGitHubInstallationToken(token) {
+1 -1
View File
@@ -30,7 +30,7 @@ export {
PULLFROG_DIVIDER, PULLFROG_DIVIDER,
stripExistingFooter, stripExistingFooter,
} from "../utils/buildPullfrogFooter.ts"; } from "../utils/buildPullfrogFooter.ts";
export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
export { export {
isValidTimeString, isValidTimeString,
parseTimeString, parseTimeString,
+11 -1
View File
@@ -12,8 +12,9 @@ import { validateAgentApiKey } from "./utils/apiKeys.ts";
import { resolveBody } from "./utils/body.ts"; import { resolveBody } from "./utils/body.ts";
import { formatUsageSummary, log, writeSummary } from "./utils/cli.ts"; import { formatUsageSummary, log, writeSummary } from "./utils/cli.ts";
import { reportErrorToComment } from "./utils/errorReport.ts"; import { reportErrorToComment } from "./utils/errorReport.ts";
import { onExitSignal } from "./utils/exitHandler.ts";
import { resolveGit } from "./utils/gitAuth.ts"; import { resolveGit } from "./utils/gitAuth.ts";
import { createOctokit } from "./utils/github.ts"; import { createOctokit, writeGitHubUsageSummaryToFile } from "./utils/github.ts";
import { resolveInstructions } from "./utils/instructions.ts"; import { resolveInstructions } from "./utils/instructions.ts";
import { executeLifecycleHook } from "./utils/lifecycle.ts"; import { executeLifecycleHook } from "./utils/lifecycle.ts";
import { normalizeEnv } from "./utils/normalizeEnv.ts"; import { normalizeEnv } from "./utils/normalizeEnv.ts";
@@ -48,6 +49,12 @@ export async function main(): Promise<MainResult> {
// normalize env var names to uppercase (handles case-insensitive workflow files) // normalize env var names to uppercase (handles case-insensitive workflow files)
normalizeEnv(); normalizeEnv();
// write usage summary on SIGINT/SIGTERM so the worker can read it after sandbox.exec
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
if (usageSummaryPath) {
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
}
const timer = new Timer(); const timer = new Timer();
let activityTimeout: ActivityTimeout | null = null; let activityTimeout: ActivityTimeout | null = null;
@@ -267,5 +274,8 @@ export async function main(): Promise<MainResult> {
}; };
} finally { } finally {
activityTimeout?.stop(); activityTimeout?.stop();
if (usageSummaryPath) {
await writeGitHubUsageSummaryToFile(usageSummaryPath);
}
} }
} }
+51 -6
View File
@@ -24695,7 +24695,7 @@ var require_lodash = __commonJS({
var symbolProto = Symbol2 ? Symbol2.prototype : void 0; var symbolProto = Symbol2 ? Symbol2.prototype : void 0;
var symbolToString = symbolProto ? symbolProto.toString : void 0; var symbolToString = symbolProto ? symbolProto.toString : void 0;
function baseIsRegExp(value2) { function baseIsRegExp(value2) {
return isObject(value2) && objectToString.call(value2) == regexpTag; return isObject2(value2) && objectToString.call(value2) == regexpTag;
} }
function baseSlice(array, start, end) { function baseSlice(array, start, end) {
var index = -1, length = array.length; var index = -1, length = array.length;
@@ -24729,7 +24729,7 @@ var require_lodash = __commonJS({
end = end === void 0 ? length : end; end = end === void 0 ? length : end;
return !start && end >= length ? array : baseSlice(array, start, end); return !start && end >= length ? array : baseSlice(array, start, end);
} }
function isObject(value2) { function isObject2(value2) {
var type2 = typeof value2; var type2 = typeof value2;
return !!value2 && (type2 == "object" || type2 == "function"); return !!value2 && (type2 == "object" || type2 == "function");
} }
@@ -24762,9 +24762,9 @@ var require_lodash = __commonJS({
if (isSymbol(value2)) { if (isSymbol(value2)) {
return NAN; return NAN;
} }
if (isObject(value2)) { if (isObject2(value2)) {
var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2; var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2;
value2 = isObject(other) ? other + "" : other; value2 = isObject2(other) ? other + "" : other;
} }
if (typeof value2 != "string") { if (typeof value2 != "string") {
return value2 === 0 ? value2 : +value2; return value2 === 0 ? value2 : +value2;
@@ -24778,7 +24778,7 @@ var require_lodash = __commonJS({
} }
function truncate(string2, options) { function truncate(string2, options) {
var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION; var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION;
if (isObject(options)) { if (isObject2(options)) {
var separator2 = "separator" in options ? options.separator : separator2; var separator2 = "separator" in options ? options.separator : separator2;
length = "length" in options ? toInteger(options.length) : length; length = "length" in options ? toInteger(options.length) : length;
omission = "omission" in options ? baseToString(options.omission) : omission; omission = "omission" in options ? baseToString(options.omission) : omission;
@@ -41183,6 +41183,9 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
); );
// utils/github.ts // utils/github.ts
function isObject(value2) {
return typeof value2 === "object" && value2 !== null;
}
function parseRepoContext() { function parseRepoContext() {
const githubRepo = process.env.GITHUB_REPOSITORY; const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) { if (!githubRepo) {
@@ -41194,9 +41197,20 @@ function parseRepoContext() {
} }
return { owner, name }; return { owner, name };
} }
function emptyResourceUsage() {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null
};
}
var usageByResource = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage()
};
function createOctokit(token) { function createOctokit(token) {
const OctokitWithPlugins = Octokit2.plugin(throttling); const OctokitWithPlugins = Octokit2.plugin(throttling);
return new OctokitWithPlugins({ const octokit = new OctokitWithPlugins({
auth: token, auth: token,
throttle: { throttle: {
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => { onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
@@ -41207,6 +41221,37 @@ function createOctokit(token) {
} }
} }
}); });
const onResponse = (response) => {
const resource = response.headers["x-ratelimit-resource"];
if (!resource) {
return response;
}
usageByResource[resource] ??= emptyResourceUsage();
const usage = usageByResource[resource];
usage.requestCount++;
const remaining = response.headers["x-ratelimit-remaining"];
const reset = response.headers["x-ratelimit-reset"];
if (remaining !== void 0) {
usage.rateLimitRemaining = Number(remaining);
}
if (reset !== void 0) {
usage.rateLimitResetMs = Number(reset) * 1e3;
}
return response;
};
octokit.hook.wrap("request", async (request2, options) => {
try {
const response = await request2(options);
onResponse(response);
return response;
} catch (error2) {
if (isObject(error2) && "response" in error2 && isObject(error2.response) && "headers" in error2.response && isObject(error2.response.headers)) {
onResponse(error2.response);
}
throw error2;
}
});
return octokit;
} }
// mcp/comment.ts // mcp/comment.ts
+99 -1
View File
@@ -1,10 +1,24 @@
import { createSign } from "node:crypto"; import { createSign } from "node:crypto";
import { rename, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { throttling } from "@octokit/plugin-throttling"; import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { apiFetch } from "./apiFetch.ts"; import { apiFetch } from "./apiFetch.ts";
import { retry } from "./retry.ts"; import { retry } from "./retry.ts";
function isObject(value: unknown) {
return typeof value === "object" && value !== null;
}
// we don't get access to the actual class from @octokit/rest
// it's reachable from @octokit/request-error but we'd have to add a dependency on it
// and it would pose a risk of accidentally pulling a different version of that class (node_modules dep graphs ❤️)
// so it's safer to ducktype this
interface OctokitResponseShim {
headers: Record<string, string | number | undefined>;
}
export interface InstallationToken { export interface InstallationToken {
token: string; token: string;
expires_at: string; expires_at: string;
@@ -339,11 +353,55 @@ export type OctokitWithPlugins = InstanceType<
ReturnType<typeof Octokit.plugin<typeof Octokit, [typeof throttling]>> ReturnType<typeof Octokit.plugin<typeof Octokit, [typeof throttling]>>
>; >;
export interface ResourceUsage {
requestCount: number;
rateLimitRemaining: number | null;
rateLimitResetMs: number | null;
}
function emptyResourceUsage(): ResourceUsage {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null,
};
}
const usageByResource: Record<string, ResourceUsage> = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage(),
};
export interface UsageSummary {
version: 1;
github: {
core: ResourceUsage;
graphql: ResourceUsage;
};
}
function getGitHubUsageSummary(): UsageSummary {
return {
version: 1,
github: {
core: usageByResource.core,
graphql: usageByResource.graphql,
},
};
}
export async function writeGitHubUsageSummaryToFile(path: string): Promise<void> {
const summary = getGitHubUsageSummary();
const tmpPath = join(dirname(path), `.usage-summary-${process.pid}.tmp`);
await writeFile(tmpPath, JSON.stringify(summary));
await rename(tmpPath, path);
}
export function createOctokit(token: string): OctokitWithPlugins { export function createOctokit(token: string): OctokitWithPlugins {
// `OctokitWithPlugins` initialization based on https://github.com/actions/toolkit/blob/2506e78e82fbd2f9e94d63e75f5309118c8de1b1/packages/github/src/github.ts#L15-L22 // `OctokitWithPlugins` initialization based on https://github.com/actions/toolkit/blob/2506e78e82fbd2f9e94d63e75f5309118c8de1b1/packages/github/src/github.ts#L15-L22
// we can't use it directly because it's stuck on `@octokit/core@v5` and we use the hottest `@octokit/core@v7` // we can't use it directly because it's stuck on `@octokit/core@v5` and we use the hottest `@octokit/core@v7`
const OctokitWithPlugins = Octokit.plugin(throttling); const OctokitWithPlugins = Octokit.plugin(throttling);
return new OctokitWithPlugins({ const octokit = new OctokitWithPlugins({
auth: token, auth: token,
throttle: { throttle: {
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => { onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
@@ -354,4 +412,44 @@ export function createOctokit(token: string): OctokitWithPlugins {
}, },
}, },
}); });
const onResponse = (response: OctokitResponseShim) => {
const resource = response.headers["x-ratelimit-resource"];
if (!resource) {
return response;
}
usageByResource[resource] ??= emptyResourceUsage();
const usage = usageByResource[resource];
usage.requestCount++;
const remaining = response.headers["x-ratelimit-remaining"];
const reset = response.headers["x-ratelimit-reset"];
if (remaining !== undefined) {
usage.rateLimitRemaining = Number(remaining);
}
if (reset !== undefined) {
usage.rateLimitResetMs = Number(reset) * 1000;
}
return response;
};
octokit.hook.wrap("request", async (request, options) => {
try {
const response = await request(options);
onResponse(response);
return response;
} catch (error) {
if (
isObject(error) &&
"response" in error &&
isObject(error.response) &&
"headers" in error.response &&
isObject(error.response.headers)
) {
onResponse(error.response as OctokitResponseShim);
}
throw error;
}
});
return octokit;
} }