refactor main

This commit is contained in:
David Blass
2025-12-17 16:20:46 -05:00
parent 1f1c1602c5
commit 90ed2648be
24 changed files with 638 additions and 546 deletions
+238 -195
View File
@@ -80145,7 +80145,7 @@ function query({
// package.json
var package_default = {
name: "@pullfrog/action",
version: "0.0.144",
version: "0.0.146",
type: "module",
files: [
"index.js",
@@ -95158,7 +95158,7 @@ var handleToolError = (error42) => {
isError: true
};
};
var execute = (ctx, fn2) => {
var execute = (fn2) => {
return async (params) => {
try {
const result = await fn2(params);
@@ -95168,9 +95168,6 @@ var execute = (ctx, fn2) => {
}
};
};
function isProgressCommentDisabled(ctx) {
return ctx.payload.disableProgressComment === true;
}
function sanitizeSchema(schema2) {
if (!schema2 || typeof schema2 !== "object") {
return schema2;
@@ -95251,7 +95248,7 @@ function sanitizeTool(tool2) {
};
}
var addTools = (ctx, server, tools) => {
const shouldSanitize = ctx.agentName === "gemini" || ctx.agentName === "opencode";
const shouldSanitize = ctx.agent.name === "gemini" || ctx.agent.name === "opencode";
for (const tool2 of tools) {
const processedTool = shouldSanitize ? sanitizeTool(tool2) : tool2;
server.addTool(processedTool);
@@ -95306,7 +95303,7 @@ function CreateCommentTool(ctx) {
name: "create_issue_comment",
description: "Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.",
parameters: Comment,
execute: execute(ctx, async ({ issueNumber, body }) => {
execute: execute(async ({ issueNumber, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
@@ -95332,7 +95329,7 @@ function EditCommentTool(ctx) {
name: "edit_issue_comment",
description: "Edit a GitHub issue comment by its ID",
parameters: EditComment,
execute: execute(ctx, async ({ commentId, body }) => {
execute: execute(async ({ commentId, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
@@ -95419,7 +95416,7 @@ function ReportProgressTool(ctx) {
name: "report_progress",
description: "Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
parameters: ReportProgress,
execute: execute(ctx, async ({ body }) => {
execute: execute(async ({ body }) => {
const result = await reportProgress(ctx, { body });
if (!result) {
return {
@@ -95513,7 +95510,7 @@ function ReplyToReviewCommentTool(ctx) {
name: "reply_to_review_comment",
description: "Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
parameters: ReplyToReviewComment,
execute: execute(ctx, async ({ pull_number, comment_id, body }) => {
execute: execute(async ({ pull_number, comment_id, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
owner: ctx.owner,
@@ -123902,16 +123899,17 @@ function $(cmd, args3, options) {
var CheckoutPr = type({
pull_number: type.number.describe("the pull request number to checkout")
});
async function checkoutPrBranch(ctx, pull_number) {
log.debug(`\u{1F500} checking out PR #${pull_number}...`);
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number
async function checkoutPrBranch(params) {
const { octokit, owner, name, token, pullNumber } = params;
log.info(`\u{1F500} checking out PR #${pullNumber}...`);
const pr = await octokit.rest.pulls.get({
owner,
repo: name,
pull_number: pullNumber
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${pull_number} source repository was deleted`);
throw new Error(`PR #${pullNumber} source repository was deleted`);
}
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
const baseBranch = pr.data.base.ref;
@@ -123924,18 +123922,18 @@ async function checkoutPrBranch(ctx, pull_number) {
log.debug(`\u{1F4E5} fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
log.debug(`\u{1F33F} fetching PR #${pull_number} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
log.debug(`\u{1F33F} fetching PR #${pullNumber} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pullNumber}/head:${headBranch}`]);
$("git", ["checkout", headBranch]);
log.debug(`\u2713 checked out PR #${pull_number}`);
log.debug(`\u2713 checked out PR #${pullNumber}`);
}
if (alreadyOnBranch) {
log.debug(`\u{1F4E5} fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
}
if (isFork) {
const remoteName = `pr-${pull_number}`;
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
const remoteName = `pr-${pullNumber}`;
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
try {
$("git", ["remote", "add", remoteName, forkUrl]);
log.debug(`\u{1F4CC} added remote '${remoteName}' for fork ${headRepo.full_name}`);
@@ -123953,15 +123951,22 @@ async function checkoutPrBranch(ctx, pull_number) {
} else {
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
}
ctx.toolState.prNumber = pull_number;
return { prNumber: pullNumber };
}
function CheckoutPrTool(ctx) {
return tool({
name: "checkout_pr",
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
parameters: CheckoutPr,
execute: execute(ctx, async ({ pull_number }) => {
await checkoutPrBranch(ctx, pull_number);
execute: execute(async ({ pull_number }) => {
const result = await checkoutPrBranch({
octokit: ctx.octokit,
owner: ctx.owner,
name: ctx.name,
token: ctx.githubInstallationToken,
pullNumber: pull_number
});
ctx.toolState.prNumber = result.prNumber;
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
@@ -123995,7 +124000,7 @@ function GetCheckSuiteLogsTool(ctx) {
name: "get_check_suite_logs",
description: "get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
parameters: GetCheckSuiteLogs,
execute: execute(ctx, async ({ check_suite_id }) => {
execute: execute(async ({ check_suite_id }) => {
const workflowRuns = await ctx.octokit.paginate(
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
{
@@ -124076,7 +124081,7 @@ function DebugShellCommandTool(_ctx) {
name: "debug_shell_command",
description: "debug tool: runs 'git status' and returns the output. use this to test shell command execution in the MCP server.",
parameters: DebugShellCommand,
execute: execute(_ctx, async () => {
execute: execute(async () => {
const result = $("git", ["status"]);
return {
success: true,
@@ -124097,7 +124102,7 @@ function ListFilesTool(_ctx) {
name: "list_files",
description: "List files in the repository, including both git-tracked and untracked files. Useful for discovering the file structure and locating files, including newly created files that haven't been committed yet.",
parameters: ListFiles,
execute: execute(_ctx, async ({ path: path4 }) => {
execute: execute(async ({ path: path4 }) => {
const pathStr = path4 ?? ".";
const cwd2 = process.cwd();
let gitFiles = [];
@@ -124194,7 +124199,7 @@ function CreateBranchTool(ctx) {
name: "create_branch",
description: "Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
parameters: CreateBranch,
execute: execute(ctx, async ({ branchName, baseBranch }) => {
execute: execute(async ({ branchName, baseBranch }) => {
const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";
if (containsSecrets(branchName)) {
throw new Error(
@@ -124222,12 +124227,12 @@ var CommitFiles = type({
"Array of file paths to commit (relative to repo root). If empty, commits all staged changes."
)
});
function CommitFilesTool(ctx) {
function CommitFilesTool(_ctx) {
return tool({
name: "commit_files",
description: "Stage and commit files with a commit message. If files array is empty, commits all staged changes. The commit will be attributed to the correct bot account.",
parameters: CommitFiles,
execute: execute(ctx, async ({ message, files }) => {
execute: execute(async ({ message, files }) => {
if (containsSecrets(message)) {
throw new Error(
"Commit blocked: secrets detected in commit message. Please remove any sensitive information (API keys, tokens, passwords) before committing."
@@ -124277,7 +124282,7 @@ function PushBranchTool(_ctx) {
name: "push_branch",
description: "Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
parameters: PushBranch,
execute: execute(_ctx, async ({ branchName, force }) => {
execute: execute(async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
let remote = "origin";
try {
@@ -124313,7 +124318,7 @@ function IssueTool(ctx) {
name: "create_issue",
description: "Create a new GitHub issue",
parameters: Issue,
execute: execute(ctx, async ({ title, body, labels, assignees }) => {
execute: execute(async ({ title, body, labels, assignees }) => {
const result = await ctx.octokit.rest.issues.create({
owner: ctx.owner,
repo: ctx.name,
@@ -124329,7 +124334,9 @@ function IssueTool(ctx) {
url: result.data.html_url,
title: result.data.title,
state: result.data.state,
labels: result.data.labels?.map((label) => typeof label === "string" ? label : label.name),
labels: result.data.labels?.map(
(label) => typeof label === "string" ? label : label.name
),
assignees: result.data.assignees?.map((assignee) => assignee.login)
};
})
@@ -124345,7 +124352,7 @@ function GetIssueCommentsTool(ctx) {
name: "get_issue_comments",
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
parameters: GetIssueComments,
execute: execute(ctx, async ({ issue_number }) => {
execute: execute(async ({ issue_number }) => {
ctx.toolState.issueNumber = issue_number;
const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
owner: ctx.owner,
@@ -124379,7 +124386,7 @@ function GetIssueEventsTool(ctx) {
name: "get_issue_events",
description: "Get timeline events for a GitHub issue that aren't reflected in the current state. Returns cross-references to other issues/PRs and commit references. Note: current labels, assignees, state, and milestone are already available via get_issue.",
parameters: GetIssueEvents,
execute: execute(ctx, async ({ issue_number }) => {
execute: execute(async ({ issue_number }) => {
ctx.toolState.issueNumber = issue_number;
const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
owner: ctx.owner,
@@ -124451,7 +124458,7 @@ function IssueInfoTool(ctx) {
name: "get_issue",
description: "Retrieve GitHub issue information by issue number",
parameters: IssueInfo,
execute: execute(ctx, async ({ issue_number }) => {
execute: execute(async ({ issue_number }) => {
const issue3 = await ctx.octokit.rest.issues.get({
owner: ctx.owner,
repo: ctx.name,
@@ -124503,7 +124510,7 @@ function AddLabelsTool(ctx) {
name: "add_labels",
description: "Add labels to a GitHub issue or pull request. Only use labels that already exist in the repository.",
parameters: AddLabelsParams,
execute: execute(ctx, async ({ issue_number, labels }) => {
execute: execute(async ({ issue_number, labels }) => {
const result = await ctx.octokit.rest.issues.addLabels({
owner: ctx.owner,
repo: ctx.name,
@@ -124540,7 +124547,7 @@ function PullRequestTool(ctx) {
name: "create_pull_request",
description: "Create a pull request from the current branch",
parameters: PullRequest,
execute: execute(ctx, async ({ title, body, base }) => {
execute: execute(async ({ title, body, base }) => {
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
log.debug(`Current branch: ${currentBranch}`);
if (containsSecrets(title) || containsSecrets(body)) {
@@ -124585,7 +124592,7 @@ function PullRequestInfoTool(ctx) {
name: "get_pull_request",
description: "Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
parameters: PullRequestInfo,
execute: execute(ctx, async ({ pull_number }) => {
execute: execute(async ({ pull_number }) => {
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
@@ -124648,7 +124655,7 @@ function StartReviewTool(ctx) {
name: "start_review",
description: "Start a new review session for a pull request. Creates a scratchpad file for gathering thoughts and a pending review on GitHub. Must be called before add_review_comment.",
parameters: StartReview,
execute: execute(ctx, async ({ pull_number }) => {
execute: execute(async ({ pull_number }) => {
if (ctx.toolState.review) {
throw new Error(
`Review session already in progress. Call submit_review first to finish it.`
@@ -124723,7 +124730,7 @@ function AddReviewCommentTool(ctx) {
name: "add_review_comment",
description: "Add a comment to the current review session. Must call start_review first. Comments are stored in draft state until submit_review is called.",
parameters: AddReviewComment,
execute: execute(ctx, async ({ path: path4, line, body, side }) => {
execute: execute(async ({ path: path4, line, body, side }) => {
if (!ctx.toolState.review) {
throw new Error("No review session started. Call start_review first.");
}
@@ -124754,7 +124761,7 @@ function SubmitReviewTool(ctx) {
name: "submit_review",
description: "Submit the current review session. All comments added via add_review_comment will be published. Must call start_review first.",
parameters: SubmitReview,
execute: execute(ctx, async ({ body }) => {
execute: execute(async ({ body }) => {
if (!ctx.toolState.review) {
throw new Error("No review session started. Call start_review first.");
}
@@ -124860,7 +124867,7 @@ function GetReviewCommentsTool(ctx) {
name: "get_review_comments",
description: "Get all review comments and their replies for a specific pull request review. Returns line-by-line comments that were left on specific code locations, including any threaded replies.",
parameters: GetReviewComments,
execute: execute(ctx, async ({ pull_number, review_id }) => {
execute: execute(async ({ pull_number, review_id }) => {
const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, {
owner: ctx.owner,
repo: ctx.name,
@@ -124930,7 +124937,7 @@ function ListPullRequestReviewsTool(ctx) {
name: "list_pull_request_reviews",
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
parameters: ListPullRequestReviews,
execute: execute(ctx, async ({ pull_number }) => {
execute: execute(async ({ pull_number }) => {
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
owner: ctx.owner,
repo: ctx.name,
@@ -124964,7 +124971,7 @@ function SelectModeTool(ctx) {
name: "select_mode",
description: "Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
parameters: SelectMode,
execute: execute(ctx, async ({ modeName }) => {
execute: execute(async ({ modeName }) => {
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (!selectedMode) {
const availableModes = ctx.modes.map((m) => m.name).join(", ");
@@ -125038,7 +125045,7 @@ async function startMcpHttpServer(ctx) {
PushBranchTool(ctx),
ListFilesTool(ctx)
];
if (!isProgressCommentDisabled(ctx)) {
if (!ctx.payload.disableProgressComment) {
tools.push(ReportProgressTool(ctx));
}
addTools(ctx, server, tools);
@@ -125688,7 +125695,7 @@ function setupGitConfig() {
);
}
}
async function setupGit(ctx) {
async function setupGitAuth(params) {
const repoDir = process.cwd();
log.info("\xBB setting up git authentication...");
try {
@@ -125700,16 +125707,23 @@ async function setupGit(ctx) {
} catch {
log.debug("\xBB no existing authentication headers to remove");
}
if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
const originUrl2 = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
if (params.payload.event.is_pr !== true || !params.payload.event.issue_number) {
const originUrl2 = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl2], { cwd: repoDir });
log.info("\xBB updated origin URL with authentication token");
return;
}
const prNumber = ctx.payload.event.issue_number;
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
const prNumber = params.payload.event.issue_number;
const originUrl = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
await checkoutPrBranch(ctx, prNumber);
const prContext = await checkoutPrBranch({
octokit: params.octokit,
owner: params.owner,
name: params.name,
token: params.token,
pullNumber: prNumber
});
params.toolState.prNumber = prContext.prNumber;
}
// utils/timer.ts
@@ -125745,28 +125759,105 @@ async function main(inputs) {
try {
const timer = new Timer();
payload = parsePayload(inputs);
const partialCtx = await initializeContext(inputs, payload);
const ctx = partialCtx;
ctx.toolState = {};
timer.checkpoint("initializeContext");
await setupGit(ctx);
timer.checkpoint("setupGit");
await setupTempDirectory(ctx);
timer.checkpoint("setupTempDirectory");
const [, prepResults] = await Promise.all([installAgentCli(ctx), runPrepPhase()]);
ctx.prepResults = prepResults;
Inputs.assert(inputs);
setupGitConfig();
const [githubSetup, sharedTempDir] = await Promise.all([
initializeGitHub(),
createTempDirectory()
]);
timer.checkpoint("githubSetup");
const agent2 = resolveAgent({
inputs,
payload,
repoSettings: githubSetup.repoSettings
});
const resolvedPayload = { ...payload, agent: agent2.name };
const apiKeySetup = validateApiKey({
agent: agent2,
inputs,
owner: githubSetup.owner,
name: githubSetup.name
});
if (!apiKeySetup.success) {
await reportErrorToComment({ error: apiKeySetup.error });
return { success: false, error: apiKeySetup.error };
}
const toolState = {};
const [prepResults, cliPath] = await Promise.all([
runPrepPhase(),
installAgentCli({ agent: agent2, token: githubSetup.token }),
setupGitAuth({
token: githubSetup.token,
owner: githubSetup.owner,
name: githubSetup.name,
payload: resolvedPayload,
octokit: githubSetup.octokit,
toolState
})
]);
timer.checkpoint("prep+agentSetup+gitAuth");
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || void 0;
ctx.modes = [
const computedModes = [
...getModes({
disableProgressComment: ctx.payload.disableProgressComment,
disableProgressComment: resolvedPayload.disableProgressComment,
dependenciesPreinstalled
}),
...ctx.payload.modes || []
...resolvedPayload.modes || []
];
timer.checkpoint("installAgentCli+prepPhase");
await startMcpServer(ctx);
mcpServerClose = ctx.mcpServerClose;
timer.checkpoint("startMcpServer");
const runId = process.env.GITHUB_RUN_ID || "";
if (runId) {
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
}
}
let jobId;
const jobName = process.env.GITHUB_JOB;
if (jobName && runId) {
const jobs = await githubSetup.octokit.rest.actions.listJobsForWorkflowRun({
owner: githubSetup.owner,
repo: githubSetup.name,
run_id: parseInt(runId, 10)
});
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
if (matchingJob) {
jobId = String(matchingJob.id);
log.info(`\u{1F4CB} Found job ID: ${jobId}`);
}
}
const toolContext = {
owner: githubSetup.owner,
name: githubSetup.name,
githubInstallationToken: githubSetup.token,
octokit: githubSetup.octokit,
payload: resolvedPayload,
repo: githubSetup.repo,
repoSettings: githubSetup.repoSettings,
modes: computedModes,
prepResults,
toolState,
agent: agent2,
sharedTempDir,
runId,
jobId
};
const { url: mcpServerUrl, close: mcpServerCloseFunc } = await startMcpHttpServer(toolContext);
mcpServerClose = mcpServerCloseFunc;
log.info(`\u{1F680} MCP server started at ${mcpServerUrl}`);
const mcpServers = createMcpConfigs(mcpServerUrl);
log.debug(`\u{1F4CB} MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
timer.checkpoint("mcpServer");
const ctx = {
...toolContext,
inputs,
mcpServerUrl,
mcpServerClose: mcpServerCloseFunc,
mcpServers,
cliPath,
apiKey: apiKeySetup.apiKey,
apiKeys: apiKeySetup.apiKeys
};
if (ctx.payload.event.trigger === "fix_review" && Array.isArray(ctx.payload.event.comment_ids) && ctx.payload.event.comment_ids.length === 0) {
await reportProgress(ctx, {
body: `\u{1F44D} **No approved comments found**
@@ -125775,8 +125866,6 @@ To use "Fix \u{1F44D}s", add a \u{1F44D} reaction to one or more inline review c
});
return { success: true };
}
setupMcpServers(ctx);
await validateApiKey(ctx);
const result = await runAgent(ctx);
const mainResult = await handleAgentResult(result);
return mainResult;
@@ -125803,13 +125892,17 @@ To use "Fix \u{1F44D}s", add a \u{1F44D} reaction to one or more inline review c
await revokeGitHubInstallationToken();
}
}
function agentHasApiKeys(agent2, inputs) {
if (agent2.name === "opencode") {
const hasInputKey = Object.keys(inputs).some((key) => key.includes("api_key"));
if (hasInputKey) return true;
return Object.keys(process.env).some((key) => key.includes("API_KEY") && process.env[key]);
}
const inputsRecord = inputs;
return agent2.apiKeyNames.some((inputKey) => inputsRecord[inputKey]);
}
function getAvailableAgents(inputs) {
return Object.values(agents).filter((agent2) => {
if (agent2.name === "opencode") {
return Object.keys(inputs).some((key) => key.includes("api_key"));
}
return agent2.apiKeyNames.some((inputKey) => inputs[inputKey]);
});
return Object.values(agents).filter((agent2) => agentHasApiKeys(agent2, inputs));
}
function getAllPossibleKeyNames() {
return Object.keys(
@@ -125819,21 +125912,21 @@ function getAllPossibleKeyNames() {
)
);
}
async function throwMissingApiKeyError(ctx) {
function buildMissingApiKeyError(params) {
const apiUrl = process.env.API_URL || "https://pullfrog.com";
const settingsUrl = `${apiUrl}/console/${ctx.owner}/${ctx.name}`;
const githubRepoUrl = `https://github.com/${ctx.owner}/${ctx.name}`;
const settingsUrl = `${apiUrl}/console/${params.owner}/${params.name}`;
const githubRepoUrl = `https://github.com/${params.owner}/${params.name}`;
const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
const isOpenCode = ctx.agent?.name === "opencode";
const isOpenCode = params.agent.name === "opencode";
let secretNameList;
if (isOpenCode) {
secretNameList = "any API key (e.g., `OPENCODE_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)";
} else {
const inputKeys = ctx.agent?.apiKeyNames || getAllPossibleKeyNames();
const inputKeys = params.agent.apiKeyNames.length > 0 ? params.agent.apiKeyNames : getAllPossibleKeyNames();
const secretNames = inputKeys.map((key) => `\`${key.toUpperCase()}\``);
secretNameList = inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`;
}
const message = `Pullfrog is configured to use ${ctx.agent.displayName}, but the associated API key was not provided.
return `Pullfrog is configured to use ${params.agent.displayName}, but the associated API key was not provided.
To fix this, add the required secret to your GitHub repository:
@@ -125844,53 +125937,23 @@ To fix this, add the required secret to your GitHub repository:
5. Click "Add secret"
Alternatively, configure Pullfrog to use a different agent at ${settingsUrl}`;
await reportErrorToComment({ error: message });
throw new Error(message);
}
async function initializeContext(inputs, payload) {
async function initializeGitHub() {
log.info(`\u{1F438} Running pullfrog/action@${package_default.version}...`);
Inputs.assert(inputs);
setupGitConfig();
const githubInstallationToken2 = await setupGitHubInstallationToken();
const token = await setupGitHubInstallationToken();
const { owner, name } = parseRepoContext();
const octokit = new Octokit2({
auth: githubInstallationToken2
});
const response = await octokit.repos.get({
owner,
repo: name
});
const repo = response.data;
const repoSettings = await fetchRepoSettings({
token: githubInstallationToken2,
repoContext: { owner, name }
});
const { agentName, agent: agent2 } = resolveAgent({
inputs,
payload,
repoSettings
});
const resolvedPayload = { ...payload, agent: agentName };
const computedModes = [
...getModes({
disableProgressComment: resolvedPayload.disableProgressComment,
dependenciesPreinstalled: void 0
}),
...resolvedPayload.modes || []
];
const octokit = new Octokit2({ auth: token });
const [repoResponse, repoSettings] = await Promise.all([
octokit.repos.get({ owner, repo: name }),
fetchRepoSettings({ token, repoContext: { owner, name } })
]);
return {
token,
owner,
name,
inputs,
githubInstallationToken: githubInstallationToken2,
octokit,
repo,
agentName,
agent: agent2,
payload: resolvedPayload,
repoSettings,
modes: computedModes,
sharedTempDir: ""
repo: repoResponse.data,
repoSettings
};
}
function resolveAgent({
@@ -125901,41 +125964,37 @@ function resolveAgent({
const agentOverride = process.env.AGENT_OVERRIDE;
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
if (configuredAgentName) {
const agentName2 = configuredAgentName;
const agent3 = agents[agentName2];
const agent3 = agents[configuredAgentName];
if (!agent3) {
throw new Error(`invalid agent name: ${agentName2}`);
throw new Error(`invalid agent name: ${configuredAgentName}`);
}
const isExplicitOverride = agentOverride !== void 0 || payload.agent !== null;
if (isExplicitOverride) {
log.info(`\xBB selected configured agent: ${agentName2}`);
return { agentName: agentName2, agent: agent3 };
log.info(`Selected configured agent: ${agent3.name}`);
return agent3;
}
const hasMatchingKey = agent3.name === "opencode" ? Object.keys(inputs).some((key) => key.includes("api_key")) : agent3.apiKeyNames.some((inputKey) => inputs[inputKey]);
if (!hasMatchingKey) {
log.warning(
`Repo default agent ${agentName2} has no matching API keys. Available agents: ${getAvailableAgents(inputs).map((a) => a.name).join(", ") || "none"}`
);
} else {
log.info(`\xBB selected configured agent: ${agentName2}`);
return { agentName: agentName2, agent: agent3 };
if (agentHasApiKeys(agent3, inputs)) {
log.info(`Selected configured agent: ${agent3.name}`);
return agent3;
}
const availableAgents2 = getAvailableAgents(inputs);
log.warning(
`Repo default agent ${agent3.name} has no matching API keys. Available: ${availableAgents2.map((a) => a.name).join(", ") || "none"}`
);
}
const availableAgents = getAvailableAgents(inputs);
const availableAgentNames = availableAgents.map((agent3) => agent3.name).join(", ");
log.debug(`\xBB available agents: ${availableAgentNames || "none"}`);
if (availableAgents.length === 0) {
throw new Error("no agents available - missing API keys");
}
const agentName = availableAgents[0].name;
const agent2 = availableAgents[0];
log.info(`\xBB no agent configured, defaulting to first available agent: ${agentName}`);
return { agentName, agent: agent2 };
log.info(`No agent configured, defaulting to first available agent: ${agent2.name}`);
return agent2;
}
async function setupTempDirectory(ctx) {
ctx.sharedTempDir = await mkdtemp2(join11(tmpdir2(), "pullfrog-"));
process.env.PULLFROG_TEMP_DIR = ctx.sharedTempDir;
log.debug(`\xBB created temp dir at ${ctx.sharedTempDir}`);
async function createTempDirectory() {
const sharedTempDir = await mkdtemp2(join11(tmpdir2(), "pullfrog-"));
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
log.info(`\u{1F4C2} PULLFROG_TEMP_DIR has been created at ${sharedTempDir}`);
return sharedTempDir;
}
function parsePayload(inputs) {
try {
@@ -125956,71 +126015,55 @@ function parsePayload(inputs) {
};
}
}
async function startMcpServer(ctx) {
const runId = process.env.GITHUB_RUN_ID || "";
ctx.runId = runId;
if (runId) {
const workflowRunInfo = await fetchWorkflowRunInfo(ctx.runId);
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`\xBB using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
}
async function installAgentCli(params) {
if (params.agent.name === "gemini") {
return params.agent.install(params.token);
}
const jobName = process.env.GITHUB_JOB;
if (jobName && runId) {
const jobs = await ctx.octokit.rest.actions.listJobsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
run_id: parseInt(ctx.runId, 10)
});
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
if (matchingJob) {
ctx.jobId = String(matchingJob.id);
log.info(`\xBB found job ID: ${ctx.jobId}`);
}
}
const { url: url2, close } = await startMcpHttpServer(ctx);
ctx.mcpServerUrl = url2;
ctx.mcpServerClose = close;
log.info(`\xBB MCP server started at ${url2}`);
return params.agent.install();
}
function setupMcpServers(ctx) {
ctx.mcpServers = createMcpConfigs(ctx.mcpServerUrl);
log.debug(`\xBB MCP config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
}
async function installAgentCli(ctx) {
if (ctx.agentName === "gemini") {
ctx.cliPath = await ctx.agent.install(ctx.githubInstallationToken);
} else {
ctx.cliPath = await ctx.agent.install();
}
}
async function validateApiKey(ctx) {
function collectApiKeys(agent2, inputs) {
const apiKeys = {};
for (const inputKey of ctx.agent.apiKeyNames) {
const value2 = ctx.inputs[inputKey];
const inputsRecord = inputs;
for (const inputKey of agent2.apiKeyNames) {
const value2 = inputsRecord[inputKey];
if (value2) {
apiKeys[inputKey] = value2;
}
}
if (ctx.agentName === "opencode" && Object.keys(apiKeys).length === 0) {
if (agent2.name === "opencode" && Object.keys(apiKeys).length === 0) {
for (const [key, value2] of Object.entries(process.env)) {
if (value2 && typeof value2 === "string" && key.includes("API_KEY")) {
const inputKey = key.toLowerCase();
apiKeys[inputKey] = value2;
apiKeys[key.toLowerCase()] = value2;
}
}
}
return apiKeys;
}
function validateApiKey(params) {
const apiKeys = collectApiKeys(params.agent, params.inputs);
if (Object.keys(apiKeys).length === 0) {
await throwMissingApiKeyError(ctx);
return;
return {
success: false,
error: buildMissingApiKeyError({
agent: params.agent,
owner: params.owner,
name: params.name
})
};
}
ctx.apiKey = Object.values(apiKeys)[0];
ctx.apiKeys = apiKeys;
return {
success: true,
apiKey: Object.values(apiKeys)[0],
apiKeys
};
}
async function runAgent(ctx) {
log.info(`\xBB running ${ctx.agentName}...`);
log.box(ctx.payload.prompt.trim(), { title: "Prompt" });
log.info(`Running ${ctx.agent.name}...`);
const { context: _context, ...eventWithoutContext } = ctx.payload.event;
const promptContent = `${ctx.payload.prompt}
${encode(eventWithoutContext)}`;
log.box(promptContent, { title: "Prompt" });
return ctx.agent.run({
payload: ctx.payload,
mcpServers: ctx.mcpServers,