diff --git a/entry b/entry index ef086f9..b099916 100755 --- a/entry +++ b/entry @@ -18198,7 +18198,7 @@ var require_summary = __commonJS({ exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; var os_1 = __require("os"); 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_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary"; var Summary = class { @@ -18256,7 +18256,7 @@ var require_summary = __commonJS({ return __awaiter(this, void 0, void 0, function* () { const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); const filePath = yield this.filePath(); - const writeFunc = overwrite ? writeFile : appendFile; + const writeFunc = overwrite ? writeFile2 : appendFile; yield writeFunc(filePath, this._buffer, { encoding: "utf8" }); return this.emptyBuffer(); }); @@ -26579,13 +26579,13 @@ var init_schemas = __esm({ } return propValues; }); - const isObject4 = isObject; + const isObject5 = isObject; const catchall = def.catchall; let value2; inst._zod.parse = (payload, ctx) => { value2 ?? (value2 = _normalized.value); const input = payload.value; - if (!isObject4(input)) { + if (!isObject5(input)) { payload.issues.push({ expected: "object", code: "invalid_type", @@ -26683,7 +26683,7 @@ var init_schemas = __esm({ return (payload, ctx) => fn2(shape, payload, ctx); }; let fastpass; - const isObject4 = isObject; + const isObject5 = isObject; const jit = !globalConfig.jitless; const allowsEval3 = allowsEval; const fastEnabled = jit && allowsEval3.value; @@ -26692,7 +26692,7 @@ var init_schemas = __esm({ inst._zod.parse = (payload, ctx) => { value2 ?? (value2 = _normalized.value); const input = payload.value; - if (!isObject4(input)) { + if (!isObject5(input)) { payload.issues.push({ expected: "object", code: "invalid_type", @@ -56273,8 +56273,8 @@ var require_snapshot_utils = __commonJS({ var require_snapshot_recorder = __commonJS({ "node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) { "use strict"; - var { writeFile, readFile, mkdir } = __require("node:fs/promises"); - var { dirname: dirname3, resolve: resolve3 } = __require("node:path"); + var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises"); + var { dirname: dirname4, resolve: resolve3 } = __require("node:path"); var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers"); var { InvalidArgumentError, UndiciError } = require_errors4(); var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils(); @@ -56498,12 +56498,12 @@ var require_snapshot_recorder = __commonJS({ throw new InvalidArgumentError("Snapshot path is required"); } 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]) => ({ hash: hash2, 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 @@ -72140,7 +72140,7 @@ var require_lodash = __commonJS({ var symbolProto = Symbol2 ? Symbol2.prototype : void 0; var symbolToString = symbolProto ? symbolProto.toString : void 0; function baseIsRegExp(value2) { - return isObject4(value2) && objectToString.call(value2) == regexpTag; + return isObject5(value2) && objectToString.call(value2) == regexpTag; } function baseSlice(array3, start, end) { var index = -1, length = array3.length; @@ -72174,7 +72174,7 @@ var require_lodash = __commonJS({ end = end === void 0 ? length : end; return !start && end >= length ? array3 : baseSlice(array3, start, end); } - function isObject4(value2) { + function isObject5(value2) { var type2 = typeof value2; return !!value2 && (type2 == "object" || type2 == "function"); } @@ -72207,9 +72207,9 @@ var require_lodash = __commonJS({ if (isSymbol(value2)) { return NAN; } - if (isObject4(value2)) { + if (isObject5(value2)) { var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2; - value2 = isObject4(other) ? other + "" : other; + value2 = isObject5(other) ? other + "" : other; } if (typeof value2 != "string") { return value2 === 0 ? value2 : +value2; @@ -72223,7 +72223,7 @@ var require_lodash = __commonJS({ } function truncate(string6, options) { var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION; - if (isObject4(options)) { + if (isObject5(options)) { var separator2 = "separator" in options ? options.separator : separator2; length = "length" in options ? toInteger(options.length) : length; omission = "omission" in options ? baseToString2(options.omission) : omission; @@ -95647,14 +95647,14 @@ var require_turndown_cjs = __commonJS({ } else if (node2.nodeType === 1) { replacement = replacementForNode.call(self2, node2); } - return join17(output, replacement); + return join18(output, replacement); }, ""); } function postProcess(output) { var self2 = this; this.rules.forEach(function(rule) { 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]+$/, ""); @@ -95666,7 +95666,7 @@ var require_turndown_cjs = __commonJS({ if (whitespace.leading || whitespace.trailing) content = content.trim(); return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing; } - function join17(output, replacement) { + function join18(output, replacement) { var s1 = trimTrailingNewlines(output); var s2 = trimLeadingNewlines(replacement); var nls = Math.max(output.length - s1.length, replacement.length - s2.length); @@ -97612,7 +97612,7 @@ var require_semver2 = __commonJS({ // entry.ts 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 var liftArray = (data) => Array.isArray(data) ? data : [data]; @@ -136544,7 +136544,7 @@ function AskQuestionTool(ctx) { // mcp/checkout.ts import { writeFileSync as writeFileSync2 } from "node:fs"; -import { join as join2 } from "node:path"; +import { join as join3 } from "node:path"; // utils/gitAuth.ts import { execSync as execSync2, spawnSync } from "node:child_process"; @@ -136583,6 +136583,8 @@ function exitWithSignal(signal) { // utils/github.ts var core2 = __toESM(require_core(), 1); 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 var import_light = __toESM(require_light(), 1); @@ -140291,6 +140293,9 @@ async function retry(fn2, options = {}) { } // utils/github.ts +function isObject4(value2) { + return typeof value2 === "object" && value2 !== null; +} function isOIDCAvailable() { return Boolean( process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN @@ -140451,9 +140456,35 @@ function parseRepoContext() { } 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) { const OctokitWithPlugins = Octokit2.plugin(throttling); - return new OctokitWithPlugins({ + const octokit = new OctokitWithPlugins({ auth: token, throttle: { 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 @@ -141081,7 +141143,7 @@ ${diffPreview}`); "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); log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`); return { @@ -141105,7 +141167,7 @@ ${diffPreview}`); // mcp/checkSuite.ts 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({ check_suite_id: type.number.describe("the id from check_suite.id") }); @@ -141200,7 +141262,7 @@ function GetCheckSuiteLogsTool(ctx) { if (!tempDir) { throw new Error("PULLFROG_TEMP_DIR not set"); } - const logsDir = join3(tempDir, "ci-logs"); + const logsDir = join4(tempDir, "ci-logs"); mkdirSync2(logsDir, { recursive: true }); const jobResults = []; for (const run2 of failedRuns) { @@ -141219,7 +141281,7 @@ function GetCheckSuiteLogsTool(ctx) { }); const logsUrl = logsResponse.url; 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); const analysis = analyzeLog(logsText, 80); 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 import { writeFileSync as writeFileSync4 } from "node:fs"; -import { join as join4 } from "node:path"; +import { join as join5 } from "node:path"; var CommitInfo = type({ 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" ); } - const diffFile = join4(tempDir, `commit-${sha.slice(0, 7)}.diff`); + const diffFile = join5(tempDir, `commit-${sha.slice(0, 7)}.diff`); writeFileSync4(diffFile, formatResult.content); log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`); return { @@ -141688,7 +141750,7 @@ import { performance as performance4 } from "node:perf_hooks"; // prep/installNodeDependencies.ts 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 function dashDashArg(agent2, agentCommand) { @@ -142000,7 +142062,7 @@ async function isCommandAvailable(command) { return result.exitCode === 0; } function getPackageManagerFromPackageJson() { - const packageJsonPath = join5(process.cwd(), "package.json"); + const packageJsonPath = join6(process.cwd(), "package.json"); try { const content = readFileSync2(packageJsonPath, "utf-8"); const pkg = JSON.parse(content); @@ -142031,7 +142093,7 @@ async function installPackageManager(name, installSpec) { return result.stderr || `failed to install ${name}`; } 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}`; } log.info(`\xBB installed ${name}`); @@ -142040,7 +142102,7 @@ async function installPackageManager(name, installSpec) { var installNodeDependencies = { name: "installNodeDependencies", shouldRun: () => { - const packageJsonPath = join5(process.cwd(), "package.json"); + const packageJsonPath = join6(process.cwd(), "package.json"); return existsSync2(packageJsonPath); }, run: async (options) => { @@ -142125,7 +142187,7 @@ ${errorMessage}`] // prep/installPythonDependencies.ts import { existsSync as existsSync3 } from "node:fs"; -import { join as join6 } from "node:path"; +import { join as join7 } from "node:path"; var PYTHON_CONFIGS = [ { file: "requirements.txt", @@ -142197,11 +142259,11 @@ var installPythonDependencies = { return false; } 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) => { 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) { return { language: "python", @@ -142446,7 +142508,7 @@ import { unlinkSync, writeFileSync as writeFileSync5 } 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({ path: "string", "offset?": "number", @@ -142478,7 +142540,7 @@ function resolveReadPath(filePath) { } const home = process.env.HOME; if (home) { - const cursorProjectsDir = join7(home, ".cursor", "projects"); + const cursorProjectsDir = join8(home, ".cursor", "projects"); if (resolved.startsWith(cursorProjectsDir + "/")) { return resolved; } @@ -142505,9 +142567,9 @@ function resolveWritePath(filePath, shellPermission) { throw new Error(`path must be within the repository (symlink escape blocked): ${filePath}`); } } else { - let ancestor = dirname(resolved); + let ancestor = dirname2(resolved); while (!existsSync4(ancestor)) { - const parent = dirname(ancestor); + const parent = dirname2(ancestor); if (parent === ancestor) break; ancestor = parent; } @@ -142566,7 +142628,7 @@ function FileWriteTool(ctx) { parameters: FileWriteParams, execute: execute(async (params) => { const resolved = resolveWritePath(params.path, ctx.payload.shell); - const dir = dirname(resolved); + const dir = dirname2(resolved); mkdirSync3(dir, { recursive: true }); writeFileSync5(resolved, params.content, "utf-8"); return { path: params.path, written: true }; @@ -143411,7 +143473,7 @@ async function reportReviewNodeId(ctx, reviewNodeId) { // mcp/reviewComments.ts 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 = ` query ($owner: String!, $name: String!, $prNumber: Int!) { repository(owner: $owner, name: $name) { @@ -143781,7 +143843,7 @@ function GetReviewCommentsTool(ctx) { throw new Error("PULLFROG_TEMP_DIR not set"); } const filename = `review-${params.review_id}-threads.md`; - const commentsPath = join8(tempDir, filename); + const commentsPath = join9(tempDir, filename); writeFileSync6(commentsPath, formatted.content); log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`); return { @@ -144073,7 +144135,7 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process"; import { randomUUID as randomUUID3 } from "node:crypto"; import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs"; import { userInfo } from "node:os"; -import { join as join9 } from "node:path"; +import { join as join10 } from "node:path"; var ShellParams = type({ command: "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) { const tempDir = getTempDir(); const handle = `bg-${randomUUID3().slice(0, 8)}`; - const outputPath = join9(tempDir, `${handle}.log`); - const pidPath = join9(tempDir, `${handle}.pid`); + const outputPath = join10(tempDir, `${handle}.log`); + const pidPath = join10(tempDir, `${handle}.pid`); const logFd = openSync(outputPath, "a"); let proc2; try { @@ -144834,7 +144896,7 @@ var modes = computeModes(); // agents/claude.ts 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 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 { mkdtemp } from "node:fs/promises"; 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"; async function installFromNpmTarball(params) { const tempDir = process.env.PULLFROG_TEMP_DIR; if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set"); - const extractedDir = join10(tempDir, "package"); - const cliPath = join10(extractedDir, params.executablePath); + const extractedDir = join11(tempDir, "package"); + const cliPath = join11(extractedDir, params.executablePath); if (existsSync5(cliPath)) { log.debug(`\xBB using cached binary at ${cliPath}`); return cliPath; @@ -144963,7 +145025,7 @@ async function installFromNpmTarball(params) { } } 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"; let tarballUrl; if (params.packageName.startsWith("@")) { @@ -145037,8 +145099,8 @@ async function fetchWithRetry(url4, headers, errorMessage) { } async function installFromGithub(params) { 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 expectedCliPath = join10(installDir, params.executablePath ?? params.assetName ?? "asset"); + const installDir = pullfrogTemp ? join11(pullfrogTemp, `github-${params.owner}-${params.repo}`) : await mkdtemp(join11(tmpdir(), `${params.owner}-${params.repo}-github-`)); + const expectedCliPath = join11(installDir, params.executablePath ?? params.assetName ?? "asset"); if (existsSync5(expectedCliPath)) { log.debug(`\xBB using cached binary at ${expectedCliPath}`); return expectedCliPath; @@ -145062,13 +145124,13 @@ async function installFromGithub(params) { mkdirSync4(installDir, { recursive: true }); const urlPath = new URL(assetUrl).pathname; 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"); if (!assetResponse.body) throw new Error("Response body is null"); const fileStream = createWriteStream(downloadPath); await pipeline(assetResponse.body, fileStream); 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)) { throw new Error(`Executable not found at ${cliPath}`); } @@ -145079,14 +145141,14 @@ async function installFromGithub(params) { async function installFromDirectTarball(params) { const tempDir = process.env.PULLFROG_TEMP_DIR; if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set"); - const extractDir = join10(tempDir, "direct-package"); - const cliPath = join10(extractDir, params.executablePath); + const extractDir = join11(tempDir, "direct-package"); + const cliPath = join11(extractDir, params.executablePath); if (existsSync5(cliPath)) { log.debug(`\xBB using cached binary at ${cliPath}`); return cliPath; } 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"); if (!response.body) throw new Error("response body is null"); const fileStream = createWriteStream(tarballPath); @@ -145198,9 +145260,9 @@ function buildDisallowedTools(ctx) { return disallowed; } function writeMcpConfig(ctx) { - const configDir = join11(ctx.tmpdir, ".claude"); + const configDir = join12(ctx.tmpdir, ".claude"); mkdirSync5(configDir, { recursive: true }); - const configPath = join11(configDir, "mcp.json"); + const configPath = join12(configDir, "mcp.json"); const mcpConfig = { mcpServers: { [ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl } @@ -145417,7 +145479,7 @@ var messageHandlers = { // agents/codex.ts 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 PREFERRED_MODEL = "gpt-5.3-codex"; var FALLBACK_MODEL = "gpt-5.2-codex"; @@ -145457,9 +145519,9 @@ async function resolveModel(apiKey) { return FALLBACK_MODEL; } function writeCodexConfig(ctx) { - const codexDir = join12(ctx.tmpdir, ".codex"); + const codexDir = join13(ctx.tmpdir, ".codex"); 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}`); const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}] url = "${ctx.mcpServerUrl}"`]; @@ -145711,7 +145773,7 @@ function createMessageHandlers() { 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 { 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"; var CURSOR_CLI_VERSION = "2026.01.28-fd13201"; var cursorEffortModels = { @@ -145741,7 +145803,7 @@ var cursor = agent({ const cliPath = await installCursor(); configureCursorMcpServers(ctx); configureCursorTools(ctx); - const projectCliConfigPath = join13(process.cwd(), ".cursor", "cli.json"); + const projectCliConfigPath = join14(process.cwd(), ".cursor", "cli.json"); let modelOverride = null; if (existsSync6(projectCliConfigPath)) { try { @@ -145926,11 +145988,11 @@ var cursor = agent({ } }); function getCursorConfigDir() { - return join13(homedir(), ".cursor"); + return join14(homedir(), ".cursor"); } function configureCursorMcpServers(ctx) { const cursorConfigDir = getCursorConfigDir(); - const mcpConfigPath = join13(cursorConfigDir, "mcp.json"); + const mcpConfigPath = join14(cursorConfigDir, "mcp.json"); mkdirSync7(cursorConfigDir, { recursive: true }); const mcpServers = { [ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl } @@ -145940,7 +146002,7 @@ function configureCursorMcpServers(ctx) { } function configureCursorTools(ctx) { const cursorConfigDir = getCursorConfigDir(); - const cliConfigPath = join13(cursorConfigDir, "cli-config.json"); + const cliConfigPath = join14(cursorConfigDir, "cli-config.json"); mkdirSync7(cursorConfigDir, { recursive: true }); const shell = ctx.payload.shell; const deny = []; @@ -145969,7 +146031,7 @@ function configureCursorTools(ctx) { // agents/gemini.ts import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync11 } from "node:fs"; import { homedir as homedir2 } from "node:os"; -import { join as join14 } from "node:path"; +import { join as join15 } from "node:path"; var geminiEffortConfig = { // https://ai.google.dev/gemini-api/docs/models // 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; log.info(`\xBB model: ${model} (thinkingLevel: ${thinkingLevel})`); const realHome = homedir2(); - const geminiConfigDir = join14(realHome, ".gemini"); - const settingsPath = join14(geminiConfigDir, "settings.json"); + const geminiConfigDir = join15(realHome, ".gemini"); + const settingsPath = join15(geminiConfigDir, "settings.json"); mkdirSync8(geminiConfigDir, { recursive: true }); let existingSettings = {}; try { @@ -146241,7 +146303,7 @@ function configureGeminiSettings(ctx) { // agents/opencode.ts 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"; var OPENCODE_CLI_VERSION = "1.1.56"; var PROVIDER_ERROR_PATTERNS = [ @@ -146370,7 +146432,7 @@ var opencode = agent({ run: async (ctx) => { const cliPath = await installOpencode(); const tempHome = ctx.tmpdir; - const configDir = join15(tempHome, ".config", "opencode"); + const configDir = join16(tempHome, ".config", "opencode"); mkdirSync9(configDir, { recursive: true }); configureOpenCode(ctx); const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"]; @@ -146388,7 +146450,7 @@ var opencode = agent({ const env2 = { ...process.env, 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) 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) { - const configDir = join15(ctx.tmpdir, ".config", "opencode"); + const configDir = join16(ctx.tmpdir, ".config", "opencode"); mkdirSync9(configDir, { recursive: true }); - const configPath = join15(configDir, "opencode.json"); - const repoConfigPath = join15(process.cwd(), "opencode.json"); + const configPath = join16(configDir, "opencode.json"); + const repoConfigPath = join16(process.cwd(), "opencode.json"); const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath }); if (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 { mkdtempSync } from "node:fs"; import { tmpdir as tmpdir2 } from "node:os"; -import { join as join16 } from "node:path"; +import { join as join17 } from "node:path"; function createTempDirectory() { - const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-")); + const sharedTempDir = mkdtempSync(join17(tmpdir2(), "pullfrog-")); process.env.PULLFROG_TEMP_DIR = sharedTempDir; log.info(`\xBB created temp dir at ${sharedTempDir}`); return sharedTempDir; @@ -147768,6 +147830,10 @@ async function main() { var _stack2 = []; try { normalizeEnv(); + const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH; + if (usageSummaryPath) { + onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath)); + } const timer = new Timer(); let activityTimeout = null; const resolvedPromptInput = resolvePromptInput(); @@ -147944,6 +148010,9 @@ ${instructions.user}` : null, }; } finally { activityTimeout?.stop(); + if (usageSummaryPath) { + await writeGitHubUsageSummaryToFile(usageSummaryPath); + } } } catch (_2) { var _error2 = _2, _hasError2 = true; @@ -147954,7 +148023,7 @@ ${instructions.user}` : null, } // entry.ts -process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`; +process.env.PATH = `${dirname3(process.execPath)}:${process.env.PATH}`; async function run() { try { const result = await main(); diff --git a/get-installation-token/entry b/get-installation-token/entry index a6c6070..9386ce5 100755 --- a/get-installation-token/entry +++ b/get-installation-token/entry @@ -25699,8 +25699,8 @@ function formatJsonValue(value) { } // utils/github.ts -var core2 = __toESM(require_core(), 1); import { createSign } from "node:crypto"; +var core2 = __toESM(require_core(), 1); // utils/apiUrl.ts function isLocalUrl(url) { @@ -25930,6 +25930,17 @@ function parseRepoContext() { } return { owner, name }; } +function emptyResourceUsage() { + return { + requestCount: 0, + rateLimitRemaining: null, + rateLimitResetMs: null + }; +} +var usageByResource = { + core: emptyResourceUsage(), + graphql: emptyResourceUsage() +}; // utils/token.ts async function revokeGitHubInstallationToken(token) { diff --git a/internal/index.ts b/internal/index.ts index 91ae3de..3339673 100644 --- a/internal/index.ts +++ b/internal/index.ts @@ -30,7 +30,7 @@ export { PULLFROG_DIVIDER, stripExistingFooter, } from "../utils/buildPullfrogFooter.ts"; - +export type { ResourceUsage, UsageSummary } from "../utils/github.ts"; export { isValidTimeString, parseTimeString, diff --git a/main.ts b/main.ts index a2b3fbe..1d45a0e 100644 --- a/main.ts +++ b/main.ts @@ -12,8 +12,9 @@ import { validateAgentApiKey } from "./utils/apiKeys.ts"; import { resolveBody } from "./utils/body.ts"; import { formatUsageSummary, log, writeSummary } from "./utils/cli.ts"; import { reportErrorToComment } from "./utils/errorReport.ts"; +import { onExitSignal } from "./utils/exitHandler.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 { executeLifecycleHook } from "./utils/lifecycle.ts"; import { normalizeEnv } from "./utils/normalizeEnv.ts"; @@ -48,6 +49,12 @@ export async function main(): Promise { // normalize env var names to uppercase (handles case-insensitive workflow files) 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(); let activityTimeout: ActivityTimeout | null = null; @@ -267,5 +274,8 @@ export async function main(): Promise { }; } finally { activityTimeout?.stop(); + if (usageSummaryPath) { + await writeGitHubUsageSummaryToFile(usageSummaryPath); + } } } diff --git a/post b/post index 170f80e..b0a0062 100755 --- a/post +++ b/post @@ -24695,7 +24695,7 @@ var require_lodash = __commonJS({ var symbolProto = Symbol2 ? Symbol2.prototype : void 0; var symbolToString = symbolProto ? symbolProto.toString : void 0; function baseIsRegExp(value2) { - return isObject(value2) && objectToString.call(value2) == regexpTag; + return isObject2(value2) && objectToString.call(value2) == regexpTag; } function baseSlice(array, start, end) { var index = -1, length = array.length; @@ -24729,7 +24729,7 @@ var require_lodash = __commonJS({ end = end === void 0 ? length : end; return !start && end >= length ? array : baseSlice(array, start, end); } - function isObject(value2) { + function isObject2(value2) { var type2 = typeof value2; return !!value2 && (type2 == "object" || type2 == "function"); } @@ -24762,9 +24762,9 @@ var require_lodash = __commonJS({ if (isSymbol(value2)) { return NAN; } - if (isObject(value2)) { + if (isObject2(value2)) { var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2; - value2 = isObject(other) ? other + "" : other; + value2 = isObject2(other) ? other + "" : other; } if (typeof value2 != "string") { return value2 === 0 ? value2 : +value2; @@ -24778,7 +24778,7 @@ var require_lodash = __commonJS({ } function truncate(string2, options) { var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION; - if (isObject(options)) { + if (isObject2(options)) { var separator2 = "separator" in options ? options.separator : separator2; length = "length" in options ? toInteger(options.length) : length; omission = "omission" in options ? baseToString(options.omission) : omission; @@ -41183,6 +41183,9 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes ); // utils/github.ts +function isObject(value2) { + return typeof value2 === "object" && value2 !== null; +} function parseRepoContext() { const githubRepo = process.env.GITHUB_REPOSITORY; if (!githubRepo) { @@ -41194,9 +41197,20 @@ function parseRepoContext() { } return { owner, name }; } +function emptyResourceUsage() { + return { + requestCount: 0, + rateLimitRemaining: null, + rateLimitResetMs: null + }; +} +var usageByResource = { + core: emptyResourceUsage(), + graphql: emptyResourceUsage() +}; function createOctokit(token) { const OctokitWithPlugins = Octokit2.plugin(throttling); - return new OctokitWithPlugins({ + const octokit = new OctokitWithPlugins({ auth: token, throttle: { 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 diff --git a/utils/github.ts b/utils/github.ts index eb46816..a12ba3d 100644 --- a/utils/github.ts +++ b/utils/github.ts @@ -1,10 +1,24 @@ 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 { throttling } from "@octokit/plugin-throttling"; import { Octokit } from "@octokit/rest"; import { apiFetch } from "./apiFetch.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; +} + export interface InstallationToken { token: string; expires_at: string; @@ -339,11 +353,55 @@ export type OctokitWithPlugins = InstanceType< ReturnType> >; +export interface ResourceUsage { + requestCount: number; + rateLimitRemaining: number | null; + rateLimitResetMs: number | null; +} + +function emptyResourceUsage(): ResourceUsage { + return { + requestCount: 0, + rateLimitRemaining: null, + rateLimitResetMs: null, + }; +} + +const usageByResource: Record = { + 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 { + 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 { // `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` const OctokitWithPlugins = Octokit.plugin(throttling); - return new OctokitWithPlugins({ + const octokit = new OctokitWithPlugins({ auth: token, throttle: { 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; }