This commit is contained in:
Colin McDonnell
2026-01-16 18:43:09 +00:00
committed by pullfrog[bot]
parent 101c666610
commit 69b9b96ddd
37 changed files with 1005 additions and 1005 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ async function killProcessGroup(proc: ChildProcess): Promise<void> {
}
export function BashTool(ctx: ToolContext) {
const isPublicRepo = !ctx.repo.private;
const isPublicRepo = !ctx.repo.repo.private;
return tool({
name: "bash",
+6 -6
View File
@@ -17,8 +17,8 @@ export function GetCheckSuiteLogsTool(ctx: ToolContext) {
const workflowRuns = await ctx.octokit.paginate(
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
{
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
check_suite_id,
per_page: 100,
}
@@ -38,8 +38,8 @@ export function GetCheckSuiteLogsTool(ctx: ToolContext) {
const logsForRuns = await Promise.all(
failedRuns.map(async (run) => {
const jobs = await ctx.octokit.paginate(ctx.octokit.rest.actions.listJobsForWorkflowRun, {
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
run_id: run.id,
});
@@ -47,8 +47,8 @@ export function GetCheckSuiteLogsTool(ctx: ToolContext) {
jobs.map(async (job) => {
try {
const logsResponse = await ctx.octokit.rest.actions.downloadJobLogsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
job_id: job.id,
});
+6 -6
View File
@@ -221,8 +221,8 @@ export function CheckoutPrTool(ctx: ToolContext) {
execute: execute(async ({ pull_number }) => {
const result = await checkoutPrBranch({
octokit: ctx.octokit,
owner: ctx.owner,
name: ctx.name,
owner: ctx.repo.owner,
name: ctx.repo.name,
token: ctx.githubInstallationToken,
pullNumber: pull_number,
});
@@ -232,8 +232,8 @@ export function CheckoutPrTool(ctx: ToolContext) {
// fetch PR metadata to return result
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
});
@@ -244,8 +244,8 @@ export function CheckoutPrTool(ctx: ToolContext) {
// fetch PR files and format with line numbers
const filesResponse = await ctx.octokit.rest.pulls.listFiles({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
per_page: 100,
});
+23 -20
View File
@@ -106,8 +106,8 @@ export function CreateCommentTool(ctx: ToolContext) {
const bodyWithFooter = await addFooter(ctx, body);
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
issue_number: issueNumber,
body: bodyWithFooter,
});
@@ -136,8 +136,8 @@ export function EditCommentTool(ctx: ToolContext) {
const bodyWithFooter = await addFooter(ctx, body);
const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
comment_id: commentId,
body: bodyWithFooter,
});
@@ -211,14 +211,15 @@ export async function reportProgress(
| undefined
> {
const existingCommentId = getProgressCommentId();
const issueNumber = ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.event.issue_number;
const issueNumber =
ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number;
const isPlanMode = ctx.toolState.selectedMode === "Plan";
// if we already have a progress comment, update it
if (existingCommentId) {
const customParts =
isPlanMode && issueNumber !== undefined
? [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, existingCommentId)]
? [buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, existingCommentId)]
: undefined;
const bodyWithoutFooter = stripExistingFooter(body);
@@ -230,8 +231,8 @@ export async function reportProgress(
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
comment_id: existingCommentId,
body: bodyWithFooter,
});
@@ -259,8 +260,8 @@ export async function reportProgress(
const initialBody = await addFooter(ctx, body);
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
issue_number: issueNumber,
body: initialBody,
});
@@ -271,7 +272,9 @@ export async function reportProgress(
// if Plan mode, update the comment to add the "Implement plan" link
if (isPlanMode) {
const customParts = [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, result.data.id)];
const customParts = [
buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, result.data.id),
];
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
agent: ctx.agent,
@@ -281,8 +284,8 @@ export async function reportProgress(
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
const updateResult = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
comment_id: result.data.id,
body: bodyWithPlanLink,
});
@@ -353,8 +356,8 @@ export async function deleteProgressComment(ctx: ToolContext): Promise<boolean>
try {
await ctx.octokit.rest.issues.deleteComment({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
comment_id: existingCommentId,
});
} catch (error) {
@@ -375,7 +378,7 @@ export async function deleteProgressComment(ctx: ToolContext): Promise<boolean>
}
interface EnsureProgressCommentUpdatedParams {
disableProgressComment: boolean;
hasProgressComment: boolean;
}
/**
@@ -394,8 +397,8 @@ export async function ensureProgressCommentUpdated(
return;
}
// skip if progress comments are disabled
if (params.disableProgressComment) {
// skip if no progress comment was created for this run
if (!params.hasProgressComment) {
return;
}
@@ -485,8 +488,8 @@ export function ReplyToReviewCommentTool(ctx: ToolContext) {
const bodyWithFooter = await addFooter(ctx, body);
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
comment_id,
body: bodyWithFooter,
+2 -2
View File
@@ -6,7 +6,7 @@ import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
export function CreateBranchTool(ctx: ToolContext) {
const defaultBranch = ctx.repo.default_branch || "main";
const defaultBranch = ctx.repo.repo.default_branch || "main";
const CreateBranch = type({
branchName: type.string.describe(
@@ -24,7 +24,7 @@ export function CreateBranchTool(ctx: ToolContext) {
parameters: CreateBranch,
execute: execute(async ({ branchName, baseBranch }) => {
// 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.repo.default_branch || "main";
// validate branch name for secrets
if (containsSecrets(branchName)) {
+2 -2
View File
@@ -22,8 +22,8 @@ export function IssueTool(ctx: ToolContext) {
parameters: Issue,
execute: execute(async ({ title, body, labels, assignees }) => {
const result = await ctx.octokit.rest.issues.create({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
title: title,
body: body,
labels: labels ?? [],
+2 -2
View File
@@ -17,8 +17,8 @@ export function GetIssueCommentsTool(ctx: ToolContext) {
ctx.toolState.issueNumber = issue_number;
const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
issue_number,
});
+2 -2
View File
@@ -17,8 +17,8 @@ export function GetIssueEventsTool(ctx: ToolContext) {
ctx.toolState.issueNumber = issue_number;
const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
issue_number,
});
+2 -2
View File
@@ -13,8 +13,8 @@ export function IssueInfoTool(ctx: ToolContext) {
parameters: IssueInfo,
execute: execute(async ({ issue_number }) => {
const issue = await ctx.octokit.rest.issues.get({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
issue_number,
});
+2 -2
View File
@@ -15,8 +15,8 @@ export function AddLabelsTool(ctx: ToolContext) {
parameters: AddLabelsParams,
execute: execute(async ({ issue_number, labels }) => {
const result = await ctx.octokit.rest.issues.addLabels({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
issue_number,
labels,
});
+4 -4
View File
@@ -1,9 +1,9 @@
import { type } from "arktype";
import type { ToolContext } from "./server.ts";
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { log } from "../utils/cli.ts";
import { containsSecrets } from "../utils/secrets.ts";
import { $ } from "../utils/shell.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const PullRequest = type({
@@ -17,7 +17,7 @@ function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
triggeredBy: true,
agent: { displayName: ctx.agent.displayName, url: ctx.agent.url },
workflowRun: ctx.runId
? { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId }
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
: undefined,
});
@@ -56,8 +56,8 @@ export function CreatePullRequestTool(ctx: ToolContext) {
const bodyWithFooter = buildPrBodyWithFooter(ctx, body);
const result = await ctx.octokit.rest.pulls.create({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
title: title,
body: bodyWithFooter,
head: currentBranch,
+2 -2
View File
@@ -14,8 +14,8 @@ export function PullRequestInfoTool(ctx: ToolContext) {
parameters: PullRequestInfo,
execute: execute(async ({ pull_number }) => {
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
});
+20 -20
View File
@@ -57,8 +57,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
// compose the request
const params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"] = {
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
event: "COMMENT",
};
@@ -68,8 +68,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
} else {
// get the PR to determine the head commit if commit_id not provided
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
});
params.commit_id = pr.data.head.sha;
@@ -98,11 +98,11 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
// build quick links footer and update the review body
const apiUrl = process.env.API_URL || "https://pullfrog.com";
const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix&review_id=${reviewId}`;
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
const fixAllUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix&review_id=${reviewId}`;
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
const footer = buildPullfrogFooter({
workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId },
workflowRun: { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId },
customParts: [`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`],
});
@@ -110,8 +110,8 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
// update the review with the footer
await ctx.octokit.rest.pulls.updateReview({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
review_id: reviewId,
body: updatedBody,
@@ -171,8 +171,8 @@ async function findPendingReview(
pull_number: number
): Promise<{ id: number; node_id: string } | null> {
const reviews = await ctx.octokit.rest.pulls.listReviews({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
per_page: 100,
});
@@ -207,8 +207,8 @@ export function StartReviewTool(ctx: ToolContext) {
// get the PR to get head commit SHA
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
});
@@ -219,8 +219,8 @@ export function StartReviewTool(ctx: ToolContext) {
log.debug(`creating pending review for PR #${pull_number}...`);
try {
const result = await ctx.octokit.rest.pulls.createReview({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
commit_id: pr.data.head.sha,
// no 'event' = PENDING review
@@ -386,11 +386,11 @@ export function SubmitReviewTool(ctx: ToolContext) {
// build quick links footer
const apiUrl = process.env.API_URL || "https://pullfrog.com";
const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${ctx.toolState.prNumber}?action=fix&review_id=${reviewId}`;
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${ctx.toolState.prNumber}?action=fix-approved&review_id=${reviewId}`;
const fixAllUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${ctx.toolState.prNumber}?action=fix&review_id=${reviewId}`;
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${ctx.toolState.prNumber}?action=fix-approved&review_id=${reviewId}`;
const footer = buildPullfrogFooter({
workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId },
workflowRun: { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId },
customParts: [`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`],
});
@@ -398,8 +398,8 @@ export function SubmitReviewTool(ctx: ToolContext) {
// submit the pending review via REST
const result = await ctx.octokit.rest.pulls.submitReview({
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number: ctx.toolState.prNumber,
review_id: reviewId,
event: "COMMENT",
+4 -4
View File
@@ -95,8 +95,8 @@ export function GetReviewCommentsTool(ctx: ToolContext) {
execute: execute(async ({ pull_number, review_id }) => {
// fetch all review threads using graphql
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pullNumber: pull_number,
});
@@ -197,8 +197,8 @@ export function ListPullRequestReviewsTool(ctx: ToolContext) {
parameters: ListPullRequestReviews,
execute: execute(async ({ pull_number }) => {
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
owner: ctx.owner,
repo: ctx.name,
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
});
+12 -12
View File
@@ -1,12 +1,14 @@
import "./arkConfig.ts";
import { createServer } from "node:net";
// this must be imported first
import type { Octokit } from "@octokit/rest";
import { FastMCP, type Tool } from "fastmcp";
import type { Agent } from "../agents/index.ts";
import { ghPullfrogMcpName, type PayloadEvent } from "../external.ts";
import { ghPullfrogMcpName } from "../external.ts";
import type { Mode } from "../modes.ts";
import type { PrepResult } from "../prep/index.ts";
import type { OctokitWithPlugins } from "../utils/github.ts";
import type { ResolvedPayload } from "../utils/payload.ts";
import type { RepoData } from "../utils/repoData.ts";
export interface ToolState {
prNumber?: number;
@@ -24,22 +26,19 @@ export interface ToolState {
}
export interface ToolContext {
tools: ToolPermissions;
owner: string;
name: string;
repo: { default_branch: string; private: boolean };
repo: RepoData;
payload: ResolvedPayload;
octokit: OctokitWithPlugins;
githubInstallationToken: string;
octokit: Octokit;
agent: Agent;
event: PayloadEvent;
disableProgressComment: boolean;
modes: Mode[];
toolState: ToolState;
runId: string;
jobId: string | undefined;
/** true if a progress comment was pre-created for this run */
hasProgressComment: boolean;
}
import type { ToolPermissions } from "../agents/shared.ts";
import { BashTool } from "./bash.ts";
import { CheckoutPrTool } from "./checkout.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
@@ -137,11 +136,12 @@ export async function startMcpHttpServer(
// - "enabled": native bash + MCP bash
// - "restricted": MCP bash only (native blocked by agent)
// - "disabled": no bash at all
if (ctx.tools.bash !== "disabled") {
const bash = ctx.payload.bash ?? "enabled";
if (bash !== "disabled") {
tools.push(BashTool(ctx));
}
if (!ctx.disableProgressComment) {
if (ctx.hasProgressComment) {
tools.push(ReportProgressTool(ctx));
}