refactor main

This commit is contained in:
David Blass
2025-12-17 16:20:46 -05:00
parent 1f1c1602c5
commit 90ed2648be
24 changed files with 638 additions and 546 deletions
+3 -3
View File
@@ -1,18 +1,18 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const GetCheckSuiteLogs = type({
check_suite_id: type.number.describe("the id from check_suite.id"),
});
export function GetCheckSuiteLogsTool(ctx: Context) {
export function GetCheckSuiteLogsTool(ctx: ToolContext) {
return tool({
name: "get_check_suite_logs",
description:
"get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
parameters: GetCheckSuiteLogs,
execute: execute(ctx, async ({ check_suite_id }) => {
execute: execute(async ({ check_suite_id }) => {
// get workflow runs for this specific check suite
const workflowRuns = await ctx.octokit.paginate(
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
+43 -18
View File
@@ -1,5 +1,6 @@
import type { Octokit } from "@octokit/rest";
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { log } from "../utils/cli.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
@@ -20,23 +21,39 @@ export type CheckoutPrResult = {
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.
* 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> {
log.debug(`🔀 checking out PR #${pull_number}...`);
export async function checkoutPrBranch(
params: CheckoutPrBranchParams
): Promise<CheckoutPrBranchResult> {
const { octokit, owner, name, token, pullNumber } = params;
log.info(`🔀 checking out PR #${pullNumber}...`);
// fetch PR metadata
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number,
const pr = await octokit.rest.pulls.get({
owner,
repo: name,
pull_number: pullNumber,
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${pull_number} source repository was deleted`);
throw new Error(`PR #${pullNumber} source repository was deleted`);
}
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
@@ -59,12 +76,12 @@ export async function checkoutPrBranch(ctx: Context, pull_number: number): Promi
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
log.debug(`🌿 fetching PR #${pull_number} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
log.debug(`🌿 fetching PR #${pullNumber} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pullNumber}/head:${headBranch}`]);
// checkout the branch
$("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)
@@ -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
// fork remotes. This ensures fork PRs can push even when checkout_pr is called after setupGit.
if (isFork) {
const remoteName = `pr-${pull_number}`;
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
const remoteName = `pr-${pullNumber}`;
const forkUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
// add fork as a named remote (ignore error if already exists)
try {
@@ -107,18 +124,26 @@ export async function checkoutPrBranch(ctx: Context, pull_number: number): Promi
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
}
// set PR context
ctx.toolState.prNumber = pull_number;
return { prNumber: pullNumber };
}
export function CheckoutPrTool(ctx: Context) {
export function CheckoutPrTool(ctx: ToolContext) {
return tool({
name: "checkout_pr",
description:
"Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
parameters: CheckoutPr,
execute: execute(ctx, async ({ pull_number }) => {
await checkoutPrBranch(ctx, pull_number);
execute: execute(async ({ pull_number }) => {
const result = await checkoutPrBranch({
octokit: ctx.octokit,
owner: ctx.owner,
name: ctx.name,
token: ctx.githubInstallationToken,
pullNumber: pull_number,
});
// set prNumber on toolState
ctx.toolState.prNumber = result.prNumber;
// fetch PR metadata to return result
const pr = await ctx.octokit.rest.pulls.get({
+11 -11
View File
@@ -2,7 +2,7 @@ import { Octokit } from "@octokit/rest";
import { type } from "arktype";
import type { Payload } 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 { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { getGitHubInstallationToken, parseRepoContext } from "../utils/github.ts";
@@ -66,13 +66,13 @@ export const Comment = type({
body: type.string.describe("the comment body content"),
});
export function CreateCommentTool(ctx: Context) {
export function CreateCommentTool(ctx: ToolContext) {
return tool({
name: "create_issue_comment",
description:
"Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.",
parameters: Comment,
execute: execute(ctx, async ({ issueNumber, body }) => {
execute: execute(async ({ issueNumber, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const result = await ctx.octokit.rest.issues.createComment({
@@ -97,12 +97,12 @@ export const EditComment = type({
body: type.string.describe("the new comment body content"),
});
export function EditCommentTool(ctx: Context) {
export function EditCommentTool(ctx: ToolContext) {
return tool({
name: "edit_issue_comment",
description: "Edit a GitHub issue comment by its ID",
parameters: EditComment,
execute: execute(ctx, async ({ commentId, body }) => {
execute: execute(async ({ commentId, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const result = await ctx.octokit.rest.issues.updateComment({
@@ -170,7 +170,7 @@ export const ReportProgress = type({
* Returns result data if successful, undefined if comment cannot be created.
*/
export async function reportProgress(
ctx: Context,
ctx: ToolContext,
{ body }: { body: string }
): Promise<
| {
@@ -231,13 +231,13 @@ export async function reportProgress(
};
}
export function ReportProgressTool(ctx: Context) {
export function ReportProgressTool(ctx: ToolContext) {
return tool({
name: "report_progress",
description:
"Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
parameters: ReportProgress,
execute: execute(ctx, async ({ body }) => {
execute: execute(async ({ body }) => {
const result = await reportProgress(ctx, { body });
if (!result) {
@@ -269,7 +269,7 @@ export function wasProgressCommentUpdated(): boolean {
* Delete the progress comment if it exists.
* 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();
if (!existingCommentId) {
return false;
@@ -380,13 +380,13 @@ export const ReplyToReviewComment = type({
),
});
export function ReplyToReviewCommentTool(ctx: Context) {
export function ReplyToReviewCommentTool(ctx: ToolContext) {
return tool({
name: "reply_to_review_comment",
description:
"Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
parameters: ReplyToReviewComment,
execute: execute(ctx, async ({ pull_number, comment_id, body }) => {
execute: execute(async ({ pull_number, comment_id, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
+3 -3
View File
@@ -1,17 +1,17 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
export const DebugShellCommand = type({});
export function DebugShellCommandTool(_ctx: Context) {
export function DebugShellCommandTool(_ctx: ToolContext) {
return tool({
name: "debug_shell_command",
description:
"debug tool: runs 'git status' and returns the output. use this to test shell command execution in the MCP server.",
parameters: DebugShellCommand,
execute: execute(_ctx, async () => {
execute: execute(async () => {
const result = $("git", ["status"]);
return {
success: true,
+3 -3
View File
@@ -1,7 +1,7 @@
import { relative, resolve } from "node:path";
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import { $ } from "../utils/shell.ts";
import type { Context } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const ListFiles = type({
@@ -10,13 +10,13 @@ export const ListFiles = type({
.default("."),
});
export function ListFilesTool(_ctx: Context) {
export function ListFilesTool(_ctx: ToolContext) {
return tool({
name: "list_files",
description:
"List files in the repository, including both git-tracked and untracked files. Useful for discovering the file structure and locating files, including newly created files that haven't been committed yet.",
parameters: ListFiles,
execute: execute(_ctx, async ({ path }: { path?: string }) => {
execute: execute(async ({ path }: { path?: string }) => {
const pathStr = path ?? ".";
const cwd = process.cwd();
+7 -7
View File
@@ -1,11 +1,11 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { log } from "../utils/cli.ts";
import { containsSecrets } from "../utils/secrets.ts";
import { $ } from "../utils/shell.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 CreateBranch = type({
@@ -22,7 +22,7 @@ export function CreateBranchTool(ctx: Context) {
description:
"Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
parameters: CreateBranch,
execute: execute(ctx, async ({ branchName, baseBranch }) => {
execute: execute(async ({ branchName, baseBranch }) => {
// baseBranch should always be defined due to default, but TypeScript needs help
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({
name: "commit_files",
description:
"Stage and commit files with a commit message. If files array is empty, commits all staged changes. The commit will be attributed to the correct bot account.",
parameters: CommitFiles,
execute: execute(ctx, async ({ message, files }) => {
execute: execute(async ({ message, files }) => {
// validate commit message for secrets
if (containsSecrets(message)) {
throw new Error(
@@ -141,13 +141,13 @@ export const PushBranch = type({
force: type.boolean.describe("Force push (use with caution)").default(false),
});
export function PushBranchTool(_ctx: Context) {
export function PushBranchTool(_ctx: ToolContext) {
return tool({
name: "push_branch",
description:
"Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
parameters: PushBranch,
execute: execute(_ctx, async ({ branchName, force }) => {
execute: execute(async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
// check if branch has a configured pushRemote
+6 -4
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const Issue = type({
@@ -15,12 +15,12 @@ export const Issue = type({
.optional(),
});
export function IssueTool(ctx: Context) {
export function IssueTool(ctx: ToolContext) {
return tool({
name: "create_issue",
description: "Create a new GitHub issue",
parameters: Issue,
execute: execute(ctx, async ({ title, body, labels, assignees }) => {
execute: execute(async ({ title, body, labels, assignees }) => {
const result = await ctx.octokit.rest.issues.create({
owner: ctx.owner,
repo: ctx.name,
@@ -37,7 +37,9 @@ export function IssueTool(ctx: Context) {
url: result.data.html_url,
title: result.data.title,
state: result.data.state,
labels: result.data.labels?.map((label) => (typeof label === "string" ? label : label.name)),
labels: result.data.labels?.map((label) =>
typeof label === "string" ? label : label.name
),
assignees: result.data.assignees?.map((assignee) => assignee.login),
};
}),
+3 -3
View File
@@ -1,18 +1,18 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const GetIssueComments = type({
issue_number: type.number.describe("The issue number to get comments for"),
});
export function GetIssueCommentsTool(ctx: Context) {
export function GetIssueCommentsTool(ctx: ToolContext) {
return tool({
name: "get_issue_comments",
description:
"Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
parameters: GetIssueComments,
execute: execute(ctx, async ({ issue_number }) => {
execute: execute(async ({ issue_number }) => {
// set issue context
ctx.toolState.issueNumber = issue_number;
+3 -3
View File
@@ -1,18 +1,18 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const GetIssueEvents = type({
issue_number: type.number.describe("The issue number to get events for"),
});
export function GetIssueEventsTool(ctx: Context) {
export function GetIssueEventsTool(ctx: ToolContext) {
return tool({
name: "get_issue_events",
description:
"Get timeline events for a GitHub issue that aren't reflected in the current state. Returns cross-references to other issues/PRs and commit references. Note: current labels, assignees, state, and milestone are already available via get_issue.",
parameters: GetIssueEvents,
execute: execute(ctx, async ({ issue_number }) => {
execute: execute(async ({ issue_number }) => {
// set issue context
ctx.toolState.issueNumber = issue_number;
+3 -3
View File
@@ -1,17 +1,17 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const IssueInfo = type({
issue_number: type.number.describe("The issue number to fetch"),
});
export function IssueInfoTool(ctx: Context) {
export function IssueInfoTool(ctx: ToolContext) {
return tool({
name: "get_issue",
description: "Retrieve GitHub issue information by issue number",
parameters: IssueInfo,
execute: execute(ctx, async ({ issue_number }) => {
execute: execute(async ({ issue_number }) => {
const issue = await ctx.octokit.rest.issues.get({
owner: ctx.owner,
repo: ctx.name,
+3 -3
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
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"),
});
export function AddLabelsTool(ctx: Context) {
export function AddLabelsTool(ctx: ToolContext) {
return tool({
name: "add_labels",
description:
"Add labels to a GitHub issue or pull request. Only use labels that already exist in the repository.",
parameters: AddLabelsParams,
execute: execute(ctx, async ({ issue_number, labels }) => {
execute: execute(async ({ issue_number, labels }) => {
const result = await ctx.octokit.rest.issues.addLabels({
owner: ctx.owner,
repo: ctx.name,
+4 -4
View File
@@ -1,6 +1,6 @@
import { type } from "arktype";
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 { log } from "../utils/cli.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')"),
});
function buildPrBodyWithFooter(ctx: Context, body: string): string {
function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
const agentName = ctx.payload.agent;
const agentInfo = agentName ? agentsManifest[agentName] : null;
@@ -29,12 +29,12 @@ function buildPrBodyWithFooter(ctx: Context, body: string): string {
return `${bodyWithoutFooter}${footer}`;
}
export function PullRequestTool(ctx: Context) {
export function PullRequestTool(ctx: ToolContext) {
return tool({
name: "create_pull_request",
description: "Create a pull request from the current branch",
parameters: PullRequest,
execute: execute(ctx, async ({ title, body, base }) => {
execute: execute(async ({ title, body, base }) => {
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
log.debug(`Current branch: ${currentBranch}`);
+3 -3
View File
@@ -1,18 +1,18 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const PullRequestInfo = type({
pull_number: type.number.describe("The pull request number to fetch"),
});
export function PullRequestInfoTool(ctx: Context) {
export function PullRequestInfoTool(ctx: ToolContext) {
return tool({
name: "get_pull_request",
description:
"Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
parameters: PullRequestInfo,
execute: execute(ctx, async ({ pull_number }) => {
execute: execute(async ({ pull_number }) => {
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
+10 -10
View File
@@ -3,7 +3,7 @@ import { writeFileSync } from "node:fs";
import { join } from "node:path";
import type { RestEndpointMethodTypes } from "@octokit/rest";
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts";
import { log } from "../utils/cli.ts";
import { deleteProgressComment } from "./comment.ts";
@@ -37,7 +37,7 @@ type AddPullRequestReviewThreadResponse = {
// helper to find existing pending review for the authenticated user
async function findPendingReview(
ctx: Context,
ctx: ToolContext,
pull_number: number
): Promise<{ id: number; node_id: string } | null> {
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"),
});
export function StartReviewTool(ctx: Context) {
export function StartReviewTool(ctx: ToolContext) {
return tool({
name: "start_review",
description:
"Start a new review session for a pull request. Creates a scratchpad file for gathering thoughts and a pending review on GitHub. Must be called before add_review_comment.",
parameters: StartReview,
execute: execute(ctx, async ({ pull_number }) => {
execute: execute(async ({ pull_number }) => {
// check if review already started in this session
if (ctx.toolState.review) {
throw new Error(
@@ -154,13 +154,13 @@ export const AddReviewComment = type({
.optional(),
});
export function AddReviewCommentTool(ctx: Context) {
export function AddReviewCommentTool(ctx: ToolContext) {
return tool({
name: "add_review_comment",
description:
"Add a comment to the current review session. Must call start_review first. Comments are stored in draft state until submit_review is called.",
parameters: AddReviewComment,
execute: execute(ctx, async ({ path, line, body, side }) => {
execute: execute(async ({ path, line, body, side }) => {
// check if review started
if (!ctx.toolState.review) {
throw new Error("No review session started. Call start_review first.");
@@ -195,13 +195,13 @@ export const SubmitReview = type({
.optional(),
});
export function SubmitReviewTool(ctx: Context) {
export function SubmitReviewTool(ctx: ToolContext) {
return tool({
name: "submit_review",
description:
"Submit the current review session. All comments added via add_review_comment will be published. Must call start_review first.",
parameters: SubmitReview,
execute: execute(ctx, async ({ body }) => {
execute: execute(async ({ body }) => {
// check if review started
if (!ctx.toolState.review) {
throw new Error("No review session started. Call start_review first.");
@@ -288,7 +288,7 @@ export const Review = type({
.optional(),
});
export function ReviewTool(ctx: Context) {
export function ReviewTool(ctx: ToolContext) {
return tool({
name: "submit_pull_request_review",
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. " +
"Only use 'body' for a 1-2 sentence summary with urgency and critical callouts.",
parameters: Review,
execute: execute(ctx, async ({ pull_number, body, commit_id, comments = [] }) => {
execute: execute(async ({ pull_number, body, commit_id, comments = [] }) => {
// set PR context
ctx.toolState.prNumber = pull_number;
+5 -5
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
// 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"),
});
export function GetReviewCommentsTool(ctx: Context) {
export function GetReviewCommentsTool(ctx: ToolContext) {
return tool({
name: "get_review_comments",
description:
"Get all review comments and their replies for a specific pull request review. Returns line-by-line comments that were left on specific code locations, including any threaded replies.",
parameters: GetReviewComments,
execute: execute(ctx, async ({ pull_number, review_id }) => {
execute: execute(async ({ pull_number, review_id }) => {
// fetch all review threads using graphql
const response = await ctx.octokit.graphql<GraphQLResponse>(REVIEW_THREADS_QUERY, {
owner: ctx.owner,
@@ -189,13 +189,13 @@ export const ListPullRequestReviews = type({
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({
name: "list_pull_request_reviews",
description:
"List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
parameters: ListPullRequestReviews,
execute: execute(ctx, async ({ pull_number }) => {
execute: execute(async ({ pull_number }) => {
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
owner: ctx.owner,
repo: ctx.name,
+3 -3
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const SelectMode = type({
@@ -8,13 +8,13 @@ export const SelectMode = type({
),
});
export function SelectModeTool(ctx: Context) {
export function SelectModeTool(ctx: ToolContext) {
return tool({
name: "select_mode",
description:
"Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
parameters: SelectMode,
execute: execute(ctx, async ({ modeName }) => {
execute: execute(async ({ modeName }) => {
const selectedMode = ctx.modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
if (!selectedMode) {
+5 -6
View File
@@ -1,9 +1,9 @@
import "./arkConfig.ts";
// this must be imported first
import { createServer } from "node:net";
// this must be imported first
import { FastMCP, type Tool } from "fastmcp";
import { ghPullfrogMcpName } from "../external.ts";
import type { Context } from "../main.ts";
import type { ToolContext } from "../main.ts";
import { CheckoutPrTool } from "./checkout.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
import {
@@ -25,7 +25,7 @@ import { PullRequestInfoTool } from "./prInfo.ts";
import { AddReviewCommentTool, StartReviewTool, SubmitReviewTool } from "./review.ts";
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.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
@@ -60,7 +60,7 @@ async function findAvailablePort(startPort: number): Promise<number> {
* Start the MCP HTTP server and return the URL and close function
*/
export async function startMcpHttpServer(
ctx: Context
ctx: ToolContext
): Promise<{ url: string; close: () => Promise<void> }> {
const server = new FastMCP({
name: ghPullfrogMcpName,
@@ -95,8 +95,7 @@ export async function startMcpHttpServer(
ListFilesTool(ctx),
];
// only include ReportProgressTool if progress comment is not disabled
if (!isProgressCommentDisabled(ctx)) {
if (!ctx.payload.disableProgressComment) {
tools.push(ReportProgressTool(ctx));
}
+4 -8
View File
@@ -1,6 +1,6 @@
import type { StandardSchemaV1 } from "@standard-schema/spec";
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;
@@ -40,7 +40,7 @@ export const handleToolError = (error: unknown): ToolResult => {
* Helper to wrap a tool execute function with error handling.
* 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> => {
try {
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
* - 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;
}
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)
// 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) {
const processedTool = shouldSanitize ? sanitizeTool(tool) : tool;