Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a7a38a9a5 | |||
| 72a040aafa | |||
| cc59a16472 | |||
| 8db0c40487 | |||
| 7fb788a883 | |||
| 0cf88e1752 | |||
| dcb672b5be | |||
| 7103f5f991 | |||
| c518e8b6fd | |||
| 615a3bc8e1 |
+17
-5
@@ -19,7 +19,7 @@ export const claude = agent({
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
const prompt = addInstructions({ payload, repo });
|
||||
log.group("» Full prompt", () => log.info(prompt));
|
||||
log.group("Full prompt", () => log.info(prompt));
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
const sandboxOptions: Options = payload.sandbox
|
||||
@@ -56,6 +56,7 @@ export const claude = agent({
|
||||
options: {
|
||||
...sandboxOptions,
|
||||
mcpServers,
|
||||
model: "claude-opus-4-5",
|
||||
pathToClaudeCodeExecutable: cliPath,
|
||||
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }),
|
||||
},
|
||||
@@ -146,16 +147,27 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
},
|
||||
result: async (data) => {
|
||||
if (data.subtype === "success") {
|
||||
const usage = data.usage;
|
||||
const inputTokens = usage?.input_tokens || 0;
|
||||
const cacheRead = usage?.cache_read_input_tokens || 0;
|
||||
const cacheWrite = usage?.cache_creation_input_tokens || 0;
|
||||
const outputTokens = usage?.output_tokens || 0;
|
||||
const totalInput = inputTokens + cacheRead + cacheWrite;
|
||||
|
||||
await log.summaryTable([
|
||||
[
|
||||
{ data: "Cost", header: true },
|
||||
{ data: "Input Tokens", header: true },
|
||||
{ data: "Output Tokens", header: true },
|
||||
{ data: "Input", header: true },
|
||||
{ data: "Cache Read", header: true },
|
||||
{ data: "Cache Write", header: true },
|
||||
{ data: "Output", header: true },
|
||||
],
|
||||
[
|
||||
`$${data.total_cost_usd?.toFixed(4) || "0.0000"}`,
|
||||
String(data.usage?.input_tokens || 0),
|
||||
String(data.usage?.output_tokens || 0),
|
||||
String(totalInput),
|
||||
String(cacheRead),
|
||||
String(cacheWrite),
|
||||
String(outputTokens),
|
||||
],
|
||||
]);
|
||||
} else if (data.subtype === "error_max_turns") {
|
||||
|
||||
+1
-1
@@ -167,7 +167,7 @@ export const cursor = agent({
|
||||
|
||||
try {
|
||||
const fullPrompt = addInstructions({ payload, repo });
|
||||
log.group("» Full prompt", () => log.info(fullPrompt));
|
||||
log.group("Full prompt", () => log.info(fullPrompt));
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
// in sandbox mode: remove --force flag and rely on cli-config.json sandbox settings
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ export const gemini = agent({
|
||||
}
|
||||
|
||||
const sessionPrompt = addInstructions({ payload, repo });
|
||||
log.group("» Full prompt", () => log.info(sessionPrompt));
|
||||
log.group("Full prompt", () => log.info(sessionPrompt));
|
||||
|
||||
// configure sandbox mode if enabled
|
||||
// --allowed-tools restricts which tools are available (removes others from registry entirely)
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export const opencode = agent({
|
||||
configureOpenCode({ mcpServers, sandbox: payload.sandbox ?? false });
|
||||
|
||||
const prompt = addInstructions({ payload, repo });
|
||||
log.group("» Full prompt", () => log.info(prompt));
|
||||
log.group("Full prompt", () => log.info(prompt));
|
||||
|
||||
// message positional must come right after "run", before flags
|
||||
const args = ["run", prompt, "--format", "json"];
|
||||
|
||||
@@ -83328,7 +83328,7 @@ function query({
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/action",
|
||||
version: "0.0.153",
|
||||
version: "0.0.154",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
@@ -92563,7 +92563,7 @@ function getModes({ disableProgressComment }) {
|
||||
{
|
||||
name: "Build",
|
||||
description: "Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. If this is a PR event, the PR branch is already checked out - skip branch creation. Otherwise, create a branch using ${ghPullfrogMcpName}/create_branch. The branch name should be prefixed with "pullfrog/". The rest of the name should reflect the exact changes you are making. It should be specific to avoid collisions with other branches. Never commit directly to main, master, or production. Do NOT use git commands directly (including \`git branch\`, \`git status\`, \`git log\`) - always use ${ghPullfrogMcpName} MCP tools for git operations.
|
||||
|
||||
${dependencyInstallationGuidance}
|
||||
@@ -92601,7 +92601,7 @@ ${dependencyInstallationGuidance}
|
||||
{
|
||||
name: "Address Reviews",
|
||||
description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and configures push settings (including for fork PRs).
|
||||
|
||||
${dependencyInstallationGuidance}
|
||||
@@ -92627,44 +92627,34 @@ ${disableProgressComment ? "" : `
|
||||
{
|
||||
name: "Review",
|
||||
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps to review the PR. Think hard. Do not nitpick.
|
||||
|
||||
1. **CHECKOUT** - Use ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and returns the diff. Use this diff for your review - it shows exactly what's in the PR.
|
||||
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This should give you all PR metadata you need, including a \`diffPath\`: a path to a temp file containing the PR diff.
|
||||
|
||||
2. **UNDERSTAND CONTEXT** - Read the modified files to understand the changes in context. Don't just look at the diff - understand how the changes affect the overall codebase.
|
||||
|
||||
3. **ANALYZE** - Think through:
|
||||
- What does this PR change? Summarize in 1-2 sentences.
|
||||
2. **ANALYZE**
|
||||
- Read the modified files to understand the changes in context. Make sure you understand what's being changed.
|
||||
- Is it a good idea? Think about the tradeoffs.
|
||||
- Is the approach sound? If not, focus on the approach first. Don't waste time on implementation details if the approach is wrong.
|
||||
- What bugs, edge cases, or security issues exist?
|
||||
- Can you imagine a better approach? If so, explain. Make sure it's strictly better, not just different.
|
||||
- Are there bugs, edge cases, security issues, or usability issues? Use your imagination.
|
||||
|
||||
4. **DRAFT** - Mentally list all comments you would make. For each:
|
||||
- Note the file path (relative to repo root, e.g., \`packages/core/src/utils.ts\`)
|
||||
- Note the line number from the diff (use NEW file line number - shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12)
|
||||
- Draft the comment text
|
||||
3. **DRAFT** - For each inline comment, find the line in the diff. Each code line shows: \`| OLD | NEW | TYPE | CODE\`. Use the NEW line number (second column).
|
||||
|
||||
5. **SELF-CRITIQUE** - Before submitting, review your draft:
|
||||
- Remove nitpicks unless the user explicitly told you to be nitpicky. Nitpicks may include: requesting documentation/docstrings/JSDoc, commenting on minor code/whitespace formatting, commenting on small changes unrelated to the main changes.
|
||||
- Ensure each comment is actionable - would the author know exactly what to do?
|
||||
- Would the codebase maintainer care about this feedback?
|
||||
- If you have approach-level concerns, consider whether implementation-level comments are worth including
|
||||
- For issues appearing in multiple places, keep only the FIRST occurrence and reference others (e.g., "also at lines X, Y")
|
||||
4. **FILTER COMMENTS** - Do not nitpick! Do not leave compliments that are not actionable. Do not critique the code hygiene or anything stylistic.
|
||||
|
||||
6. **SUBMIT** - Use ${ghPullfrogMcpName}/create_pull_request_review with:
|
||||
- \`comments\`: Array of all inline comments with file paths and line numbers
|
||||
- \`body\`: 1-3 sentence summary with urgency level (e.g., "minor suggestions" vs "blocking issues") and any critical callouts (e.g., API key exposure)
|
||||
5. **SUBMIT** \u2014 Use ${ghPullfrogMcpName}/create_pull_request_review with:
|
||||
- \`comments\`: Array of all inline comments with file paths and line numbers
|
||||
- \`body\`: Everything else. Aim for a 1-3 sentence summary of the urgency level (e.g., "minor suggestions" vs "blocking issues") and any critical callouts (e.g., API key exposure). It can be longer if there are concerns that do not lend themselves to inline comments.
|
||||
- If you have no substantive feedback, submit an empty comments array with a brief approving body.
|
||||
- Again, do not nitpick.
|
||||
|
||||
**CRITICAL RULES**
|
||||
- 95%+ of review content should be in inline \`comments\` array, not the \`body\`
|
||||
- Only comment on lines that appear in the diff - GitHub will reject comments on unchanged lines
|
||||
- Do not leave complimentary comments just to be nice
|
||||
- Do not leave comments that are not actionable
|
||||
`
|
||||
},
|
||||
{
|
||||
name: "Plan",
|
||||
description: "Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. If the request requires understanding the codebase structure or conventions, gather relevant context (read AGENTS.md if it exists). Skip this step if the prompt is trivial and self-contained.
|
||||
|
||||
2. Analyze the request and break it down into clear, actionable tasks
|
||||
@@ -92678,7 +92668,7 @@ ${disableProgressComment ? "" : `
|
||||
{
|
||||
name: "Prompt",
|
||||
description: "Fallback for tasks that don't fit other workflows, e.g. direct prompts via comments, or requests requiring general assistance",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. Perform the requested task. Only take action if you have high confidence that you understand what is being asked. If you are not sure, ask for clarification. Take stock of the tools at your disposal.${disableProgressComment ? "" : "\n\n2. When creating comments, always use report_progress. Do not use create_issue_comment."}
|
||||
|
||||
2. If the task involves making code changes:
|
||||
@@ -93312,7 +93302,7 @@ var claude = agent({
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, repo }) => {
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
const prompt = addInstructions({ payload, repo });
|
||||
log.group("\xBB Full prompt", () => log.info(prompt));
|
||||
log.group("Full prompt", () => log.info(prompt));
|
||||
const sandboxOptions = payload.sandbox ? {
|
||||
permissionMode: "default",
|
||||
disallowedTools: ["Bash", "WebSearch", "WebFetch", "Write"],
|
||||
@@ -93340,6 +93330,7 @@ var claude = agent({
|
||||
options: {
|
||||
...sandboxOptions,
|
||||
mcpServers,
|
||||
model: "claude-opus-4-5",
|
||||
pathToClaudeCodeExecutable: cliPath,
|
||||
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey })
|
||||
}
|
||||
@@ -93400,16 +93391,26 @@ var messageHandlers = {
|
||||
},
|
||||
result: async (data) => {
|
||||
if (data.subtype === "success") {
|
||||
const usage = data.usage;
|
||||
const inputTokens = usage?.input_tokens || 0;
|
||||
const cacheRead = usage?.cache_read_input_tokens || 0;
|
||||
const cacheWrite = usage?.cache_creation_input_tokens || 0;
|
||||
const outputTokens = usage?.output_tokens || 0;
|
||||
const totalInput = inputTokens + cacheRead + cacheWrite;
|
||||
await log.summaryTable([
|
||||
[
|
||||
{ data: "Cost", header: true },
|
||||
{ data: "Input Tokens", header: true },
|
||||
{ data: "Output Tokens", header: true }
|
||||
{ data: "Input", header: true },
|
||||
{ data: "Cache Read", header: true },
|
||||
{ data: "Cache Write", header: true },
|
||||
{ data: "Output", header: true }
|
||||
],
|
||||
[
|
||||
`$${data.total_cost_usd?.toFixed(4) || "0.0000"}`,
|
||||
String(data.usage?.input_tokens || 0),
|
||||
String(data.usage?.output_tokens || 0)
|
||||
String(totalInput),
|
||||
String(cacheRead),
|
||||
String(cacheWrite),
|
||||
String(outputTokens)
|
||||
]
|
||||
]);
|
||||
} else if (data.subtype === "error_max_turns") {
|
||||
@@ -93994,7 +93995,7 @@ var cursor = agent({
|
||||
};
|
||||
try {
|
||||
const fullPrompt = addInstructions({ payload, repo });
|
||||
log.group("\xBB Full prompt", () => log.info(fullPrompt));
|
||||
log.group("Full prompt", () => log.info(fullPrompt));
|
||||
const cursorArgs = payload.sandbox ? [
|
||||
"--print",
|
||||
fullPrompt,
|
||||
@@ -94305,7 +94306,7 @@ var gemini = agent({
|
||||
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
|
||||
}
|
||||
const sessionPrompt = addInstructions({ payload, repo });
|
||||
log.group("\xBB Full prompt", () => log.info(sessionPrompt));
|
||||
log.group("Full prompt", () => log.info(sessionPrompt));
|
||||
const args3 = payload.sandbox ? [
|
||||
"--allowed-tools",
|
||||
"read_file,list_directory,search_file_content,glob,save_memory,write_todos",
|
||||
@@ -94428,7 +94429,7 @@ var opencode = agent({
|
||||
mkdirSync4(configDir, { recursive: true });
|
||||
configureOpenCode({ mcpServers, sandbox: payload.sandbox ?? false });
|
||||
const prompt = addInstructions({ payload, repo });
|
||||
log.group("\xBB Full prompt", () => log.info(prompt));
|
||||
log.group("Full prompt", () => log.info(prompt));
|
||||
const args3 = ["run", prompt, "--format", "json"];
|
||||
if (payload.sandbox) {
|
||||
log.info("\u{1F512} sandbox mode enabled: restricting to read-only operations");
|
||||
@@ -98287,13 +98288,9 @@ function stripExistingFooter(body) {
|
||||
// mcp/shared.ts
|
||||
var tool = (toolDef) => toolDef;
|
||||
var handleToolSuccess = (data) => {
|
||||
const text = typeof data === "string" ? data : encode(data);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2)
|
||||
}
|
||||
]
|
||||
content: [{ type: "text", text }]
|
||||
};
|
||||
};
|
||||
var handleToolError = (error41) => {
|
||||
@@ -123018,6 +123015,55 @@ function $(cmd, args3, options) {
|
||||
}
|
||||
|
||||
// mcp/checkout.ts
|
||||
function formatFilesWithLineNumbers(files) {
|
||||
const output = [];
|
||||
for (const file of files) {
|
||||
output.push(`diff --git a/${file.filename} b/${file.filename}`);
|
||||
output.push(`--- a/${file.filename}`);
|
||||
output.push(`+++ b/${file.filename}`);
|
||||
if (!file.patch) {
|
||||
output.push("(binary file or no changes)");
|
||||
output.push("");
|
||||
continue;
|
||||
}
|
||||
const lines = file.patch.split("\n");
|
||||
let oldLine = 0;
|
||||
let newLine = 0;
|
||||
for (const line of lines) {
|
||||
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
||||
if (hunkMatch) {
|
||||
oldLine = parseInt(hunkMatch[1], 10);
|
||||
newLine = parseInt(hunkMatch[2], 10);
|
||||
output.push(line);
|
||||
continue;
|
||||
}
|
||||
const changeType = line[0] || " ";
|
||||
const code = line.slice(1);
|
||||
if (changeType === "-") {
|
||||
output.push(`| ${padNum(oldLine)} | | - | ${code}`);
|
||||
oldLine++;
|
||||
} else if (changeType === "+") {
|
||||
output.push(`| | ${padNum(newLine)} | + | ${code}`);
|
||||
newLine++;
|
||||
} else if (changeType === " " || changeType === "\\") {
|
||||
if (changeType === "\\") {
|
||||
output.push(line);
|
||||
} else {
|
||||
output.push(`| ${padNum(oldLine)} | ${padNum(newLine)} | | ${code}`);
|
||||
oldLine++;
|
||||
newLine++;
|
||||
}
|
||||
} else {
|
||||
output.push(line);
|
||||
}
|
||||
}
|
||||
output.push("");
|
||||
}
|
||||
return output.join("\n");
|
||||
}
|
||||
function padNum(n) {
|
||||
return n.toString().padStart(4, " ");
|
||||
}
|
||||
var CheckoutPr = type({
|
||||
pull_number: type.number.describe("the pull request number to checkout")
|
||||
});
|
||||
@@ -123081,7 +123127,7 @@ async function checkoutPrBranch(params) {
|
||||
function CheckoutPrTool(ctx) {
|
||||
return tool({
|
||||
name: "checkout_pr",
|
||||
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. The PR diff is written to a file (diffPath) for grep access. For small diffs, it's also returned inline.",
|
||||
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file.",
|
||||
parameters: CheckoutPr,
|
||||
execute: execute(async ({ pull_number }) => {
|
||||
const result = await checkoutPrBranch({
|
||||
@@ -123101,13 +123147,16 @@ function CheckoutPrTool(ctx) {
|
||||
if (!headRepo) {
|
||||
throw new Error(`PR #${pull_number} source repository was deleted`);
|
||||
}
|
||||
const diffResponse = await ctx.octokit.rest.pulls.get({
|
||||
const filesResponse = await ctx.octokit.rest.pulls.listFiles({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
pull_number,
|
||||
mediaType: { format: "diff" }
|
||||
per_page: 100
|
||||
});
|
||||
const diffContent = diffResponse.data;
|
||||
const diffContent = formatFilesWithLineNumbers(filesResponse.data);
|
||||
const diffPreview = diffContent.split("\n").slice(0, 100).join("\n");
|
||||
log.debug(`formatted diff preview (first 100 lines):
|
||||
${diffPreview}`);
|
||||
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||
if (!tempDir) {
|
||||
throw new Error(
|
||||
@@ -123117,8 +123166,6 @@ function CheckoutPrTool(ctx) {
|
||||
const diffPath = join8(tempDir, `pr-${pull_number}.diff`);
|
||||
writeFileSync4(diffPath, diffContent);
|
||||
log.debug(`wrote diff to ${diffPath} (${diffContent.length} bytes)`);
|
||||
const MAX_INLINE_DIFF_SIZE = 50 * 1024;
|
||||
const diff = diffContent.length <= MAX_INLINE_DIFF_SIZE ? diffContent : `Diff is ${(diffContent.length / 1024).toFixed(0)}KB - use grep or read from ${diffPath}`;
|
||||
return {
|
||||
success: true,
|
||||
number: pr.data.number,
|
||||
@@ -123129,7 +123176,6 @@ function CheckoutPrTool(ctx) {
|
||||
maintainerCanModify: pr.data.maintainer_can_modify,
|
||||
url: pr.data.html_url,
|
||||
headRepo: headRepo.full_name,
|
||||
diff,
|
||||
diffPath
|
||||
};
|
||||
})
|
||||
@@ -124223,11 +124269,7 @@ function GetIssueCommentsTool(ctx) {
|
||||
id: comment.id,
|
||||
body: comment.body,
|
||||
user: comment.user?.login,
|
||||
created_at: comment.created_at,
|
||||
updated_at: comment.updated_at,
|
||||
html_url: comment.html_url,
|
||||
author_association: comment.author_association,
|
||||
reactions: comment.reactions
|
||||
author_association: comment.author_association
|
||||
})),
|
||||
count: comments.length
|
||||
};
|
||||
@@ -124400,7 +124442,7 @@ function buildPrBodyWithFooter(ctx, body) {
|
||||
const bodyWithoutFooter = stripExistingFooter(body);
|
||||
return `${bodyWithoutFooter}${footer}`;
|
||||
}
|
||||
function PullRequestTool(ctx) {
|
||||
function CreatePullRequestTool(ctx) {
|
||||
return tool({
|
||||
name: "create_pull_request",
|
||||
description: "Create a pull request from the current branch",
|
||||
@@ -124477,24 +124519,23 @@ function PullRequestInfoTool(ctx) {
|
||||
var CreatePullRequestReview = type({
|
||||
pull_number: type.number.describe("The pull request number to review"),
|
||||
body: type.string.describe(
|
||||
"1-2 sentence high-level summary ONLY. Include urgency level and critical callouts (e.g., API key leak). ALL specific feedback MUST go in 'comments' array instead."
|
||||
"1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
|
||||
).optional(),
|
||||
commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(),
|
||||
comments: type({
|
||||
path: type.string.describe("The file path to comment on (relative to repo root)"),
|
||||
line: type.number.describe(
|
||||
"The line number in the file (use line numbers from the diff - usually the RIGHT side/new code)"
|
||||
"Line number from the diff. Each code line shows 'OLD | NEW | TYPE | CODE'. Use the NEW column (second column)."
|
||||
),
|
||||
side: type.enumerated("LEFT", "RIGHT").describe(
|
||||
"Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided."
|
||||
"Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
|
||||
).optional(),
|
||||
body: type.string.describe(
|
||||
"The comment text for this specific line. For issues appearing multiple times, comment on the first occurrence and reference others."
|
||||
),
|
||||
start_line: type.number.describe("Start line for multi-line comments (optional, for commenting on ranges)").optional()
|
||||
}).array().describe(
|
||||
// FORK PR NOTE: checkout_pr returns the diff via GitHub API - use that for line numbers
|
||||
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use the diff returned from checkout_pr to find correct line numbers (RIGHT side for new code, LEFT for old)."
|
||||
"Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
|
||||
).optional()
|
||||
});
|
||||
function CreatePullRequestReviewTool(ctx) {
|
||||
@@ -124777,7 +124818,7 @@ async function startMcpHttpServer(ctx) {
|
||||
IssueInfoTool(ctx),
|
||||
GetIssueCommentsTool(ctx),
|
||||
GetIssueEventsTool(ctx),
|
||||
PullRequestTool(ctx),
|
||||
CreatePullRequestTool(ctx),
|
||||
CreatePullRequestReviewTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CheckoutPrTool(ctx),
|
||||
|
||||
+81
-17
@@ -1,12 +1,85 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import type { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
|
||||
import { type } from "arktype";
|
||||
import type { ToolContext } from "../main.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { $ } from "../utils/shell.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
|
||||
type PullFile = RestEndpointMethodTypes["pulls"]["listFiles"]["response"]["data"][number];
|
||||
|
||||
/**
|
||||
* formats PR files with explicit line numbers for each code line.
|
||||
* preserves all original diff info (file headers, hunk headers) and adds:
|
||||
* | OLD | NEW | TYPE | code
|
||||
*/
|
||||
export function formatFilesWithLineNumbers(files: PullFile[]): string {
|
||||
const output: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
// file header
|
||||
output.push(`diff --git a/${file.filename} b/${file.filename}`);
|
||||
output.push(`--- a/${file.filename}`);
|
||||
output.push(`+++ b/${file.filename}`);
|
||||
|
||||
if (!file.patch) {
|
||||
output.push("(binary file or no changes)");
|
||||
output.push("");
|
||||
continue;
|
||||
}
|
||||
|
||||
// parse and format the patch with line numbers
|
||||
const lines = file.patch.split("\n");
|
||||
let oldLine = 0;
|
||||
let newLine = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// hunk header: @@ -OLD,COUNT +NEW,COUNT @@ optional context
|
||||
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
||||
if (hunkMatch) {
|
||||
oldLine = parseInt(hunkMatch[1], 10);
|
||||
newLine = parseInt(hunkMatch[2], 10);
|
||||
output.push(line); // pass through unchanged
|
||||
continue;
|
||||
}
|
||||
|
||||
// code lines within hunks
|
||||
const changeType = line[0] || " ";
|
||||
const code = line.slice(1);
|
||||
|
||||
if (changeType === "-") {
|
||||
// removed line: show old line number, no new line number
|
||||
output.push(`| ${padNum(oldLine)} | | - | ${code}`);
|
||||
oldLine++;
|
||||
} else if (changeType === "+") {
|
||||
// added line: no old line number, show new line number
|
||||
output.push(`| | ${padNum(newLine)} | + | ${code}`);
|
||||
newLine++;
|
||||
} else if (changeType === " " || changeType === "\\") {
|
||||
// context line or "\ No newline at end of file"
|
||||
if (changeType === "\\") {
|
||||
output.push(line); // pass through as-is
|
||||
} else {
|
||||
output.push(`| ${padNum(oldLine)} | ${padNum(newLine)} | | ${code}`);
|
||||
oldLine++;
|
||||
newLine++;
|
||||
}
|
||||
} else {
|
||||
// unknown line type, pass through
|
||||
output.push(line);
|
||||
}
|
||||
}
|
||||
output.push(""); // blank line between files
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
function padNum(n: number): string {
|
||||
return n.toString().padStart(4, " ");
|
||||
}
|
||||
|
||||
export const CheckoutPr = type({
|
||||
pull_number: type.number.describe("the pull request number to checkout"),
|
||||
});
|
||||
@@ -21,7 +94,6 @@ export type CheckoutPrResult = {
|
||||
maintainerCanModify: boolean;
|
||||
url: string;
|
||||
headRepo: string;
|
||||
diff: string;
|
||||
diffPath: string;
|
||||
};
|
||||
|
||||
@@ -144,7 +216,7 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
||||
name: "checkout_pr",
|
||||
description:
|
||||
"Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. " +
|
||||
"The PR diff is written to a file (diffPath) for grep access. For small diffs, it's also returned inline.",
|
||||
"Returns diffPath pointing to the formatted diff file.",
|
||||
parameters: CheckoutPr,
|
||||
execute: execute(async ({ pull_number }) => {
|
||||
const result = await checkoutPrBranch({
|
||||
@@ -170,16 +242,16 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
||||
throw new Error(`PR #${pull_number} source repository was deleted`);
|
||||
}
|
||||
|
||||
// fetch PR diff via API (authoritative source - not affected by main advancing)
|
||||
const diffResponse = await ctx.octokit.rest.pulls.get({
|
||||
// fetch PR files and format with line numbers
|
||||
const filesResponse = await ctx.octokit.rest.pulls.listFiles({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
pull_number,
|
||||
mediaType: { format: "diff" },
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// write diff to file for grep access
|
||||
const diffContent = diffResponse.data as unknown as string;
|
||||
const diffContent = formatFilesWithLineNumbers(filesResponse.data);
|
||||
const diffPreview = diffContent.split("\n").slice(0, 100).join("\n");
|
||||
log.debug(`formatted diff preview (first 100 lines):\n${diffPreview}`);
|
||||
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||
if (!tempDir) {
|
||||
throw new Error(
|
||||
@@ -190,13 +262,6 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
||||
writeFileSync(diffPath, diffContent);
|
||||
log.debug(`wrote diff to ${diffPath} (${diffContent.length} bytes)`);
|
||||
|
||||
// return diff inline only if small enough
|
||||
const MAX_INLINE_DIFF_SIZE = 50 * 1024; // 50KB
|
||||
const diff =
|
||||
diffContent.length <= MAX_INLINE_DIFF_SIZE
|
||||
? diffContent
|
||||
: `Diff is ${(diffContent.length / 1024).toFixed(0)}KB - use grep or read from ${diffPath}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
number: pr.data.number,
|
||||
@@ -207,7 +272,6 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
||||
maintainerCanModify: pr.data.maintainer_can_modify,
|
||||
url: pr.data.html_url,
|
||||
headRepo: headRepo.full_name,
|
||||
diff,
|
||||
diffPath,
|
||||
} satisfies CheckoutPrResult;
|
||||
}),
|
||||
|
||||
@@ -28,11 +28,7 @@ export function GetIssueCommentsTool(ctx: ToolContext) {
|
||||
id: comment.id,
|
||||
body: comment.body,
|
||||
user: comment.user?.login,
|
||||
created_at: comment.created_at,
|
||||
updated_at: comment.updated_at,
|
||||
html_url: comment.html_url,
|
||||
author_association: comment.author_association,
|
||||
reactions: comment.reactions,
|
||||
})),
|
||||
count: comments.length,
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
|
||||
return `${bodyWithoutFooter}${footer}`;
|
||||
}
|
||||
|
||||
export function PullRequestTool(ctx: ToolContext) {
|
||||
export function CreatePullRequestTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "create_pull_request",
|
||||
description: "Create a pull request from the current branch",
|
||||
|
||||
+4
-5
@@ -11,7 +11,7 @@ export const CreatePullRequestReview = type({
|
||||
pull_number: type.number.describe("The pull request number to review"),
|
||||
body: type.string
|
||||
.describe(
|
||||
"1-2 sentence high-level summary ONLY. Include urgency level and critical callouts (e.g., API key leak). ALL specific feedback MUST go in 'comments' array instead."
|
||||
"1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
|
||||
)
|
||||
.optional(),
|
||||
commit_id: type.string
|
||||
@@ -20,12 +20,12 @@ export const CreatePullRequestReview = type({
|
||||
comments: type({
|
||||
path: type.string.describe("The file path to comment on (relative to repo root)"),
|
||||
line: type.number.describe(
|
||||
"The line number in the file (use line numbers from the diff - usually the RIGHT side/new code)"
|
||||
"Line number from the diff. Each code line shows 'OLD | NEW | TYPE | CODE'. Use the NEW column (second column)."
|
||||
),
|
||||
side: type
|
||||
.enumerated("LEFT", "RIGHT")
|
||||
.describe(
|
||||
"Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided."
|
||||
"Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
|
||||
)
|
||||
.optional(),
|
||||
body: type.string.describe(
|
||||
@@ -37,8 +37,7 @@ export const CreatePullRequestReview = type({
|
||||
})
|
||||
.array()
|
||||
.describe(
|
||||
// FORK PR NOTE: checkout_pr returns the diff via GitHub API - use that for line numbers
|
||||
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use the diff returned from checkout_pr to find correct line numbers (RIGHT side for new code, LEFT for old)."
|
||||
"Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ import { GetIssueCommentsTool } from "./issueComments.ts";
|
||||
import { GetIssueEventsTool } from "./issueEvents.ts";
|
||||
import { IssueInfoTool } from "./issueInfo.ts";
|
||||
import { AddLabelsTool } from "./labels.ts";
|
||||
import { PullRequestTool } from "./pr.ts";
|
||||
import { CreatePullRequestTool } from "./pr.ts";
|
||||
import { PullRequestInfoTool } from "./prInfo.ts";
|
||||
import { CreatePullRequestReviewTool } from "./review.ts";
|
||||
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
|
||||
@@ -83,7 +83,7 @@ export async function startMcpHttpServer(
|
||||
IssueInfoTool(ctx),
|
||||
GetIssueCommentsTool(ctx),
|
||||
GetIssueEventsTool(ctx),
|
||||
PullRequestTool(ctx),
|
||||
CreatePullRequestTool(ctx),
|
||||
CreatePullRequestReviewTool(ctx),
|
||||
PullRequestInfoTool(ctx),
|
||||
CheckoutPrTool(ctx),
|
||||
|
||||
+5
-8
@@ -1,4 +1,5 @@
|
||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||
import { encode as toonEncode } from "@toon-format/toon";
|
||||
import type { FastMCP, Tool } from "fastmcp";
|
||||
import type { ToolContext } from "../main.ts";
|
||||
|
||||
@@ -12,14 +13,10 @@ export interface ToolResult {
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export const handleToolSuccess = (data: Record<string, any>): ToolResult => {
|
||||
export const handleToolSuccess = (data: Record<string, any> | string): ToolResult => {
|
||||
const text = typeof data === "string" ? data : toonEncode(data);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2),
|
||||
},
|
||||
],
|
||||
content: [{ type: "text", text }],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,7 +37,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>(fn: (params: T) => Promise<Record<string, any>>) => {
|
||||
export const execute = <T>(fn: (params: T) => Promise<Record<string, any> | string>) => {
|
||||
return async (params: T): Promise<ToolResult> => {
|
||||
try {
|
||||
const result = await fn(params);
|
||||
|
||||
@@ -29,7 +29,7 @@ export function getModes({ disableProgressComment }: GetModesParams): Mode[] {
|
||||
name: "Build",
|
||||
description:
|
||||
"Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. If this is a PR event, the PR branch is already checked out - skip branch creation. Otherwise, create a branch using ${ghPullfrogMcpName}/create_branch. The branch name should be prefixed with "pullfrog/". The rest of the name should reflect the exact changes you are making. It should be specific to avoid collisions with other branches. Never commit directly to main, master, or production. Do NOT use git commands directly (including \`git branch\`, \`git status\`, \`git log\`) - always use ${ghPullfrogMcpName} MCP tools for git operations.
|
||||
|
||||
${dependencyInstallationGuidance}
|
||||
@@ -68,7 +68,7 @@ ${dependencyInstallationGuidance}
|
||||
name: "Address Reviews",
|
||||
description:
|
||||
"Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and configures push settings (including for fork PRs).
|
||||
|
||||
${dependencyInstallationGuidance}
|
||||
@@ -99,45 +99,35 @@ ${
|
||||
name: "Review",
|
||||
description:
|
||||
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps to review the PR. Think hard. Do not nitpick.
|
||||
|
||||
1. **CHECKOUT** - Use ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and returns the diff. Use this diff for your review - it shows exactly what's in the PR.
|
||||
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This should give you all PR metadata you need, including a \`diffPath\`: a path to a temp file containing the PR diff.
|
||||
|
||||
2. **UNDERSTAND CONTEXT** - Read the modified files to understand the changes in context. Don't just look at the diff - understand how the changes affect the overall codebase.
|
||||
|
||||
3. **ANALYZE** - Think through:
|
||||
- What does this PR change? Summarize in 1-2 sentences.
|
||||
2. **ANALYZE**
|
||||
- Read the modified files to understand the changes in context. Make sure you understand what's being changed.
|
||||
- Is it a good idea? Think about the tradeoffs.
|
||||
- Is the approach sound? If not, focus on the approach first. Don't waste time on implementation details if the approach is wrong.
|
||||
- What bugs, edge cases, or security issues exist?
|
||||
- Can you imagine a better approach? If so, explain. Make sure it's strictly better, not just different.
|
||||
- Are there bugs, edge cases, security issues, or usability issues? Use your imagination.
|
||||
|
||||
4. **DRAFT** - Mentally list all comments you would make. For each:
|
||||
- Note the file path (relative to repo root, e.g., \`packages/core/src/utils.ts\`)
|
||||
- Note the line number from the diff (use NEW file line number - shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12)
|
||||
- Draft the comment text
|
||||
3. **DRAFT** - For each inline comment, find the line in the diff. Each code line shows: \`| OLD | NEW | TYPE | CODE\`. Use the NEW line number (second column).
|
||||
|
||||
5. **SELF-CRITIQUE** - Before submitting, review your draft:
|
||||
- Remove nitpicks unless the user explicitly told you to be nitpicky. Nitpicks may include: requesting documentation/docstrings/JSDoc, commenting on minor code/whitespace formatting, commenting on small changes unrelated to the main changes.
|
||||
- Ensure each comment is actionable - would the author know exactly what to do?
|
||||
- Would the codebase maintainer care about this feedback?
|
||||
- If you have approach-level concerns, consider whether implementation-level comments are worth including
|
||||
- For issues appearing in multiple places, keep only the FIRST occurrence and reference others (e.g., "also at lines X, Y")
|
||||
4. **FILTER COMMENTS** - Do not nitpick! Do not leave compliments that are not actionable. Do not critique the code hygiene or anything stylistic.
|
||||
|
||||
6. **SUBMIT** - Use ${ghPullfrogMcpName}/create_pull_request_review with:
|
||||
- \`comments\`: Array of all inline comments with file paths and line numbers
|
||||
- \`body\`: 1-3 sentence summary with urgency level (e.g., "minor suggestions" vs "blocking issues") and any critical callouts (e.g., API key exposure)
|
||||
5. **SUBMIT** — Use ${ghPullfrogMcpName}/create_pull_request_review with:
|
||||
- \`comments\`: Array of all inline comments with file paths and line numbers
|
||||
- \`body\`: Everything else. Aim for a 1-3 sentence summary of the urgency level (e.g., "minor suggestions" vs "blocking issues") and any critical callouts (e.g., API key exposure). It can be longer if there are concerns that do not lend themselves to inline comments.
|
||||
- If you have no substantive feedback, submit an empty comments array with a brief approving body.
|
||||
- Again, do not nitpick.
|
||||
|
||||
**CRITICAL RULES**
|
||||
- 95%+ of review content should be in inline \`comments\` array, not the \`body\`
|
||||
- Only comment on lines that appear in the diff - GitHub will reject comments on unchanged lines
|
||||
- Do not leave complimentary comments just to be nice
|
||||
- Do not leave comments that are not actionable
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Plan",
|
||||
description:
|
||||
"Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. If the request requires understanding the codebase structure or conventions, gather relevant context (read AGENTS.md if it exists). Skip this step if the prompt is trivial and self-contained.
|
||||
|
||||
2. Analyze the request and break it down into clear, actionable tasks
|
||||
@@ -150,7 +140,7 @@ ${
|
||||
name: "Prompt",
|
||||
description:
|
||||
"Fallback for tasks that don't fit other workflows, e.g. direct prompts via comments, or requests requiring general assistance",
|
||||
prompt: `Follow these steps:
|
||||
prompt: `Follow these steps. THINK HARDER.
|
||||
1. Perform the requested task. Only take action if you have high confidence that you understand what is being asked. If you are not sure, ask for clarification. Take stock of the tools at your disposal.${disableProgressComment ? "" : "\n\n2. When creating comments, always use report_progress. Do not use create_issue_comment."}
|
||||
|
||||
2. If the task involves making code changes:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.154",
|
||||
"version": "0.0.155",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { log } from "./cli.ts";
|
||||
|
||||
export type RetryOptions = {
|
||||
maxAttempts?: number;
|
||||
delayMs?: number;
|
||||
shouldRetry?: (error: unknown) => boolean;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const defaultShouldRetry = (error: unknown): boolean => {
|
||||
if (!(error instanceof Error)) return false;
|
||||
// retry on transient network errors
|
||||
return (
|
||||
error.name === "AbortError" ||
|
||||
error.message.includes("fetch failed") ||
|
||||
error.message.includes("ECONNRESET") ||
|
||||
error.message.includes("ETIMEDOUT")
|
||||
);
|
||||
};
|
||||
|
||||
export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
||||
const maxAttempts = options.maxAttempts ?? 3;
|
||||
const delayMs = options.delayMs ?? 1000;
|
||||
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
||||
const label = options.label ?? "operation";
|
||||
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt === maxAttempts || !shouldRetry(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const delay = delayMs * attempt;
|
||||
log.warning(`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user