refactor main
This commit is contained in:
@@ -6,6 +6,8 @@ import { gemini } from "./gemini.ts";
|
|||||||
import { opencode } from "./opencode.ts";
|
import { opencode } from "./opencode.ts";
|
||||||
import type { Agent } from "./shared.ts";
|
import type { Agent } from "./shared.ts";
|
||||||
|
|
||||||
|
export type { Agent } from "./shared.ts";
|
||||||
|
|
||||||
export const agents = {
|
export const agents = {
|
||||||
claude,
|
claude,
|
||||||
codex,
|
codex,
|
||||||
|
|||||||
+1
-1
@@ -506,7 +506,7 @@ export const agent = <const input extends AgentInput>(input: input): defineAgent
|
|||||||
|
|
||||||
export interface AgentInput {
|
export interface AgentInput {
|
||||||
name: AgentName;
|
name: AgentName;
|
||||||
install: () => Promise<string>;
|
install: (token?: string) => Promise<string>;
|
||||||
run: (config: AgentConfig) => Promise<AgentResult>;
|
run: (config: AgentConfig) => Promise<AgentResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80145,7 +80145,7 @@ function query({
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/action",
|
name: "@pullfrog/action",
|
||||||
version: "0.0.144",
|
version: "0.0.146",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -95158,7 +95158,7 @@ var handleToolError = (error42) => {
|
|||||||
isError: true
|
isError: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
var execute = (ctx, fn2) => {
|
var execute = (fn2) => {
|
||||||
return async (params) => {
|
return async (params) => {
|
||||||
try {
|
try {
|
||||||
const result = await fn2(params);
|
const result = await fn2(params);
|
||||||
@@ -95168,9 +95168,6 @@ var execute = (ctx, fn2) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
function isProgressCommentDisabled(ctx) {
|
|
||||||
return ctx.payload.disableProgressComment === true;
|
|
||||||
}
|
|
||||||
function sanitizeSchema(schema2) {
|
function sanitizeSchema(schema2) {
|
||||||
if (!schema2 || typeof schema2 !== "object") {
|
if (!schema2 || typeof schema2 !== "object") {
|
||||||
return schema2;
|
return schema2;
|
||||||
@@ -95251,7 +95248,7 @@ function sanitizeTool(tool2) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
var addTools = (ctx, server, tools) => {
|
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) {
|
for (const tool2 of tools) {
|
||||||
const processedTool = shouldSanitize ? sanitizeTool(tool2) : tool2;
|
const processedTool = shouldSanitize ? sanitizeTool(tool2) : tool2;
|
||||||
server.addTool(processedTool);
|
server.addTool(processedTool);
|
||||||
@@ -95306,7 +95303,7 @@ function CreateCommentTool(ctx) {
|
|||||||
name: "create_issue_comment",
|
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.",
|
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,
|
parameters: Comment,
|
||||||
execute: execute(ctx, async ({ issueNumber, body }) => {
|
execute: execute(async ({ issueNumber, body }) => {
|
||||||
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
||||||
const result = await ctx.octokit.rest.issues.createComment({
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -95332,7 +95329,7 @@ function EditCommentTool(ctx) {
|
|||||||
name: "edit_issue_comment",
|
name: "edit_issue_comment",
|
||||||
description: "Edit a GitHub issue comment by its ID",
|
description: "Edit a GitHub issue comment by its ID",
|
||||||
parameters: EditComment,
|
parameters: EditComment,
|
||||||
execute: execute(ctx, async ({ commentId, body }) => {
|
execute: execute(async ({ commentId, body }) => {
|
||||||
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
||||||
const result = await ctx.octokit.rest.issues.updateComment({
|
const result = await ctx.octokit.rest.issues.updateComment({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -95419,7 +95416,7 @@ function ReportProgressTool(ctx) {
|
|||||||
name: "report_progress",
|
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.",
|
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,
|
parameters: ReportProgress,
|
||||||
execute: execute(ctx, async ({ body }) => {
|
execute: execute(async ({ body }) => {
|
||||||
const result = await reportProgress(ctx, { body });
|
const result = await reportProgress(ctx, { body });
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return {
|
return {
|
||||||
@@ -95513,7 +95510,7 @@ function ReplyToReviewCommentTool(ctx) {
|
|||||||
name: "reply_to_review_comment",
|
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).",
|
description: "Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
|
||||||
parameters: ReplyToReviewComment,
|
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 bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
||||||
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -123902,16 +123899,17 @@ function $(cmd, args3, options) {
|
|||||||
var CheckoutPr = type({
|
var CheckoutPr = type({
|
||||||
pull_number: type.number.describe("the pull request number to checkout")
|
pull_number: type.number.describe("the pull request number to checkout")
|
||||||
});
|
});
|
||||||
async function checkoutPrBranch(ctx, pull_number) {
|
async function checkoutPrBranch(params) {
|
||||||
log.debug(`\u{1F500} checking out PR #${pull_number}...`);
|
const { octokit, owner, name, token, pullNumber } = params;
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
log.info(`\u{1F500} checking out PR #${pullNumber}...`);
|
||||||
owner: ctx.owner,
|
const pr = await octokit.rest.pulls.get({
|
||||||
repo: ctx.name,
|
owner,
|
||||||
pull_number
|
repo: name,
|
||||||
|
pull_number: pullNumber
|
||||||
});
|
});
|
||||||
const headRepo = pr.data.head.repo;
|
const headRepo = pr.data.head.repo;
|
||||||
if (!headRepo) {
|
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 isFork = headRepo.full_name !== pr.data.base.repo.full_name;
|
||||||
const baseBranch = pr.data.base.ref;
|
const baseBranch = pr.data.base.ref;
|
||||||
@@ -123924,18 +123922,18 @@ async function checkoutPrBranch(ctx, pull_number) {
|
|||||||
log.debug(`\u{1F4E5} fetching base branch (${baseBranch})...`);
|
log.debug(`\u{1F4E5} fetching base branch (${baseBranch})...`);
|
||||||
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
|
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
|
||||||
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
||||||
log.debug(`\u{1F33F} fetching PR #${pull_number} (${headBranch})...`);
|
log.debug(`\u{1F33F} fetching PR #${pullNumber} (${headBranch})...`);
|
||||||
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
|
$("git", ["fetch", "--no-tags", "origin", `pull/${pullNumber}/head:${headBranch}`]);
|
||||||
$("git", ["checkout", headBranch]);
|
$("git", ["checkout", headBranch]);
|
||||||
log.debug(`\u2713 checked out PR #${pull_number}`);
|
log.debug(`\u2713 checked out PR #${pullNumber}`);
|
||||||
}
|
}
|
||||||
if (alreadyOnBranch) {
|
if (alreadyOnBranch) {
|
||||||
log.debug(`\u{1F4E5} fetching base branch (${baseBranch})...`);
|
log.debug(`\u{1F4E5} fetching base branch (${baseBranch})...`);
|
||||||
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
|
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
|
||||||
}
|
}
|
||||||
if (isFork) {
|
if (isFork) {
|
||||||
const remoteName = `pr-${pull_number}`;
|
const remoteName = `pr-${pullNumber}`;
|
||||||
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
|
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
|
||||||
try {
|
try {
|
||||||
$("git", ["remote", "add", remoteName, forkUrl]);
|
$("git", ["remote", "add", remoteName, forkUrl]);
|
||||||
log.debug(`\u{1F4CC} added remote '${remoteName}' for fork ${headRepo.full_name}`);
|
log.debug(`\u{1F4CC} added remote '${remoteName}' for fork ${headRepo.full_name}`);
|
||||||
@@ -123953,15 +123951,22 @@ async function checkoutPrBranch(ctx, pull_number) {
|
|||||||
} else {
|
} else {
|
||||||
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
|
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
|
||||||
}
|
}
|
||||||
ctx.toolState.prNumber = pull_number;
|
return { prNumber: pullNumber };
|
||||||
}
|
}
|
||||||
function CheckoutPrTool(ctx) {
|
function CheckoutPrTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "checkout_pr",
|
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.",
|
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,
|
parameters: CheckoutPr,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
await checkoutPrBranch(ctx, 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({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -123995,7 +124000,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|||||||
name: "get_check_suite_logs",
|
name: "get_check_suite_logs",
|
||||||
description: "get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
|
description: "get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
|
||||||
parameters: GetCheckSuiteLogs,
|
parameters: GetCheckSuiteLogs,
|
||||||
execute: execute(ctx, async ({ check_suite_id }) => {
|
execute: execute(async ({ check_suite_id }) => {
|
||||||
const workflowRuns = await ctx.octokit.paginate(
|
const workflowRuns = await ctx.octokit.paginate(
|
||||||
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
|
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
|
||||||
{
|
{
|
||||||
@@ -124076,7 +124081,7 @@ function DebugShellCommandTool(_ctx) {
|
|||||||
name: "debug_shell_command",
|
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.",
|
description: "debug tool: runs 'git status' and returns the output. use this to test shell command execution in the MCP server.",
|
||||||
parameters: DebugShellCommand,
|
parameters: DebugShellCommand,
|
||||||
execute: execute(_ctx, async () => {
|
execute: execute(async () => {
|
||||||
const result = $("git", ["status"]);
|
const result = $("git", ["status"]);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -124097,7 +124102,7 @@ function ListFilesTool(_ctx) {
|
|||||||
name: "list_files",
|
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.",
|
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,
|
parameters: ListFiles,
|
||||||
execute: execute(_ctx, async ({ path: path4 }) => {
|
execute: execute(async ({ path: path4 }) => {
|
||||||
const pathStr = path4 ?? ".";
|
const pathStr = path4 ?? ".";
|
||||||
const cwd2 = process.cwd();
|
const cwd2 = process.cwd();
|
||||||
let gitFiles = [];
|
let gitFiles = [];
|
||||||
@@ -124194,7 +124199,7 @@ function CreateBranchTool(ctx) {
|
|||||||
name: "create_branch",
|
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.",
|
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,
|
parameters: CreateBranch,
|
||||||
execute: execute(ctx, async ({ branchName, baseBranch }) => {
|
execute: execute(async ({ branchName, baseBranch }) => {
|
||||||
const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";
|
const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";
|
||||||
if (containsSecrets(branchName)) {
|
if (containsSecrets(branchName)) {
|
||||||
throw new Error(
|
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."
|
"Array of file paths to commit (relative to repo root). If empty, commits all staged changes."
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
function CommitFilesTool(ctx) {
|
function CommitFilesTool(_ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "commit_files",
|
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.",
|
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,
|
parameters: CommitFiles,
|
||||||
execute: execute(ctx, async ({ message, files }) => {
|
execute: execute(async ({ message, files }) => {
|
||||||
if (containsSecrets(message)) {
|
if (containsSecrets(message)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Commit blocked: secrets detected in commit message. Please remove any sensitive information (API keys, tokens, passwords) before committing."
|
"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",
|
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.",
|
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,
|
parameters: PushBranch,
|
||||||
execute: execute(_ctx, async ({ branchName, force }) => {
|
execute: execute(async ({ branchName, force }) => {
|
||||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||||
let remote = "origin";
|
let remote = "origin";
|
||||||
try {
|
try {
|
||||||
@@ -124313,7 +124318,7 @@ function IssueTool(ctx) {
|
|||||||
name: "create_issue",
|
name: "create_issue",
|
||||||
description: "Create a new GitHub issue",
|
description: "Create a new GitHub issue",
|
||||||
parameters: 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({
|
const result = await ctx.octokit.rest.issues.create({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -124329,7 +124334,9 @@ function IssueTool(ctx) {
|
|||||||
url: result.data.html_url,
|
url: result.data.html_url,
|
||||||
title: result.data.title,
|
title: result.data.title,
|
||||||
state: result.data.state,
|
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)
|
assignees: result.data.assignees?.map((assignee) => assignee.login)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -124345,7 +124352,7 @@ function GetIssueCommentsTool(ctx) {
|
|||||||
name: "get_issue_comments",
|
name: "get_issue_comments",
|
||||||
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
||||||
parameters: GetIssueComments,
|
parameters: GetIssueComments,
|
||||||
execute: execute(ctx, async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
ctx.toolState.issueNumber = issue_number;
|
ctx.toolState.issueNumber = issue_number;
|
||||||
const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
|
const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -124379,7 +124386,7 @@ function GetIssueEventsTool(ctx) {
|
|||||||
name: "get_issue_events",
|
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.",
|
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,
|
parameters: GetIssueEvents,
|
||||||
execute: execute(ctx, async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
ctx.toolState.issueNumber = issue_number;
|
ctx.toolState.issueNumber = issue_number;
|
||||||
const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
|
const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -124451,7 +124458,7 @@ function IssueInfoTool(ctx) {
|
|||||||
name: "get_issue",
|
name: "get_issue",
|
||||||
description: "Retrieve GitHub issue information by issue number",
|
description: "Retrieve GitHub issue information by issue number",
|
||||||
parameters: IssueInfo,
|
parameters: IssueInfo,
|
||||||
execute: execute(ctx, async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
const issue3 = await ctx.octokit.rest.issues.get({
|
const issue3 = await ctx.octokit.rest.issues.get({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -124503,7 +124510,7 @@ function AddLabelsTool(ctx) {
|
|||||||
name: "add_labels",
|
name: "add_labels",
|
||||||
description: "Add labels to a GitHub issue or pull request. Only use labels that already exist in the repository.",
|
description: "Add labels to a GitHub issue or pull request. Only use labels that already exist in the repository.",
|
||||||
parameters: AddLabelsParams,
|
parameters: AddLabelsParams,
|
||||||
execute: execute(ctx, async ({ issue_number, labels }) => {
|
execute: execute(async ({ issue_number, labels }) => {
|
||||||
const result = await ctx.octokit.rest.issues.addLabels({
|
const result = await ctx.octokit.rest.issues.addLabels({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -124540,7 +124547,7 @@ function PullRequestTool(ctx) {
|
|||||||
name: "create_pull_request",
|
name: "create_pull_request",
|
||||||
description: "Create a pull request from the current branch",
|
description: "Create a pull request from the current branch",
|
||||||
parameters: PullRequest,
|
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 });
|
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||||
log.debug(`Current branch: ${currentBranch}`);
|
log.debug(`Current branch: ${currentBranch}`);
|
||||||
if (containsSecrets(title) || containsSecrets(body)) {
|
if (containsSecrets(title) || containsSecrets(body)) {
|
||||||
@@ -124585,7 +124592,7 @@ function PullRequestInfoTool(ctx) {
|
|||||||
name: "get_pull_request",
|
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.",
|
description: "Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
|
||||||
parameters: PullRequestInfo,
|
parameters: PullRequestInfo,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -124648,7 +124655,7 @@ function StartReviewTool(ctx) {
|
|||||||
name: "start_review",
|
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.",
|
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,
|
parameters: StartReview,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
if (ctx.toolState.review) {
|
if (ctx.toolState.review) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Review session already in progress. Call submit_review first to finish it.`
|
`Review session already in progress. Call submit_review first to finish it.`
|
||||||
@@ -124723,7 +124730,7 @@ function AddReviewCommentTool(ctx) {
|
|||||||
name: "add_review_comment",
|
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.",
|
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,
|
parameters: AddReviewComment,
|
||||||
execute: execute(ctx, async ({ path: path4, line, body, side }) => {
|
execute: execute(async ({ path: path4, line, body, side }) => {
|
||||||
if (!ctx.toolState.review) {
|
if (!ctx.toolState.review) {
|
||||||
throw new Error("No review session started. Call start_review first.");
|
throw new Error("No review session started. Call start_review first.");
|
||||||
}
|
}
|
||||||
@@ -124754,7 +124761,7 @@ function SubmitReviewTool(ctx) {
|
|||||||
name: "submit_review",
|
name: "submit_review",
|
||||||
description: "Submit the current review session. All comments added via add_review_comment will be published. Must call start_review first.",
|
description: "Submit the current review session. All comments added via add_review_comment will be published. Must call start_review first.",
|
||||||
parameters: SubmitReview,
|
parameters: SubmitReview,
|
||||||
execute: execute(ctx, async ({ body }) => {
|
execute: execute(async ({ body }) => {
|
||||||
if (!ctx.toolState.review) {
|
if (!ctx.toolState.review) {
|
||||||
throw new Error("No review session started. Call start_review first.");
|
throw new Error("No review session started. Call start_review first.");
|
||||||
}
|
}
|
||||||
@@ -124860,7 +124867,7 @@ function GetReviewCommentsTool(ctx) {
|
|||||||
name: "get_review_comments",
|
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.",
|
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,
|
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, {
|
const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -124930,7 +124937,7 @@ function ListPullRequestReviewsTool(ctx) {
|
|||||||
name: "list_pull_request_reviews",
|
name: "list_pull_request_reviews",
|
||||||
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
||||||
parameters: ListPullRequestReviews,
|
parameters: ListPullRequestReviews,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -124964,7 +124971,7 @@ function SelectModeTool(ctx) {
|
|||||||
name: "select_mode",
|
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.",
|
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,
|
parameters: SelectMode,
|
||||||
execute: execute(ctx, async ({ modeName }) => {
|
execute: execute(async ({ modeName }) => {
|
||||||
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
|
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
|
||||||
if (!selectedMode) {
|
if (!selectedMode) {
|
||||||
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
||||||
@@ -125038,7 +125045,7 @@ async function startMcpHttpServer(ctx) {
|
|||||||
PushBranchTool(ctx),
|
PushBranchTool(ctx),
|
||||||
ListFilesTool(ctx)
|
ListFilesTool(ctx)
|
||||||
];
|
];
|
||||||
if (!isProgressCommentDisabled(ctx)) {
|
if (!ctx.payload.disableProgressComment) {
|
||||||
tools.push(ReportProgressTool(ctx));
|
tools.push(ReportProgressTool(ctx));
|
||||||
}
|
}
|
||||||
addTools(ctx, server, tools);
|
addTools(ctx, server, tools);
|
||||||
@@ -125688,7 +125695,7 @@ function setupGitConfig() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function setupGit(ctx) {
|
async function setupGitAuth(params) {
|
||||||
const repoDir = process.cwd();
|
const repoDir = process.cwd();
|
||||||
log.info("\xBB setting up git authentication...");
|
log.info("\xBB setting up git authentication...");
|
||||||
try {
|
try {
|
||||||
@@ -125700,16 +125707,23 @@ async function setupGit(ctx) {
|
|||||||
} catch {
|
} catch {
|
||||||
log.debug("\xBB no existing authentication headers to remove");
|
log.debug("\xBB no existing authentication headers to remove");
|
||||||
}
|
}
|
||||||
if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
|
if (params.payload.event.is_pr !== true || !params.payload.event.issue_number) {
|
||||||
const originUrl2 = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
|
const originUrl2 = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
|
||||||
$("git", ["remote", "set-url", "origin", originUrl2], { cwd: repoDir });
|
$("git", ["remote", "set-url", "origin", originUrl2], { cwd: repoDir });
|
||||||
log.info("\xBB updated origin URL with authentication token");
|
log.info("\xBB updated origin URL with authentication token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const prNumber = ctx.payload.event.issue_number;
|
const prNumber = params.payload.event.issue_number;
|
||||||
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
|
const originUrl = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
|
||||||
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
$("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
|
// utils/timer.ts
|
||||||
@@ -125745,28 +125759,105 @@ async function main(inputs) {
|
|||||||
try {
|
try {
|
||||||
const timer = new Timer();
|
const timer = new Timer();
|
||||||
payload = parsePayload(inputs);
|
payload = parsePayload(inputs);
|
||||||
const partialCtx = await initializeContext(inputs, payload);
|
Inputs.assert(inputs);
|
||||||
const ctx = partialCtx;
|
setupGitConfig();
|
||||||
ctx.toolState = {};
|
const [githubSetup, sharedTempDir] = await Promise.all([
|
||||||
timer.checkpoint("initializeContext");
|
initializeGitHub(),
|
||||||
await setupGit(ctx);
|
createTempDirectory()
|
||||||
timer.checkpoint("setupGit");
|
]);
|
||||||
await setupTempDirectory(ctx);
|
timer.checkpoint("githubSetup");
|
||||||
timer.checkpoint("setupTempDirectory");
|
const agent2 = resolveAgent({
|
||||||
const [, prepResults] = await Promise.all([installAgentCli(ctx), runPrepPhase()]);
|
inputs,
|
||||||
ctx.prepResults = prepResults;
|
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;
|
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || void 0;
|
||||||
ctx.modes = [
|
const computedModes = [
|
||||||
...getModes({
|
...getModes({
|
||||||
disableProgressComment: ctx.payload.disableProgressComment,
|
disableProgressComment: resolvedPayload.disableProgressComment,
|
||||||
dependenciesPreinstalled
|
dependenciesPreinstalled
|
||||||
}),
|
}),
|
||||||
...ctx.payload.modes || []
|
...resolvedPayload.modes || []
|
||||||
];
|
];
|
||||||
timer.checkpoint("installAgentCli+prepPhase");
|
const runId = process.env.GITHUB_RUN_ID || "";
|
||||||
await startMcpServer(ctx);
|
if (runId) {
|
||||||
mcpServerClose = ctx.mcpServerClose;
|
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
|
||||||
timer.checkpoint("startMcpServer");
|
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) {
|
if (ctx.payload.event.trigger === "fix_review" && Array.isArray(ctx.payload.event.comment_ids) && ctx.payload.event.comment_ids.length === 0) {
|
||||||
await reportProgress(ctx, {
|
await reportProgress(ctx, {
|
||||||
body: `\u{1F44D} **No approved comments found**
|
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 };
|
return { success: true };
|
||||||
}
|
}
|
||||||
setupMcpServers(ctx);
|
|
||||||
await validateApiKey(ctx);
|
|
||||||
const result = await runAgent(ctx);
|
const result = await runAgent(ctx);
|
||||||
const mainResult = await handleAgentResult(result);
|
const mainResult = await handleAgentResult(result);
|
||||||
return mainResult;
|
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();
|
await revokeGitHubInstallationToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getAvailableAgents(inputs) {
|
function agentHasApiKeys(agent2, inputs) {
|
||||||
return Object.values(agents).filter((agent2) => {
|
|
||||||
if (agent2.name === "opencode") {
|
if (agent2.name === "opencode") {
|
||||||
return Object.keys(inputs).some((key) => key.includes("api_key"));
|
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]);
|
||||||
}
|
}
|
||||||
return agent2.apiKeyNames.some((inputKey) => inputs[inputKey]);
|
const inputsRecord = inputs;
|
||||||
});
|
return agent2.apiKeyNames.some((inputKey) => inputsRecord[inputKey]);
|
||||||
|
}
|
||||||
|
function getAvailableAgents(inputs) {
|
||||||
|
return Object.values(agents).filter((agent2) => agentHasApiKeys(agent2, inputs));
|
||||||
}
|
}
|
||||||
function getAllPossibleKeyNames() {
|
function getAllPossibleKeyNames() {
|
||||||
return Object.keys(
|
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 apiUrl = process.env.API_URL || "https://pullfrog.com";
|
||||||
const settingsUrl = `${apiUrl}/console/${ctx.owner}/${ctx.name}`;
|
const settingsUrl = `${apiUrl}/console/${params.owner}/${params.name}`;
|
||||||
const githubRepoUrl = `https://github.com/${ctx.owner}/${ctx.name}`;
|
const githubRepoUrl = `https://github.com/${params.owner}/${params.name}`;
|
||||||
const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
|
const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
|
||||||
const isOpenCode = ctx.agent?.name === "opencode";
|
const isOpenCode = params.agent.name === "opencode";
|
||||||
let secretNameList;
|
let secretNameList;
|
||||||
if (isOpenCode) {
|
if (isOpenCode) {
|
||||||
secretNameList = "any API key (e.g., `OPENCODE_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)";
|
secretNameList = "any API key (e.g., `OPENCODE_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)";
|
||||||
} else {
|
} 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()}\``);
|
const secretNames = inputKeys.map((key) => `\`${key.toUpperCase()}\``);
|
||||||
secretNameList = inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`;
|
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:
|
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"
|
5. Click "Add secret"
|
||||||
|
|
||||||
Alternatively, configure Pullfrog to use a different agent at ${settingsUrl}`;
|
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}...`);
|
log.info(`\u{1F438} Running pullfrog/action@${package_default.version}...`);
|
||||||
Inputs.assert(inputs);
|
const token = await setupGitHubInstallationToken();
|
||||||
setupGitConfig();
|
|
||||||
const githubInstallationToken2 = await setupGitHubInstallationToken();
|
|
||||||
const { owner, name } = parseRepoContext();
|
const { owner, name } = parseRepoContext();
|
||||||
const octokit = new Octokit2({
|
const octokit = new Octokit2({ auth: token });
|
||||||
auth: githubInstallationToken2
|
const [repoResponse, repoSettings] = await Promise.all([
|
||||||
});
|
octokit.repos.get({ owner, repo: name }),
|
||||||
const response = await octokit.repos.get({
|
fetchRepoSettings({ token, repoContext: { owner, name } })
|
||||||
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 || []
|
|
||||||
];
|
|
||||||
return {
|
return {
|
||||||
|
token,
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
inputs,
|
|
||||||
githubInstallationToken: githubInstallationToken2,
|
|
||||||
octokit,
|
octokit,
|
||||||
repo,
|
repo: repoResponse.data,
|
||||||
agentName,
|
repoSettings
|
||||||
agent: agent2,
|
|
||||||
payload: resolvedPayload,
|
|
||||||
repoSettings,
|
|
||||||
modes: computedModes,
|
|
||||||
sharedTempDir: ""
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function resolveAgent({
|
function resolveAgent({
|
||||||
@@ -125901,41 +125964,37 @@ function resolveAgent({
|
|||||||
const agentOverride = process.env.AGENT_OVERRIDE;
|
const agentOverride = process.env.AGENT_OVERRIDE;
|
||||||
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
|
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
|
||||||
if (configuredAgentName) {
|
if (configuredAgentName) {
|
||||||
const agentName2 = configuredAgentName;
|
const agent3 = agents[configuredAgentName];
|
||||||
const agent3 = agents[agentName2];
|
|
||||||
if (!agent3) {
|
if (!agent3) {
|
||||||
throw new Error(`invalid agent name: ${agentName2}`);
|
throw new Error(`invalid agent name: ${configuredAgentName}`);
|
||||||
}
|
}
|
||||||
const isExplicitOverride = agentOverride !== void 0 || payload.agent !== null;
|
const isExplicitOverride = agentOverride !== void 0 || payload.agent !== null;
|
||||||
if (isExplicitOverride) {
|
if (isExplicitOverride) {
|
||||||
log.info(`\xBB selected configured agent: ${agentName2}`);
|
log.info(`Selected configured agent: ${agent3.name}`);
|
||||||
return { agentName: agentName2, agent: agent3 };
|
return agent3;
|
||||||
}
|
}
|
||||||
const hasMatchingKey = agent3.name === "opencode" ? Object.keys(inputs).some((key) => key.includes("api_key")) : agent3.apiKeyNames.some((inputKey) => inputs[inputKey]);
|
if (agentHasApiKeys(agent3, inputs)) {
|
||||||
if (!hasMatchingKey) {
|
log.info(`Selected configured agent: ${agent3.name}`);
|
||||||
|
return agent3;
|
||||||
|
}
|
||||||
|
const availableAgents2 = getAvailableAgents(inputs);
|
||||||
log.warning(
|
log.warning(
|
||||||
`Repo default agent ${agentName2} has no matching API keys. Available agents: ${getAvailableAgents(inputs).map((a) => a.name).join(", ") || "none"}`
|
`Repo default agent ${agent3.name} has no matching API keys. Available: ${availableAgents2.map((a) => a.name).join(", ") || "none"}`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
log.info(`\xBB selected configured agent: ${agentName2}`);
|
|
||||||
return { agentName: agentName2, agent: agent3 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const availableAgents = getAvailableAgents(inputs);
|
const availableAgents = getAvailableAgents(inputs);
|
||||||
const availableAgentNames = availableAgents.map((agent3) => agent3.name).join(", ");
|
|
||||||
log.debug(`\xBB available agents: ${availableAgentNames || "none"}`);
|
|
||||||
if (availableAgents.length === 0) {
|
if (availableAgents.length === 0) {
|
||||||
throw new Error("no agents available - missing API keys");
|
throw new Error("no agents available - missing API keys");
|
||||||
}
|
}
|
||||||
const agentName = availableAgents[0].name;
|
|
||||||
const agent2 = availableAgents[0];
|
const agent2 = availableAgents[0];
|
||||||
log.info(`\xBB no agent configured, defaulting to first available agent: ${agentName}`);
|
log.info(`No agent configured, defaulting to first available agent: ${agent2.name}`);
|
||||||
return { agentName, agent: agent2 };
|
return agent2;
|
||||||
}
|
}
|
||||||
async function setupTempDirectory(ctx) {
|
async function createTempDirectory() {
|
||||||
ctx.sharedTempDir = await mkdtemp2(join11(tmpdir2(), "pullfrog-"));
|
const sharedTempDir = await mkdtemp2(join11(tmpdir2(), "pullfrog-"));
|
||||||
process.env.PULLFROG_TEMP_DIR = ctx.sharedTempDir;
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
||||||
log.debug(`\xBB created temp dir at ${ctx.sharedTempDir}`);
|
log.info(`\u{1F4C2} PULLFROG_TEMP_DIR has been created at ${sharedTempDir}`);
|
||||||
|
return sharedTempDir;
|
||||||
}
|
}
|
||||||
function parsePayload(inputs) {
|
function parsePayload(inputs) {
|
||||||
try {
|
try {
|
||||||
@@ -125956,71 +126015,55 @@ function parsePayload(inputs) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function startMcpServer(ctx) {
|
async function installAgentCli(params) {
|
||||||
const runId = process.env.GITHUB_RUN_ID || "";
|
if (params.agent.name === "gemini") {
|
||||||
ctx.runId = runId;
|
return params.agent.install(params.token);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
return params.agent.install();
|
||||||
}
|
}
|
||||||
const jobName = process.env.GITHUB_JOB;
|
function collectApiKeys(agent2, inputs) {
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
const apiKeys = {};
|
const apiKeys = {};
|
||||||
for (const inputKey of ctx.agent.apiKeyNames) {
|
const inputsRecord = inputs;
|
||||||
const value2 = ctx.inputs[inputKey];
|
for (const inputKey of agent2.apiKeyNames) {
|
||||||
|
const value2 = inputsRecord[inputKey];
|
||||||
if (value2) {
|
if (value2) {
|
||||||
apiKeys[inputKey] = 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)) {
|
for (const [key, value2] of Object.entries(process.env)) {
|
||||||
if (value2 && typeof value2 === "string" && key.includes("API_KEY")) {
|
if (value2 && typeof value2 === "string" && key.includes("API_KEY")) {
|
||||||
const inputKey = key.toLowerCase();
|
apiKeys[key.toLowerCase()] = value2;
|
||||||
apiKeys[inputKey] = value2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return apiKeys;
|
||||||
|
}
|
||||||
|
function validateApiKey(params) {
|
||||||
|
const apiKeys = collectApiKeys(params.agent, params.inputs);
|
||||||
if (Object.keys(apiKeys).length === 0) {
|
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];
|
return {
|
||||||
ctx.apiKeys = apiKeys;
|
success: true,
|
||||||
|
apiKey: Object.values(apiKeys)[0],
|
||||||
|
apiKeys
|
||||||
|
};
|
||||||
}
|
}
|
||||||
async function runAgent(ctx) {
|
async function runAgent(ctx) {
|
||||||
log.info(`\xBB running ${ctx.agentName}...`);
|
log.info(`Running ${ctx.agent.name}...`);
|
||||||
log.box(ctx.payload.prompt.trim(), { title: "Prompt" });
|
const { context: _context, ...eventWithoutContext } = ctx.payload.event;
|
||||||
|
const promptContent = `${ctx.payload.prompt}
|
||||||
|
|
||||||
|
${encode(eventWithoutContext)}`;
|
||||||
|
log.box(promptContent, { title: "Prompt" });
|
||||||
return ctx.agent.run({
|
return ctx.agent.run({
|
||||||
payload: ctx.payload,
|
payload: ctx.payload,
|
||||||
mcpServers: ctx.mcpServers,
|
mcpServers: ctx.mcpServers,
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { flatMorph } from "@ark/util";
|
import { flatMorph } from "@ark/util";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import { encode as toonEncode } from "@toon-format/toon";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { agents } from "./agents/index.ts";
|
import { type Agent, agents } from "./agents/index.ts";
|
||||||
import type { AgentResult } from "./agents/shared.ts";
|
import type { AgentResult } from "./agents/shared.ts";
|
||||||
import type { AgentName, Payload } from "./external.ts";
|
import type { AgentName, Payload } from "./external.ts";
|
||||||
import { agentsManifest } from "./external.ts";
|
import { agentsManifest } from "./external.ts";
|
||||||
@@ -22,7 +23,7 @@ import {
|
|||||||
revokeGitHubInstallationToken,
|
revokeGitHubInstallationToken,
|
||||||
setupGitHubInstallationToken,
|
setupGitHubInstallationToken,
|
||||||
} from "./utils/github.ts";
|
} from "./utils/github.ts";
|
||||||
import { setupGit, setupGitConfig } from "./utils/setup.ts";
|
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
|
||||||
import { Timer } from "./utils/timer.ts";
|
import { Timer } from "./utils/timer.ts";
|
||||||
|
|
||||||
// runtime validation using agents (needed for ArkType)
|
// runtime validation using agents (needed for ArkType)
|
||||||
@@ -50,6 +51,20 @@ export interface MainResult {
|
|||||||
error?: string | undefined;
|
error?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// intermediate result types for deterministic context building
|
||||||
|
interface GitHubSetup {
|
||||||
|
token: string;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
octokit: Octokit;
|
||||||
|
repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||||
|
repoSettings: RepoSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiKeySetup =
|
||||||
|
| { success: true; apiKey: string; apiKeys: Record<string, string> }
|
||||||
|
| { success: false; error: string };
|
||||||
|
|
||||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||||
let mcpServerClose: (() => Promise<void>) | undefined;
|
let mcpServerClose: (() => Promise<void>) | undefined;
|
||||||
let payload: Payload | undefined;
|
let payload: Payload | undefined;
|
||||||
@@ -57,39 +72,126 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
try {
|
try {
|
||||||
const timer = new Timer();
|
const timer = new Timer();
|
||||||
|
|
||||||
// parse payload early to extract agent
|
// phase 1: parse and validate inputs
|
||||||
payload = parsePayload(inputs);
|
payload = parsePayload(inputs);
|
||||||
|
Inputs.assert(inputs);
|
||||||
|
setupGitConfig();
|
||||||
|
|
||||||
const partialCtx = await initializeContext(inputs, payload);
|
// phase 2: fast setup (github + temp dir)
|
||||||
const ctx = partialCtx as Context;
|
const [githubSetup, sharedTempDir] = await Promise.all([
|
||||||
ctx.toolState = {};
|
initializeGitHub(),
|
||||||
timer.checkpoint("initializeContext");
|
createTempDirectory(),
|
||||||
|
]);
|
||||||
|
timer.checkpoint("githubSetup");
|
||||||
|
|
||||||
await setupGit(ctx);
|
// phase 3: resolve agent (needs repo settings)
|
||||||
timer.checkpoint("setupGit");
|
const agent = resolveAgent({
|
||||||
|
inputs,
|
||||||
|
payload,
|
||||||
|
repoSettings: githubSetup.repoSettings,
|
||||||
|
});
|
||||||
|
const resolvedPayload = { ...payload, agent: agent.name };
|
||||||
|
|
||||||
await setupTempDirectory(ctx);
|
// phase 4: validate API key (sync, needs agent) - fail fast before long-running operations
|
||||||
timer.checkpoint("setupTempDirectory");
|
const apiKeySetup = validateApiKey({
|
||||||
|
agent,
|
||||||
|
inputs,
|
||||||
|
owner: githubSetup.owner,
|
||||||
|
name: githubSetup.name,
|
||||||
|
});
|
||||||
|
if (!apiKeySetup.success) {
|
||||||
|
await reportErrorToComment({ error: apiKeySetup.error });
|
||||||
|
return { success: false, error: apiKeySetup.error };
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: david fix this garbage
|
// phase 5: parallel long-running operations (prep + agent install + git auth)
|
||||||
// run agent CLI installation and prep phase in parallel
|
const toolState: ToolState = {};
|
||||||
const [, prepResults] = await Promise.all([installAgentCli(ctx), runPrepPhase()]);
|
const [prepResults, cliPath] = await Promise.all([
|
||||||
ctx.prepResults = prepResults;
|
runPrepPhase(),
|
||||||
|
installAgentCli({ agent, token: githubSetup.token }),
|
||||||
|
setupGitAuth({
|
||||||
|
token: githubSetup.token,
|
||||||
|
owner: githubSetup.owner,
|
||||||
|
name: githubSetup.name,
|
||||||
|
payload: resolvedPayload,
|
||||||
|
octokit: githubSetup.octokit,
|
||||||
|
toolState,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
timer.checkpoint("prep+agentSetup+gitAuth");
|
||||||
|
|
||||||
// recompute modes now that we know if dependencies were preinstalled
|
// phase 6: compute modes (needs prep results)
|
||||||
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined;
|
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined;
|
||||||
ctx.modes = [
|
const computedModes: Mode[] = [
|
||||||
...getModes({
|
...getModes({
|
||||||
disableProgressComment: ctx.payload.disableProgressComment,
|
disableProgressComment: resolvedPayload.disableProgressComment,
|
||||||
dependenciesPreinstalled,
|
dependenciesPreinstalled,
|
||||||
}),
|
}),
|
||||||
...(ctx.payload.modes || []),
|
...(resolvedPayload.modes || []),
|
||||||
];
|
];
|
||||||
timer.checkpoint("installAgentCli+prepPhase");
|
|
||||||
|
|
||||||
await startMcpServer(ctx);
|
// phase 7: compute runId/jobId for MCP tools
|
||||||
mcpServerClose = ctx.mcpServerClose;
|
const runId = process.env.GITHUB_RUN_ID || "";
|
||||||
timer.checkpoint("startMcpServer");
|
if (runId) {
|
||||||
|
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
|
||||||
|
if (workflowRunInfo.progressCommentId) {
|
||||||
|
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
||||||
|
log.info(`📝 Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let jobId: string | undefined;
|
||||||
|
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(`📋 Found job ID: ${jobId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase 8: build tool context and start MCP server
|
||||||
|
const toolContext: 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,
|
||||||
|
sharedTempDir,
|
||||||
|
runId,
|
||||||
|
jobId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { url: mcpServerUrl, close: mcpServerCloseFunc } = await startMcpHttpServer(toolContext);
|
||||||
|
mcpServerClose = mcpServerCloseFunc;
|
||||||
|
log.info(`🚀 MCP server started at ${mcpServerUrl}`);
|
||||||
|
|
||||||
|
const mcpServers = createMcpConfigs(mcpServerUrl);
|
||||||
|
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
||||||
|
timer.checkpoint("mcpServer");
|
||||||
|
|
||||||
|
// BUILD FINAL IMMUTABLE CONTEXT
|
||||||
|
const ctx: AgentContext = {
|
||||||
|
...toolContext,
|
||||||
|
inputs,
|
||||||
|
mcpServerUrl,
|
||||||
|
mcpServerClose: mcpServerCloseFunc,
|
||||||
|
mcpServers,
|
||||||
|
cliPath,
|
||||||
|
apiKey: apiKeySetup.apiKey,
|
||||||
|
apiKeys: apiKeySetup.apiKeys,
|
||||||
|
};
|
||||||
|
|
||||||
// check for empty comment_ids in fix_review trigger - report and exit early
|
// check for empty comment_ids in fix_review trigger - report and exit early
|
||||||
if (
|
if (
|
||||||
@@ -103,10 +205,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
setupMcpServers(ctx);
|
|
||||||
|
|
||||||
await validateApiKey(ctx);
|
|
||||||
|
|
||||||
const result = await runAgent(ctx);
|
const result = await runAgent(ctx);
|
||||||
const mainResult = await handleAgentResult(result);
|
const mainResult = await handleAgentResult(result);
|
||||||
return mainResult;
|
return mainResult;
|
||||||
@@ -142,15 +240,22 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
/**
|
/**
|
||||||
* Get agents that have matching API keys in the inputs
|
* Get agents that have matching API keys in the inputs
|
||||||
*/
|
*/
|
||||||
function getAvailableAgents(inputs: Inputs): (typeof agents)[AgentName][] {
|
/**
|
||||||
return Object.values(agents).filter((agent) => {
|
* Check if an agent has API keys available (inputs or process.env for opencode)
|
||||||
// for OpenCode, check if any API_KEY variable exists in inputs
|
*/
|
||||||
|
function agentHasApiKeys(agent: Agent, inputs: Inputs): boolean {
|
||||||
if (agent.name === "opencode") {
|
if (agent.name === "opencode") {
|
||||||
return Object.keys(inputs).some((key) => key.includes("api_key"));
|
// check inputs first, then process.env
|
||||||
|
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]);
|
||||||
}
|
}
|
||||||
// for other agents, check apiKeyNames
|
const inputsRecord = inputs as Record<string, string | undefined>;
|
||||||
return agent.apiKeyNames.some((inputKey) => inputs[inputKey]);
|
return agent.apiKeyNames.some((inputKey) => inputsRecord[inputKey]);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function getAvailableAgents(inputs: Inputs): Agent[] {
|
||||||
|
return Object.values(agents).filter((agent) => agentHasApiKeys(agent, inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,28 +270,29 @@ function getAllPossibleKeyNames(): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throw an error for missing API key with helpful message linking to repo settings
|
* Build a helpful error message for missing API key with links to repo settings
|
||||||
*/
|
*/
|
||||||
async function throwMissingApiKeyError(ctx: Context): Promise<never> {
|
function buildMissingApiKeyError(params: { agent: Agent; owner: string; name: string }): string {
|
||||||
const apiUrl = process.env.API_URL || "https://pullfrog.com";
|
const apiUrl = process.env.API_URL || "https://pullfrog.com";
|
||||||
const settingsUrl = `${apiUrl}/console/${ctx.owner}/${ctx.name}`;
|
const settingsUrl = `${apiUrl}/console/${params.owner}/${params.name}`;
|
||||||
|
|
||||||
const githubRepoUrl = `https://github.com/${ctx.owner}/${ctx.name}`;
|
const githubRepoUrl = `https://github.com/${params.owner}/${params.name}`;
|
||||||
const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
|
const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
|
||||||
|
|
||||||
// for OpenCode, use a generic message since it accepts any API key
|
// for OpenCode, use a generic message since it accepts any API key
|
||||||
const isOpenCode = ctx.agent?.name === "opencode";
|
const isOpenCode = params.agent.name === "opencode";
|
||||||
let secretNameList: string;
|
let secretNameList: string;
|
||||||
if (isOpenCode) {
|
if (isOpenCode) {
|
||||||
secretNameList =
|
secretNameList =
|
||||||
"any API key (e.g., `OPENCODE_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)";
|
"any API key (e.g., `OPENCODE_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)";
|
||||||
} else {
|
} 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()}\``);
|
const secretNames = inputKeys.map((key) => `\`${key.toUpperCase()}\``);
|
||||||
secretNameList = inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`;
|
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:
|
To fix this, add the required secret to your GitHub repository:
|
||||||
|
|
||||||
@@ -197,54 +303,34 @@ To fix this, add the required secret to your GitHub repository:
|
|||||||
5. Click "Add secret"
|
5. Click "Add secret"
|
||||||
|
|
||||||
Alternatively, configure Pullfrog to use a different agent at ${settingsUrl}`;
|
Alternatively, configure Pullfrog to use a different agent at ${settingsUrl}`;
|
||||||
|
|
||||||
// report to comment if MCP context is available (server has started)
|
|
||||||
await reportErrorToComment({ error: message });
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Context {
|
// tool context - subset of Context needed by MCP tools
|
||||||
// flattened from RepoContext
|
export interface ToolContext {
|
||||||
owner: string;
|
owner: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
// core fields
|
|
||||||
inputs: Inputs;
|
|
||||||
payload: Payload;
|
|
||||||
repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
|
||||||
agentName: AgentName;
|
|
||||||
agent: (typeof agents)[AgentName];
|
|
||||||
githubInstallationToken: string;
|
githubInstallationToken: string;
|
||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
|
payload: Payload;
|
||||||
// repo settings from Pullfrog API
|
repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||||
repoSettings: RepoSettings;
|
repoSettings: RepoSettings;
|
||||||
|
|
||||||
// modes for MCP tools
|
|
||||||
modes: Mode[];
|
modes: Mode[];
|
||||||
|
|
||||||
// setup fields
|
|
||||||
sharedTempDir: string;
|
|
||||||
|
|
||||||
// mcp fields
|
|
||||||
mcpServerUrl: string;
|
|
||||||
mcpServerClose: () => Promise<void>;
|
|
||||||
mcpServers: ReturnType<typeof createMcpConfigs>;
|
|
||||||
|
|
||||||
// agent fields
|
|
||||||
cliPath: string;
|
|
||||||
apiKey: string;
|
|
||||||
apiKeys: Record<string, string>;
|
|
||||||
|
|
||||||
// prep phase results
|
|
||||||
prepResults: PrepResult[];
|
prepResults: PrepResult[];
|
||||||
|
toolState: ToolState;
|
||||||
// workflow run info
|
agent: Agent;
|
||||||
|
sharedTempDir: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
jobId: string | undefined;
|
jobId: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// tool state - mutable scratchpad for tools
|
export interface AgentContext extends Readonly<ToolContext> {
|
||||||
toolState: ToolState;
|
readonly inputs: Inputs;
|
||||||
|
readonly mcpServerUrl: string;
|
||||||
|
readonly mcpServerClose: () => Promise<void>;
|
||||||
|
readonly mcpServers: ReturnType<typeof createMcpConfigs>;
|
||||||
|
readonly cliPath: string;
|
||||||
|
readonly apiKey: string;
|
||||||
|
readonly apiKeys: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolState {
|
export interface ToolState {
|
||||||
@@ -256,80 +342,30 @@ export interface ToolState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeContext(
|
/**
|
||||||
inputs: Inputs,
|
* Initialize GitHub connection: token, octokit, repo data, settings
|
||||||
payload: Payload
|
*/
|
||||||
): Promise<
|
async function initializeGitHub(): Promise<GitHubSetup> {
|
||||||
Omit<
|
|
||||||
Context,
|
|
||||||
| "mcpServerUrl"
|
|
||||||
| "mcpServerClose"
|
|
||||||
| "mcpServers"
|
|
||||||
| "cliPath"
|
|
||||||
| "apiKey"
|
|
||||||
| "apiKeys"
|
|
||||||
| "prepResults"
|
|
||||||
| "runId"
|
|
||||||
| "jobId"
|
|
||||||
| "toolState"
|
|
||||||
>
|
|
||||||
> {
|
|
||||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||||
Inputs.assert(inputs);
|
|
||||||
setupGitConfig();
|
|
||||||
|
|
||||||
const githubInstallationToken = await setupGitHubInstallationToken();
|
const token = await setupGitHubInstallationToken();
|
||||||
const { owner, name } = parseRepoContext();
|
const { owner, name } = parseRepoContext();
|
||||||
|
|
||||||
// create octokit instance
|
const octokit = new Octokit({ auth: token });
|
||||||
const octokit = new Octokit({
|
|
||||||
auth: githubInstallationToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
// fetch repo data
|
// fetch repo data and settings in parallel
|
||||||
const response = await octokit.repos.get({
|
const [repoResponse, repoSettings] = await Promise.all([
|
||||||
owner,
|
octokit.repos.get({ owner, repo: name }),
|
||||||
repo: name,
|
fetchRepoSettings({ token, repoContext: { owner, name } }),
|
||||||
});
|
]);
|
||||||
const repo = response.data;
|
|
||||||
|
|
||||||
// fetch repo settings
|
|
||||||
const repoSettings = await fetchRepoSettings({
|
|
||||||
token: githubInstallationToken,
|
|
||||||
repoContext: { owner, name },
|
|
||||||
});
|
|
||||||
|
|
||||||
// resolve agent and update payload with resolved agent name
|
|
||||||
const { agentName, agent } = resolveAgent({
|
|
||||||
inputs,
|
|
||||||
payload,
|
|
||||||
repoSettings,
|
|
||||||
});
|
|
||||||
const resolvedPayload = { ...payload, agent: agentName };
|
|
||||||
|
|
||||||
// compute modes from defaults + payload overrides
|
|
||||||
// note: dependenciesPreinstalled is undefined here since prepPhase runs after this
|
|
||||||
const computedModes = [
|
|
||||||
...getModes({
|
|
||||||
disableProgressComment: resolvedPayload.disableProgressComment,
|
|
||||||
dependenciesPreinstalled: undefined,
|
|
||||||
}),
|
|
||||||
...(resolvedPayload.modes || []),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
token,
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
inputs,
|
|
||||||
githubInstallationToken,
|
|
||||||
octokit,
|
octokit,
|
||||||
repo,
|
repo: repoResponse.data,
|
||||||
agentName,
|
|
||||||
agent,
|
|
||||||
payload: resolvedPayload,
|
|
||||||
repoSettings,
|
repoSettings,
|
||||||
modes: computedModes,
|
|
||||||
sharedTempDir: "",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,65 +377,54 @@ function resolveAgent({
|
|||||||
inputs: Inputs;
|
inputs: Inputs;
|
||||||
payload: Payload;
|
payload: Payload;
|
||||||
repoSettings: RepoSettings;
|
repoSettings: RepoSettings;
|
||||||
}): { agentName: AgentName; agent: (typeof agents)[AgentName] } {
|
}): Agent {
|
||||||
const agentOverride = process.env.AGENT_OVERRIDE as AgentName | undefined;
|
const agentOverride = process.env.AGENT_OVERRIDE as AgentName | undefined;
|
||||||
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
|
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
|
||||||
|
|
||||||
if (configuredAgentName) {
|
if (configuredAgentName) {
|
||||||
const agentName = configuredAgentName;
|
const agent = agents[configuredAgentName];
|
||||||
const agent = agents[agentName];
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
throw new Error(`invalid agent name: ${agentName}`);
|
throw new Error(`invalid agent name: ${configuredAgentName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if explicitly configured (via override or payload), respect it even without matching keys
|
// if explicitly configured (via override or payload), respect it even without matching keys
|
||||||
// this allows users to force an agent selection (will fail later with clear error if no keys)
|
// this allows users to force an agent selection (will fail later with clear error if no keys)
|
||||||
const isExplicitOverride = agentOverride !== undefined || payload.agent !== null;
|
const isExplicitOverride = agentOverride !== undefined || payload.agent !== null;
|
||||||
|
|
||||||
if (isExplicitOverride) {
|
if (isExplicitOverride) {
|
||||||
log.info(`» selected configured agent: ${agentName}`);
|
log.info(`Selected configured agent: ${agent.name}`);
|
||||||
return { agentName, agent };
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// for repo-level defaults, check if agent has matching keys before selecting
|
// for repo-level defaults, check if agent has matching keys before selecting
|
||||||
const hasMatchingKey =
|
if (agentHasApiKeys(agent, inputs)) {
|
||||||
agent.name === "opencode"
|
log.info(`Selected configured agent: ${agent.name}`);
|
||||||
? Object.keys(inputs).some((key) => key.includes("api_key"))
|
return agent;
|
||||||
: agent.apiKeyNames.some((inputKey) => inputs[inputKey]);
|
}
|
||||||
if (!hasMatchingKey) {
|
|
||||||
|
// fall through to auto-selection
|
||||||
|
const availableAgents = getAvailableAgents(inputs);
|
||||||
log.warning(
|
log.warning(
|
||||||
`Repo default agent ${agentName} has no matching API keys. Available agents: ${
|
`Repo default agent ${agent.name} has no matching API keys. Available: ${
|
||||||
getAvailableAgents(inputs)
|
availableAgents.map((a) => a.name).join(", ") || "none"
|
||||||
.map((a) => a.name)
|
|
||||||
.join(", ") || "none"
|
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
// fall through to auto-selection for repo defaults
|
|
||||||
} else {
|
|
||||||
log.info(`» selected configured agent: ${agentName}`);
|
|
||||||
return { agentName, agent };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableAgents = getAvailableAgents(inputs);
|
const availableAgents = getAvailableAgents(inputs);
|
||||||
const availableAgentNames = availableAgents.map((agent) => agent.name).join(", ");
|
|
||||||
log.debug(`» available agents: ${availableAgentNames || "none"}`);
|
|
||||||
|
|
||||||
if (availableAgents.length === 0) {
|
if (availableAgents.length === 0) {
|
||||||
// this will be caught and reported later in validateApiKey
|
|
||||||
throw new Error("no agents available - missing API keys");
|
throw new Error("no agents available - missing API keys");
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentName = availableAgents[0].name;
|
|
||||||
const agent = availableAgents[0];
|
const agent = availableAgents[0];
|
||||||
log.info(`» no agent configured, defaulting to first available agent: ${agentName}`);
|
log.info(`No agent configured, defaulting to first available agent: ${agent.name}`);
|
||||||
return { agentName, agent };
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupTempDirectory(ctx: Context): Promise<void> {
|
async function createTempDirectory(): Promise<string> {
|
||||||
ctx.sharedTempDir = await mkdtemp(join(tmpdir(), "pullfrog-"));
|
const sharedTempDir = await mkdtemp(join(tmpdir(), "pullfrog-"));
|
||||||
process.env.PULLFROG_TEMP_DIR = ctx.sharedTempDir;
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
||||||
log.debug(`» created temp dir at ${ctx.sharedTempDir}`);
|
log.info(`📂 PULLFROG_TEMP_DIR has been created at ${sharedTempDir}`);
|
||||||
|
return sharedTempDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePayload(inputs: Inputs): Payload {
|
function parsePayload(inputs: Inputs): Payload {
|
||||||
@@ -422,90 +447,70 @@ function parsePayload(inputs: Inputs): Payload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startMcpServer(ctx: Context): Promise<void> {
|
async function installAgentCli(params: { agent: Agent; token: string }): Promise<string> {
|
||||||
const runId = process.env.GITHUB_RUN_ID || "";
|
|
||||||
ctx.runId = runId;
|
|
||||||
|
|
||||||
// fetch the pre-created progress comment ID from the database
|
|
||||||
// this must be set BEFORE starting the MCP server so comment.ts can read it
|
|
||||||
if (runId) {
|
|
||||||
const workflowRunInfo = await fetchWorkflowRunInfo(ctx.runId);
|
|
||||||
if (workflowRunInfo.progressCommentId) {
|
|
||||||
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
|
||||||
log.info(`» using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch job ID by matching GITHUB_JOB name
|
|
||||||
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(`» found job ID: ${ctx.jobId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url, close } = await startMcpHttpServer(ctx);
|
|
||||||
ctx.mcpServerUrl = url;
|
|
||||||
ctx.mcpServerClose = close;
|
|
||||||
log.info(`» MCP server started at ${url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupMcpServers(ctx: Context): void {
|
|
||||||
ctx.mcpServers = createMcpConfigs(ctx.mcpServerUrl);
|
|
||||||
log.debug(`» MCP config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installAgentCli(ctx: Context): Promise<void> {
|
|
||||||
// gemini is the only agent that needs githubInstallationToken for install
|
// gemini is the only agent that needs githubInstallationToken for install
|
||||||
if (ctx.agentName === "gemini") {
|
if (params.agent.name === "gemini") {
|
||||||
ctx.cliPath = await ctx.agent.install(ctx.githubInstallationToken);
|
return params.agent.install(params.token);
|
||||||
} else {
|
|
||||||
ctx.cliPath = await ctx.agent.install();
|
|
||||||
}
|
}
|
||||||
|
return params.agent.install();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateApiKey(ctx: Context): Promise<void> {
|
function collectApiKeys(agent: Agent, inputs: Inputs): Record<string, string> {
|
||||||
// collect all matching API keys for this agent
|
|
||||||
const apiKeys: Record<string, string> = {};
|
const apiKeys: Record<string, string> = {};
|
||||||
for (const inputKey of ctx.agent.apiKeyNames) {
|
const inputsRecord = inputs as Record<string, string | undefined>;
|
||||||
const value = ctx.inputs[inputKey];
|
|
||||||
|
for (const inputKey of agent.apiKeyNames) {
|
||||||
|
const value = inputsRecord[inputKey];
|
||||||
if (value) {
|
if (value) {
|
||||||
apiKeys[inputKey] = value;
|
apiKeys[inputKey] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for OpenCode: if no keys found in inputs, check process.env for any API_KEY variables
|
// for OpenCode: also check process.env for any API_KEY variables
|
||||||
if (ctx.agentName === "opencode" && Object.keys(apiKeys).length === 0) {
|
if (agent.name === "opencode" && Object.keys(apiKeys).length === 0) {
|
||||||
for (const [key, value] of Object.entries(process.env)) {
|
for (const [key, value] of Object.entries(process.env)) {
|
||||||
if (value && typeof value === "string" && key.includes("API_KEY")) {
|
if (value && typeof value === "string" && key.includes("API_KEY")) {
|
||||||
// convert env var name back to input key format (lowercase with underscores)
|
apiKeys[key.toLowerCase()] = value;
|
||||||
const inputKey = key.toLowerCase();
|
|
||||||
apiKeys[inputKey] = value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return apiKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateApiKey(params: {
|
||||||
|
agent: Agent;
|
||||||
|
inputs: Inputs;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
}): ApiKeySetup {
|
||||||
|
const apiKeys = collectApiKeys(params.agent, params.inputs);
|
||||||
|
|
||||||
if (Object.keys(apiKeys).length === 0) {
|
if (Object.keys(apiKeys).length === 0) {
|
||||||
await throwMissingApiKeyError(ctx);
|
return {
|
||||||
// unreachable - throwMissingApiKeyError always throws
|
success: false,
|
||||||
return;
|
error: buildMissingApiKeyError({
|
||||||
|
agent: params.agent,
|
||||||
|
owner: params.owner,
|
||||||
|
name: params.name,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep apiKey for backward compat (first available key)
|
return {
|
||||||
ctx.apiKey = Object.values(apiKeys)[0];
|
success: true,
|
||||||
ctx.apiKeys = apiKeys;
|
apiKey: Object.values(apiKeys)[0],
|
||||||
|
apiKeys,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAgent(ctx: Context): Promise<AgentResult> {
|
async function runAgent(ctx: AgentContext): Promise<AgentResult> {
|
||||||
log.info(`» running ${ctx.agentName}...`);
|
log.info(`Running ${ctx.agent.name}...`);
|
||||||
log.box(ctx.payload.prompt.trim(), { title: "Prompt" });
|
// strip context from event
|
||||||
|
const { context: _context, ...eventWithoutContext } = ctx.payload.event;
|
||||||
|
// format: prompt + two newlines + TOON encoded event
|
||||||
|
const promptContent = `${ctx.payload.prompt}\n\n${toonEncode(eventWithoutContext)}`;
|
||||||
|
log.box(promptContent, { title: "Prompt" });
|
||||||
|
|
||||||
return ctx.agent.run({
|
return ctx.agent.run({
|
||||||
payload: ctx.payload,
|
payload: ctx.payload,
|
||||||
|
|||||||
+3
-3
@@ -1,18 +1,18 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const GetCheckSuiteLogs = type({
|
export const 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"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function GetCheckSuiteLogsTool(ctx: Context) {
|
export function GetCheckSuiteLogsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_check_suite_logs",
|
name: "get_check_suite_logs",
|
||||||
description:
|
description:
|
||||||
"get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
|
"get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
|
||||||
parameters: GetCheckSuiteLogs,
|
parameters: GetCheckSuiteLogs,
|
||||||
execute: execute(ctx, async ({ check_suite_id }) => {
|
execute: execute(async ({ check_suite_id }) => {
|
||||||
// get workflow runs for this specific check suite
|
// get workflow runs for this specific check suite
|
||||||
const workflowRuns = await ctx.octokit.paginate(
|
const workflowRuns = await ctx.octokit.paginate(
|
||||||
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
|
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
|
||||||
|
|||||||
+43
-18
@@ -1,5 +1,6 @@
|
|||||||
|
import type { Octokit } from "@octokit/rest";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { $ } from "../utils/shell.ts";
|
import { $ } from "../utils/shell.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
@@ -20,23 +21,39 @@ export type CheckoutPrResult = {
|
|||||||
headRepo: string;
|
headRepo: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CheckoutPrBranchParams {
|
||||||
|
octokit: Octokit;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
token: string;
|
||||||
|
pullNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckoutPrBranchResult {
|
||||||
|
prNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared helper to checkout a PR branch and configure fork remotes.
|
* Shared helper to checkout a PR branch and configure fork remotes.
|
||||||
* Assumes origin remote is already configured with authentication.
|
* Assumes origin remote is already configured with authentication.
|
||||||
|
* Returns the PR number for caller to set on toolState.
|
||||||
*/
|
*/
|
||||||
export async function checkoutPrBranch(ctx: Context, pull_number: number): Promise<void> {
|
export async function checkoutPrBranch(
|
||||||
log.debug(`🔀 checking out PR #${pull_number}...`);
|
params: CheckoutPrBranchParams
|
||||||
|
): Promise<CheckoutPrBranchResult> {
|
||||||
|
const { octokit, owner, name, token, pullNumber } = params;
|
||||||
|
log.info(`🔀 checking out PR #${pullNumber}...`);
|
||||||
|
|
||||||
// fetch PR metadata
|
// fetch PR metadata
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await octokit.rest.pulls.get({
|
||||||
owner: ctx.owner,
|
owner,
|
||||||
repo: ctx.name,
|
repo: name,
|
||||||
pull_number,
|
pull_number: pullNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
const headRepo = pr.data.head.repo;
|
const headRepo = pr.data.head.repo;
|
||||||
if (!headRepo) {
|
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 isFork = headRepo.full_name !== pr.data.base.repo.full_name;
|
||||||
@@ -59,12 +76,12 @@ export async function checkoutPrBranch(ctx: Context, pull_number: number): Promi
|
|||||||
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
||||||
|
|
||||||
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
|
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
|
||||||
log.debug(`🌿 fetching PR #${pull_number} (${headBranch})...`);
|
log.debug(`🌿 fetching PR #${pullNumber} (${headBranch})...`);
|
||||||
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
|
$("git", ["fetch", "--no-tags", "origin", `pull/${pullNumber}/head:${headBranch}`]);
|
||||||
|
|
||||||
// checkout the branch
|
// checkout the branch
|
||||||
$("git", ["checkout", headBranch]);
|
$("git", ["checkout", headBranch]);
|
||||||
log.debug(`✓ checked out PR #${pull_number}`);
|
log.debug(`✓ checked out PR #${pullNumber}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure base branch is fetched (needed for diff operations)
|
// ensure base branch is fetched (needed for diff operations)
|
||||||
@@ -78,8 +95,8 @@ export async function checkoutPrBranch(ctx: Context, pull_number: number): Promi
|
|||||||
// NOTE: This always runs regardless of alreadyOnBranch, because setupGit doesn't configure
|
// NOTE: This always runs regardless of alreadyOnBranch, because setupGit doesn't configure
|
||||||
// fork remotes. This ensures fork PRs can push even when checkout_pr is called after setupGit.
|
// fork remotes. This ensures fork PRs can push even when checkout_pr is called after setupGit.
|
||||||
if (isFork) {
|
if (isFork) {
|
||||||
const remoteName = `pr-${pull_number}`;
|
const remoteName = `pr-${pullNumber}`;
|
||||||
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
|
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
|
||||||
|
|
||||||
// add fork as a named remote (ignore error if already exists)
|
// add fork as a named remote (ignore error if already exists)
|
||||||
try {
|
try {
|
||||||
@@ -107,18 +124,26 @@ export async function checkoutPrBranch(ctx: Context, pull_number: number): Promi
|
|||||||
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
|
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set PR context
|
return { prNumber: pullNumber };
|
||||||
ctx.toolState.prNumber = pull_number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckoutPrTool(ctx: Context) {
|
export function CheckoutPrTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "checkout_pr",
|
name: "checkout_pr",
|
||||||
description:
|
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.",
|
"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,
|
parameters: CheckoutPr,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
await checkoutPrBranch(ctx, pull_number);
|
const result = await checkoutPrBranch({
|
||||||
|
octokit: ctx.octokit,
|
||||||
|
owner: ctx.owner,
|
||||||
|
name: ctx.name,
|
||||||
|
token: ctx.githubInstallationToken,
|
||||||
|
pullNumber: pull_number,
|
||||||
|
});
|
||||||
|
|
||||||
|
// set prNumber on toolState
|
||||||
|
ctx.toolState.prNumber = result.prNumber;
|
||||||
|
|
||||||
// fetch PR metadata to return result
|
// fetch PR metadata to return result
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
|
|||||||
+11
-11
@@ -2,7 +2,7 @@ import { Octokit } from "@octokit/rest";
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Payload } from "../external.ts";
|
import type { Payload } from "../external.ts";
|
||||||
import { agentsManifest } from "../external.ts";
|
import { agentsManifest } from "../external.ts";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { fetchWorkflowRunInfo } from "../utils/api.ts";
|
import { fetchWorkflowRunInfo } from "../utils/api.ts";
|
||||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||||
import { getGitHubInstallationToken, parseRepoContext } from "../utils/github.ts";
|
import { getGitHubInstallationToken, parseRepoContext } from "../utils/github.ts";
|
||||||
@@ -66,13 +66,13 @@ export const Comment = type({
|
|||||||
body: type.string.describe("the comment body content"),
|
body: type.string.describe("the comment body content"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function CreateCommentTool(ctx: Context) {
|
export function CreateCommentTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_issue_comment",
|
name: "create_issue_comment",
|
||||||
description:
|
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.",
|
"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,
|
parameters: Comment,
|
||||||
execute: execute(ctx, async ({ issueNumber, body }) => {
|
execute: execute(async ({ issueNumber, body }) => {
|
||||||
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
||||||
|
|
||||||
const result = await ctx.octokit.rest.issues.createComment({
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
@@ -97,12 +97,12 @@ export const EditComment = type({
|
|||||||
body: type.string.describe("the new comment body content"),
|
body: type.string.describe("the new comment body content"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function EditCommentTool(ctx: Context) {
|
export function EditCommentTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "edit_issue_comment",
|
name: "edit_issue_comment",
|
||||||
description: "Edit a GitHub issue comment by its ID",
|
description: "Edit a GitHub issue comment by its ID",
|
||||||
parameters: EditComment,
|
parameters: EditComment,
|
||||||
execute: execute(ctx, async ({ commentId, body }) => {
|
execute: execute(async ({ commentId, body }) => {
|
||||||
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
||||||
|
|
||||||
const result = await ctx.octokit.rest.issues.updateComment({
|
const result = await ctx.octokit.rest.issues.updateComment({
|
||||||
@@ -170,7 +170,7 @@ export const ReportProgress = type({
|
|||||||
* Returns result data if successful, undefined if comment cannot be created.
|
* Returns result data if successful, undefined if comment cannot be created.
|
||||||
*/
|
*/
|
||||||
export async function reportProgress(
|
export async function reportProgress(
|
||||||
ctx: Context,
|
ctx: ToolContext,
|
||||||
{ body }: { body: string }
|
{ body }: { body: string }
|
||||||
): Promise<
|
): Promise<
|
||||||
| {
|
| {
|
||||||
@@ -231,13 +231,13 @@ export async function reportProgress(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportProgressTool(ctx: Context) {
|
export function ReportProgressTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "report_progress",
|
name: "report_progress",
|
||||||
description:
|
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.",
|
"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,
|
parameters: ReportProgress,
|
||||||
execute: execute(ctx, async ({ body }) => {
|
execute: execute(async ({ body }) => {
|
||||||
const result = await reportProgress(ctx, { body });
|
const result = await reportProgress(ctx, { body });
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -269,7 +269,7 @@ export function wasProgressCommentUpdated(): boolean {
|
|||||||
* Delete the progress comment if it exists.
|
* Delete the progress comment if it exists.
|
||||||
* Used after submitting a PR review since the review body contains all necessary info.
|
* Used after submitting a PR review since the review body contains all necessary info.
|
||||||
*/
|
*/
|
||||||
export async function deleteProgressComment(ctx: Context): Promise<boolean> {
|
export async function deleteProgressComment(ctx: ToolContext): Promise<boolean> {
|
||||||
const existingCommentId = getProgressCommentId();
|
const existingCommentId = getProgressCommentId();
|
||||||
if (!existingCommentId) {
|
if (!existingCommentId) {
|
||||||
return false;
|
return false;
|
||||||
@@ -380,13 +380,13 @@ export const ReplyToReviewComment = type({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ReplyToReviewCommentTool(ctx: Context) {
|
export function ReplyToReviewCommentTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "reply_to_review_comment",
|
name: "reply_to_review_comment",
|
||||||
description:
|
description:
|
||||||
"Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
|
"Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
|
||||||
parameters: ReplyToReviewComment,
|
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 bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
|
||||||
|
|
||||||
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
||||||
|
|||||||
+3
-3
@@ -1,17 +1,17 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { $ } from "../utils/shell.ts";
|
import { $ } from "../utils/shell.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const DebugShellCommand = type({});
|
export const DebugShellCommand = type({});
|
||||||
|
|
||||||
export function DebugShellCommandTool(_ctx: Context) {
|
export function DebugShellCommandTool(_ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "debug_shell_command",
|
name: "debug_shell_command",
|
||||||
description:
|
description:
|
||||||
"debug tool: runs 'git status' and returns the output. use this to test shell command execution in the MCP server.",
|
"debug tool: runs 'git status' and returns the output. use this to test shell command execution in the MCP server.",
|
||||||
parameters: DebugShellCommand,
|
parameters: DebugShellCommand,
|
||||||
execute: execute(_ctx, async () => {
|
execute: execute(async () => {
|
||||||
const result = $("git", ["status"]);
|
const result = $("git", ["status"]);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { relative, resolve } from "node:path";
|
import { relative, resolve } from "node:path";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
|
import type { ToolContext } from "../main.ts";
|
||||||
import { $ } from "../utils/shell.ts";
|
import { $ } from "../utils/shell.ts";
|
||||||
import type { Context } from "../main.ts";
|
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const ListFiles = type({
|
export const ListFiles = type({
|
||||||
@@ -10,13 +10,13 @@ export const ListFiles = type({
|
|||||||
.default("."),
|
.default("."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ListFilesTool(_ctx: Context) {
|
export function ListFilesTool(_ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "list_files",
|
name: "list_files",
|
||||||
description:
|
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.",
|
"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,
|
parameters: ListFiles,
|
||||||
execute: execute(_ctx, async ({ path }: { path?: string }) => {
|
execute: execute(async ({ path }: { path?: string }) => {
|
||||||
const pathStr = path ?? ".";
|
const pathStr = path ?? ".";
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -1,11 +1,11 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { containsSecrets } from "../utils/secrets.ts";
|
import { containsSecrets } from "../utils/secrets.ts";
|
||||||
import { $ } from "../utils/shell.ts";
|
import { $ } from "../utils/shell.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export function CreateBranchTool(ctx: Context) {
|
export function CreateBranchTool(ctx: ToolContext) {
|
||||||
const defaultBranch = ctx.repo.default_branch || "main";
|
const defaultBranch = ctx.repo.default_branch || "main";
|
||||||
|
|
||||||
const CreateBranch = type({
|
const CreateBranch = type({
|
||||||
@@ -22,7 +22,7 @@ export function CreateBranchTool(ctx: Context) {
|
|||||||
description:
|
description:
|
||||||
"Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
|
"Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
|
||||||
parameters: CreateBranch,
|
parameters: CreateBranch,
|
||||||
execute: execute(ctx, async ({ branchName, baseBranch }) => {
|
execute: execute(async ({ branchName, baseBranch }) => {
|
||||||
// baseBranch should always be defined due to default, but TypeScript needs help
|
// baseBranch should always be defined due to default, but TypeScript needs help
|
||||||
const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";
|
const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";
|
||||||
|
|
||||||
@@ -70,13 +70,13 @@ export const CommitFiles = type({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function CommitFilesTool(ctx: Context) {
|
export function CommitFilesTool(_ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "commit_files",
|
name: "commit_files",
|
||||||
description:
|
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.",
|
"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,
|
parameters: CommitFiles,
|
||||||
execute: execute(ctx, async ({ message, files }) => {
|
execute: execute(async ({ message, files }) => {
|
||||||
// validate commit message for secrets
|
// validate commit message for secrets
|
||||||
if (containsSecrets(message)) {
|
if (containsSecrets(message)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -141,13 +141,13 @@ export const PushBranch = type({
|
|||||||
force: type.boolean.describe("Force push (use with caution)").default(false),
|
force: type.boolean.describe("Force push (use with caution)").default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function PushBranchTool(_ctx: Context) {
|
export function PushBranchTool(_ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "push_branch",
|
name: "push_branch",
|
||||||
description:
|
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.",
|
"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,
|
parameters: PushBranch,
|
||||||
execute: execute(_ctx, async ({ branchName, force }) => {
|
execute: execute(async ({ branchName, force }) => {
|
||||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||||
|
|
||||||
// check if branch has a configured pushRemote
|
// check if branch has a configured pushRemote
|
||||||
|
|||||||
+6
-4
@@ -1,5 +1,5 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const Issue = type({
|
export const Issue = type({
|
||||||
@@ -15,12 +15,12 @@ export const Issue = type({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function IssueTool(ctx: Context) {
|
export function IssueTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_issue",
|
name: "create_issue",
|
||||||
description: "Create a new GitHub issue",
|
description: "Create a new GitHub issue",
|
||||||
parameters: 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({
|
const result = await ctx.octokit.rest.issues.create({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
@@ -37,7 +37,9 @@ export function IssueTool(ctx: Context) {
|
|||||||
url: result.data.html_url,
|
url: result.data.html_url,
|
||||||
title: result.data.title,
|
title: result.data.title,
|
||||||
state: result.data.state,
|
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),
|
assignees: result.data.assignees?.map((assignee) => assignee.login),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const GetIssueComments = type({
|
export const GetIssueComments = type({
|
||||||
issue_number: type.number.describe("The issue number to get comments for"),
|
issue_number: type.number.describe("The issue number to get comments for"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function GetIssueCommentsTool(ctx: Context) {
|
export function GetIssueCommentsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_issue_comments",
|
name: "get_issue_comments",
|
||||||
description:
|
description:
|
||||||
"Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
"Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
||||||
parameters: GetIssueComments,
|
parameters: GetIssueComments,
|
||||||
execute: execute(ctx, async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
// set issue context
|
// set issue context
|
||||||
ctx.toolState.issueNumber = issue_number;
|
ctx.toolState.issueNumber = issue_number;
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,18 +1,18 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const GetIssueEvents = type({
|
export const GetIssueEvents = type({
|
||||||
issue_number: type.number.describe("The issue number to get events for"),
|
issue_number: type.number.describe("The issue number to get events for"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function GetIssueEventsTool(ctx: Context) {
|
export function GetIssueEventsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_issue_events",
|
name: "get_issue_events",
|
||||||
description:
|
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.",
|
"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,
|
parameters: GetIssueEvents,
|
||||||
execute: execute(ctx, async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
// set issue context
|
// set issue context
|
||||||
ctx.toolState.issueNumber = issue_number;
|
ctx.toolState.issueNumber = issue_number;
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,17 +1,17 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const IssueInfo = type({
|
export const IssueInfo = type({
|
||||||
issue_number: type.number.describe("The issue number to fetch"),
|
issue_number: type.number.describe("The issue number to fetch"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function IssueInfoTool(ctx: Context) {
|
export function IssueInfoTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_issue",
|
name: "get_issue",
|
||||||
description: "Retrieve GitHub issue information by issue number",
|
description: "Retrieve GitHub issue information by issue number",
|
||||||
parameters: IssueInfo,
|
parameters: IssueInfo,
|
||||||
execute: execute(ctx, async ({ issue_number }) => {
|
execute: execute(async ({ issue_number }) => {
|
||||||
const issue = await ctx.octokit.rest.issues.get({
|
const issue = await ctx.octokit.rest.issues.get({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const AddLabelsParams = type({
|
export const AddLabelsParams = type({
|
||||||
@@ -7,13 +7,13 @@ export const AddLabelsParams = type({
|
|||||||
labels: type.string.array().atLeastLength(1).describe("array of label names to add"),
|
labels: type.string.array().atLeastLength(1).describe("array of label names to add"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AddLabelsTool(ctx: Context) {
|
export function AddLabelsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "add_labels",
|
name: "add_labels",
|
||||||
description:
|
description:
|
||||||
"Add labels to a GitHub issue or pull request. Only use labels that already exist in the repository.",
|
"Add labels to a GitHub issue or pull request. Only use labels that already exist in the repository.",
|
||||||
parameters: AddLabelsParams,
|
parameters: AddLabelsParams,
|
||||||
execute: execute(ctx, async ({ issue_number, labels }) => {
|
execute: execute(async ({ issue_number, labels }) => {
|
||||||
const result = await ctx.octokit.rest.issues.addLabels({
|
const result = await ctx.octokit.rest.issues.addLabels({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { agentsManifest } from "../external.ts";
|
import { agentsManifest } from "../external.ts";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { containsSecrets } from "../utils/secrets.ts";
|
import { containsSecrets } from "../utils/secrets.ts";
|
||||||
@@ -13,7 +13,7 @@ export const PullRequest = type({
|
|||||||
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
|
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildPrBodyWithFooter(ctx: Context, body: string): string {
|
function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
|
||||||
const agentName = ctx.payload.agent;
|
const agentName = ctx.payload.agent;
|
||||||
const agentInfo = agentName ? agentsManifest[agentName] : null;
|
const agentInfo = agentName ? agentsManifest[agentName] : null;
|
||||||
|
|
||||||
@@ -29,12 +29,12 @@ function buildPrBodyWithFooter(ctx: Context, body: string): string {
|
|||||||
return `${bodyWithoutFooter}${footer}`;
|
return `${bodyWithoutFooter}${footer}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PullRequestTool(ctx: Context) {
|
export function PullRequestTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_pull_request",
|
name: "create_pull_request",
|
||||||
description: "Create a pull request from the current branch",
|
description: "Create a pull request from the current branch",
|
||||||
parameters: PullRequest,
|
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 });
|
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||||
log.debug(`Current branch: ${currentBranch}`);
|
log.debug(`Current branch: ${currentBranch}`);
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,18 +1,18 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const PullRequestInfo = type({
|
export const PullRequestInfo = type({
|
||||||
pull_number: type.number.describe("The pull request number to fetch"),
|
pull_number: type.number.describe("The pull request number to fetch"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function PullRequestInfoTool(ctx: Context) {
|
export function PullRequestInfoTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_pull_request",
|
name: "get_pull_request",
|
||||||
description:
|
description:
|
||||||
"Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
|
"Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
|
||||||
parameters: PullRequestInfo,
|
parameters: PullRequestInfo,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const pr = await ctx.octokit.rest.pulls.get({
|
const pr = await ctx.octokit.rest.pulls.get({
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
|
|||||||
+10
-10
@@ -3,7 +3,7 @@ import { writeFileSync } from "node:fs";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts";
|
import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { deleteProgressComment } from "./comment.ts";
|
import { deleteProgressComment } from "./comment.ts";
|
||||||
@@ -37,7 +37,7 @@ type AddPullRequestReviewThreadResponse = {
|
|||||||
|
|
||||||
// helper to find existing pending review for the authenticated user
|
// helper to find existing pending review for the authenticated user
|
||||||
async function findPendingReview(
|
async function findPendingReview(
|
||||||
ctx: Context,
|
ctx: ToolContext,
|
||||||
pull_number: number
|
pull_number: number
|
||||||
): Promise<{ id: number; node_id: string } | null> {
|
): Promise<{ id: number; node_id: string } | null> {
|
||||||
const reviews = await ctx.octokit.rest.pulls.listReviews({
|
const reviews = await ctx.octokit.rest.pulls.listReviews({
|
||||||
@@ -61,13 +61,13 @@ export const StartReview = type({
|
|||||||
pull_number: type.number.describe("The pull request number to review"),
|
pull_number: type.number.describe("The pull request number to review"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function StartReviewTool(ctx: Context) {
|
export function StartReviewTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "start_review",
|
name: "start_review",
|
||||||
description:
|
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.",
|
"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,
|
parameters: StartReview,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
// check if review already started in this session
|
// check if review already started in this session
|
||||||
if (ctx.toolState.review) {
|
if (ctx.toolState.review) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -154,13 +154,13 @@ export const AddReviewComment = type({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AddReviewCommentTool(ctx: Context) {
|
export function AddReviewCommentTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "add_review_comment",
|
name: "add_review_comment",
|
||||||
description:
|
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.",
|
"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,
|
parameters: AddReviewComment,
|
||||||
execute: execute(ctx, async ({ path, line, body, side }) => {
|
execute: execute(async ({ path, line, body, side }) => {
|
||||||
// check if review started
|
// check if review started
|
||||||
if (!ctx.toolState.review) {
|
if (!ctx.toolState.review) {
|
||||||
throw new Error("No review session started. Call start_review first.");
|
throw new Error("No review session started. Call start_review first.");
|
||||||
@@ -195,13 +195,13 @@ export const SubmitReview = type({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function SubmitReviewTool(ctx: Context) {
|
export function SubmitReviewTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "submit_review",
|
name: "submit_review",
|
||||||
description:
|
description:
|
||||||
"Submit the current review session. All comments added via add_review_comment will be published. Must call start_review first.",
|
"Submit the current review session. All comments added via add_review_comment will be published. Must call start_review first.",
|
||||||
parameters: SubmitReview,
|
parameters: SubmitReview,
|
||||||
execute: execute(ctx, async ({ body }) => {
|
execute: execute(async ({ body }) => {
|
||||||
// check if review started
|
// check if review started
|
||||||
if (!ctx.toolState.review) {
|
if (!ctx.toolState.review) {
|
||||||
throw new Error("No review session started. Call start_review first.");
|
throw new Error("No review session started. Call start_review first.");
|
||||||
@@ -288,7 +288,7 @@ export const Review = type({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ReviewTool(ctx: Context) {
|
export function ReviewTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "submit_pull_request_review",
|
name: "submit_pull_request_review",
|
||||||
description:
|
description:
|
||||||
@@ -297,7 +297,7 @@ export function ReviewTool(ctx: Context) {
|
|||||||
"IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. " +
|
"IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. " +
|
||||||
"Only use 'body' for a 1-2 sentence summary with urgency and critical callouts.",
|
"Only use 'body' for a 1-2 sentence summary with urgency and critical callouts.",
|
||||||
parameters: Review,
|
parameters: Review,
|
||||||
execute: execute(ctx, async ({ pull_number, body, commit_id, comments = [] }) => {
|
execute: execute(async ({ pull_number, body, commit_id, comments = [] }) => {
|
||||||
// set PR context
|
// set PR context
|
||||||
ctx.toolState.prNumber = pull_number;
|
ctx.toolState.prNumber = pull_number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
// graphql query to fetch all review threads with comments and replies
|
// graphql query to fetch all review threads with comments and replies
|
||||||
@@ -86,13 +86,13 @@ export const GetReviewComments = type({
|
|||||||
review_id: type.number.describe("The review ID to get comments for"),
|
review_id: type.number.describe("The review ID to get comments for"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function GetReviewCommentsTool(ctx: Context) {
|
export function GetReviewCommentsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "get_review_comments",
|
name: "get_review_comments",
|
||||||
description:
|
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.",
|
"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,
|
parameters: GetReviewComments,
|
||||||
execute: execute(ctx, async ({ pull_number, review_id }) => {
|
execute: execute(async ({ pull_number, review_id }) => {
|
||||||
// fetch all review threads using graphql
|
// fetch all review threads using graphql
|
||||||
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
|
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -189,13 +189,13 @@ export const ListPullRequestReviews = type({
|
|||||||
pull_number: type.number.describe("The pull request number to list reviews for"),
|
pull_number: type.number.describe("The pull request number to list reviews for"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ListPullRequestReviewsTool(ctx: Context) {
|
export function ListPullRequestReviewsTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "list_pull_request_reviews",
|
name: "list_pull_request_reviews",
|
||||||
description:
|
description:
|
||||||
"List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
"List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
||||||
parameters: ListPullRequestReviews,
|
parameters: ListPullRequestReviews,
|
||||||
execute: execute(ctx, async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
repo: ctx.name,
|
repo: ctx.name,
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
export const SelectMode = type({
|
export const SelectMode = type({
|
||||||
@@ -8,13 +8,13 @@ export const SelectMode = type({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function SelectModeTool(ctx: Context) {
|
export function SelectModeTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "select_mode",
|
name: "select_mode",
|
||||||
description:
|
description:
|
||||||
"Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
|
"Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
|
||||||
parameters: SelectMode,
|
parameters: SelectMode,
|
||||||
execute: execute(ctx, async ({ modeName }) => {
|
execute: execute(async ({ modeName }) => {
|
||||||
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
|
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
|
||||||
|
|
||||||
if (!selectedMode) {
|
if (!selectedMode) {
|
||||||
|
|||||||
+5
-6
@@ -1,9 +1,9 @@
|
|||||||
import "./arkConfig.ts";
|
import "./arkConfig.ts";
|
||||||
// this must be imported first
|
|
||||||
import { createServer } from "node:net";
|
import { createServer } from "node:net";
|
||||||
|
// this must be imported first
|
||||||
import { FastMCP, type Tool } from "fastmcp";
|
import { FastMCP, type Tool } from "fastmcp";
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
import { CheckoutPrTool } from "./checkout.ts";
|
import { CheckoutPrTool } from "./checkout.ts";
|
||||||
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +25,7 @@ import { PullRequestInfoTool } from "./prInfo.ts";
|
|||||||
import { AddReviewCommentTool, StartReviewTool, SubmitReviewTool } from "./review.ts";
|
import { AddReviewCommentTool, StartReviewTool, SubmitReviewTool } from "./review.ts";
|
||||||
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
|
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
|
||||||
import { SelectModeTool } from "./selectMode.ts";
|
import { SelectModeTool } from "./selectMode.ts";
|
||||||
import { addTools, isProgressCommentDisabled } from "./shared.ts";
|
import { addTools } from "./shared.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find an available port starting from the given port
|
* Find an available port starting from the given port
|
||||||
@@ -60,7 +60,7 @@ async function findAvailablePort(startPort: number): Promise<number> {
|
|||||||
* Start the MCP HTTP server and return the URL and close function
|
* Start the MCP HTTP server and return the URL and close function
|
||||||
*/
|
*/
|
||||||
export async function startMcpHttpServer(
|
export async function startMcpHttpServer(
|
||||||
ctx: Context
|
ctx: ToolContext
|
||||||
): Promise<{ url: string; close: () => Promise<void> }> {
|
): Promise<{ url: string; close: () => Promise<void> }> {
|
||||||
const server = new FastMCP({
|
const server = new FastMCP({
|
||||||
name: ghPullfrogMcpName,
|
name: ghPullfrogMcpName,
|
||||||
@@ -95,8 +95,7 @@ export async function startMcpHttpServer(
|
|||||||
ListFilesTool(ctx),
|
ListFilesTool(ctx),
|
||||||
];
|
];
|
||||||
|
|
||||||
// only include ReportProgressTool if progress comment is not disabled
|
if (!ctx.payload.disableProgressComment) {
|
||||||
if (!isProgressCommentDisabled(ctx)) {
|
|
||||||
tools.push(ReportProgressTool(ctx));
|
tools.push(ReportProgressTool(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-8
@@ -1,6 +1,6 @@
|
|||||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||||
import type { FastMCP, Tool } from "fastmcp";
|
import type { FastMCP, Tool } from "fastmcp";
|
||||||
import type { Context } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
|
|
||||||
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => toolDef;
|
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => toolDef;
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export const handleToolError = (error: unknown): ToolResult => {
|
|||||||
* Helper to wrap a tool execute function with error handling.
|
* Helper to wrap a tool execute function with error handling.
|
||||||
* Captures ctx in closure so tools don't need to handle try/catch.
|
* Captures ctx in closure so tools don't need to handle try/catch.
|
||||||
*/
|
*/
|
||||||
export const execute = <T>(ctx: Context, fn: (params: T) => Promise<Record<string, any>>) => {
|
export const execute = <T>(fn: (params: T) => Promise<Record<string, any>>) => {
|
||||||
return async (params: T): Promise<ToolResult> => {
|
return async (params: T): Promise<ToolResult> => {
|
||||||
try {
|
try {
|
||||||
const result = await fn(params);
|
const result = await fn(params);
|
||||||
@@ -51,10 +51,6 @@ export const execute = <T>(ctx: Context, fn: (params: T) => Promise<Record<strin
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isProgressCommentDisabled(ctx: Context): boolean {
|
|
||||||
return ctx.payload.disableProgressComment === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize JSON schema to remove problematic fields that Gemini CLI/API can't handle
|
* Sanitize JSON schema to remove problematic fields that Gemini CLI/API can't handle
|
||||||
* - Removes $schema field (causes "no schema with key or ref" errors)
|
* - Removes $schema field (causes "no schema with key or ref" errors)
|
||||||
@@ -176,10 +172,10 @@ function sanitizeTool<T extends Tool<any, any>>(tool: T): T {
|
|||||||
} as T;
|
} as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addTools = (ctx: Context, server: FastMCP, tools: Tool<any, any>[]) => {
|
export const addTools = (ctx: ToolContext, server: FastMCP, tools: Tool<any, any>[]) => {
|
||||||
// sanitize schemas for gemini agent and opencode (when using Google API)
|
// sanitize schemas for gemini agent and opencode (when using Google API)
|
||||||
// both have issues with draft-2020-12 schemas and any_of enum constructs
|
// both have issues with draft-2020-12 schemas and any_of enum constructs
|
||||||
const shouldSanitize = ctx.agentName === "gemini" || ctx.agentName === "opencode";
|
const shouldSanitize = ctx.agent.name === "gemini" || ctx.agent.name === "opencode";
|
||||||
|
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
const processedTool = shouldSanitize ? sanitizeTool(tool) : tool;
|
const processedTool = shouldSanitize ? sanitizeTool(tool) : tool;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.144",
|
"version": "0.0.146",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
+27
-7
@@ -1,6 +1,8 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { existsSync, rmSync } from "node:fs";
|
import { existsSync, rmSync } from "node:fs";
|
||||||
import type { Context } from "../main.ts";
|
import type { Octokit } from "@octokit/rest";
|
||||||
|
import type { Payload } from "../external.ts";
|
||||||
|
import type { ToolState } from "../main.ts";
|
||||||
import { checkoutPrBranch } from "../mcp/checkout.ts";
|
import { checkoutPrBranch } from "../mcp/checkout.ts";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
import { $ } from "./shell.ts";
|
import { $ } from "./shell.ts";
|
||||||
@@ -71,6 +73,15 @@ export function setupGitConfig(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SetupGitAuthParams {
|
||||||
|
token: string;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
payload: Payload;
|
||||||
|
octokit: Octokit;
|
||||||
|
toolState: ToolState;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup git authentication for the repository.
|
* Setup git authentication for the repository.
|
||||||
* For PR events, uses the shared checkoutPrBranch helper (also used by checkout_pr MCP tool).
|
* For PR events, uses the shared checkoutPrBranch helper (also used by checkout_pr MCP tool).
|
||||||
@@ -80,7 +91,7 @@ export function setupGitConfig(): void {
|
|||||||
* - checkoutPrBranch sets per-branch pushRemote config for fork PRs
|
* - checkoutPrBranch sets per-branch pushRemote config for fork PRs
|
||||||
* - diff operations use: git diff origin/<base>..HEAD
|
* - diff operations use: git diff origin/<base>..HEAD
|
||||||
*/
|
*/
|
||||||
export async function setupGit(ctx: Context): Promise<void> {
|
export async function setupGitAuth(params: SetupGitAuthParams): Promise<void> {
|
||||||
const repoDir = process.cwd();
|
const repoDir = process.cwd();
|
||||||
|
|
||||||
log.info("» setting up git authentication...");
|
log.info("» setting up git authentication...");
|
||||||
@@ -97,20 +108,29 @@ export async function setupGit(ctx: Context): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// non-PR events: set up origin with token, stay on default branch
|
// non-PR events: set up origin with token, stay on default branch
|
||||||
if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
|
if (params.payload.event.is_pr !== true || !params.payload.event.issue_number) {
|
||||||
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
|
const originUrl = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
|
||||||
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
||||||
log.info("» updated origin URL with authentication token");
|
log.info("» updated origin URL with authentication token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PR event: checkout PR branch using shared helper
|
// PR event: checkout PR branch using shared helper
|
||||||
const prNumber = ctx.payload.event.issue_number;
|
const prNumber = params.payload.event.issue_number;
|
||||||
|
|
||||||
// ensure origin is configured with auth token before checkout
|
// ensure origin is configured with auth token before checkout
|
||||||
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
|
const originUrl = `https://x-access-token:${params.token}@github.com/${params.owner}/${params.name}.git`;
|
||||||
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
|
||||||
|
|
||||||
// use shared checkout helper (handles fork remotes, push config, etc.)
|
// use shared checkout helper (handles fork remotes, push config, etc.)
|
||||||
await checkoutPrBranch(ctx, prNumber);
|
const prContext = await checkoutPrBranch({
|
||||||
|
octokit: params.octokit,
|
||||||
|
owner: params.owner,
|
||||||
|
name: params.name,
|
||||||
|
token: params.token,
|
||||||
|
pullNumber: prNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// set prNumber on toolState (the only mutation)
|
||||||
|
params.toolState.prNumber = prContext.prNumber;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user