diff --git a/entry b/entry index 5168328..ff83e30 100755 --- a/entry +++ b/entry @@ -121083,6 +121083,55 @@ function CreatePullRequestTool(ctx) { }); } +// mcp/commitInfo.ts +import { writeFileSync as writeFileSync3 } from "node:fs"; +import { join as join5 } from "node:path"; +var CommitInfo = type({ + sha: type.string.describe("the commit SHA (full or abbreviated) to fetch") +}); +function CommitInfoTool(ctx) { + return tool({ + name: "get_commit_info", + description: "Retrieve commit metadata and diff via GitHub API. Use this instead of git show for reviewing commits - it works with shallow clones and shows the actual changes in the commit. Returns diffPath pointing to formatted diff file.", + parameters: CommitInfo, + execute: execute(async ({ sha }) => { + const response = await ctx.octokit.rest.repos.getCommit({ + owner: ctx.repo.owner, + repo: ctx.repo.name, + ref: sha + }); + const data = response.data; + const files = data.files ?? []; + const diffContent = formatFilesWithLineNumbers(files); + const tempDir = process.env.PULLFROG_TEMP_DIR; + if (!tempDir) { + throw new Error( + "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context" + ); + } + const diffFile = join5(tempDir, `commit-${sha.slice(0, 7)}.diff`); + writeFileSync3(diffFile, diffContent); + log.debug(`wrote commit diff to ${diffFile} (${diffContent.length} bytes)`); + return { + sha: data.sha, + message: data.commit.message, + author: data.author?.login ?? null, + committer: data.committer?.login ?? null, + date: data.commit.author?.date ?? data.commit.committer?.date ?? "", + url: data.html_url, + parents: data.parents.map((p) => p.sha), + stats: { + additions: data.stats?.additions ?? 0, + deletions: data.stats?.deletions ?? 0, + total: data.stats?.total ?? 0 + }, + fileCount: files.length, + diffFile + }; + }) + }); +} + // mcp/prInfo.ts var CLOSING_ISSUES_QUERY = ` query($owner: String!, $repo: String!, $number: Int!) { @@ -121253,8 +121302,8 @@ function CreatePullRequestReviewTool(ctx) { } // mcp/reviewComments.ts -import { writeFileSync as writeFileSync3 } from "node:fs"; -import { join as join5 } from "node:path"; +import { writeFileSync as writeFileSync4 } from "node:fs"; +import { join as join6 } from "node:path"; var REPLY_TO_FRAGMENT = ` replyTo { databaseId @@ -121421,8 +121470,8 @@ function GetReviewCommentsTool(ctx) { throw new Error("PULLFROG_TEMP_DIR not set"); } const filename = approved_by ? `review-${review_id}-approved-by-${approved_by}.xml` : `review-${review_id}-comments.xml`; - const commentsPath = join5(tempDir, filename); - writeFileSync3(commentsPath, content); + const commentsPath = join6(tempDir, filename); + writeFileSync4(commentsPath, content); log.info(`wrote ${comments.length} comments to ${commentsPath}`); log.box(content); return { @@ -121568,6 +121617,7 @@ async function startMcpHttpServer(ctx) { CreatePullRequestTool(ctx), CreatePullRequestReviewTool(ctx), PullRequestInfoTool(ctx), + CommitInfoTool(ctx), CheckoutPrTool(ctx), GetReviewCommentsTool(ctx), ListPullRequestReviewsTool(ctx), @@ -121766,7 +121816,7 @@ import { spawn as spawn3 } from "child_process"; import { createInterface } from "readline"; import * as fs2 from "fs"; import { stat as statPromise, open } from "fs/promises"; -import { join as join6 } from "path"; +import { join as join7 } from "path"; import { homedir } from "os"; import { dirname, join as join22 } from "path"; import { cwd } from "process"; @@ -128468,7 +128518,7 @@ function shouldShowDebugMessage(message, filter) { return shouldShowDebugCategories(categories, filter); } function getClaudeConfigHomeDir() { - return process.env.CLAUDE_CONFIG_DIR ?? join6(homedir(), ".claude"); + return process.env.CLAUDE_CONFIG_DIR ?? join7(homedir(), ".claude"); } function isEnvTruthy(envVar) { if (!envVar) @@ -138375,7 +138425,7 @@ import { spawnSync as spawnSync2 } from "node:child_process"; import { chmodSync, createWriteStream as createWriteStream2, existsSync as existsSync5 } from "node:fs"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join as join7 } from "node:path"; +import { join as join8 } from "node:path"; import { pipeline } from "node:stream/promises"; async function installFromNpmTarball(params) { let resolvedVersion = params.version; @@ -138399,7 +138449,7 @@ async function installFromNpmTarball(params) { } log.debug(`\xBB installing ${params.packageName}@${resolvedVersion}...`); const tempDir = process.env.PULLFROG_TEMP_DIR; - const tarballPath = join7(tempDir, "package.tgz"); + const tarballPath = join8(tempDir, "package.tgz"); const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org"; let tarballUrl; if (params.packageName.startsWith("@")) { @@ -138428,8 +138478,8 @@ async function installFromNpmTarball(params) { `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}` ); } - const extractedDir = join7(tempDir, "package"); - const cliPath = join7(extractedDir, params.executablePath); + const extractedDir = join8(tempDir, "package"); + const cliPath = join8(extractedDir, params.executablePath); if (!existsSync5(cliPath)) { throw new Error(`Executable not found in extracted package at ${cliPath}`); } @@ -138491,10 +138541,10 @@ async function installFromGithub(params) { const assetUrl = asset.browser_download_url; log.debug(`\xBB downloading asset from ${assetUrl}...`); const tempDirPrefix = `${params.owner}-${params.repo}-github-`; - const tempDirPath = await mkdtemp(join7(tmpdir(), tempDirPrefix)); + const tempDirPath = await mkdtemp(join8(tmpdir(), tempDirPrefix)); const urlPath = new URL(assetUrl).pathname; const fileName3 = urlPath.split("/").pop() || "asset"; - const downloadPath = join7(tempDirPath, fileName3); + const downloadPath = join8(tempDirPath, fileName3); const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset"); if (!assetResponse.body) throw new Error("Response body is null"); const fileStream = createWriteStream2(downloadPath); @@ -138502,7 +138552,7 @@ async function installFromGithub(params) { log.debug(`\xBB downloaded asset to ${downloadPath}`); let cliPath; if (params.executablePath) { - cliPath = join7(tempDirPath, params.executablePath); + cliPath = join8(tempDirPath, params.executablePath); } else { cliPath = downloadPath; } @@ -138516,7 +138566,7 @@ async function installFromGithub(params) { async function installFromCurl(params) { log.info(`\xBB installing ${params.executableName}...`); const tempDir = process.env.PULLFROG_TEMP_DIR; - const installScriptPath = join7(tempDir, "install.sh"); + const installScriptPath = join8(tempDir, "install.sh"); log.debug(`\xBB downloading install script from ${params.installUrl}...`); const installScriptResponse = await fetch(params.installUrl); if (!installScriptResponse.ok) { @@ -138535,7 +138585,7 @@ async function installFromCurl(params) { // ensuring a fresh install for each run HOME: tempDir, // XDG_CONFIG_HOME must match HOME so CLI tools find config in the right place - XDG_CONFIG_HOME: join7(tempDir, ".config"), + XDG_CONFIG_HOME: join8(tempDir, ".config"), SHELL: process.env.SHELL, USER: process.env.USER }, @@ -138548,7 +138598,7 @@ async function installFromCurl(params) { `Failed to install ${params.executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}` ); } - const cliPath = join7(tempDir, ".local", "bin", params.executableName); + const cliPath = join8(tempDir, ".local", "bin", params.executableName); if (!existsSync5(cliPath)) { throw new Error(`Executable not found at ${cliPath}`); } @@ -138722,8 +138772,8 @@ var messageHandlers = { }; // agents/codex.ts -import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs"; -import { join as join8 } from "node:path"; +import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "node:fs"; +import { join as join9 } from "node:path"; // node_modules/.pnpm/@openai+codex-sdk@0.80.0/node_modules/@openai/codex-sdk/dist/index.js import { promises as fs3 } from "fs"; @@ -139083,9 +139133,9 @@ var codexReasoningEffort = { max: "high" }; function writeCodexConfig(ctx) { - const codexDir = join8(ctx.tmpdir, ".codex"); + const codexDir = join9(ctx.tmpdir, ".codex"); mkdirSync3(codexDir, { recursive: true }); - const configPath = join8(codexDir, "config.toml"); + const configPath = join9(codexDir, "config.toml"); log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`); const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}] url = "${ctx.mcpServerUrl}"`]; @@ -139097,7 +139147,7 @@ url = "${ctx.mcpServerUrl}"`]; } const featuresSection = features.length > 0 ? `[features] ${features.join("\n")}` : ""; - writeFileSync4( + writeFileSync5( configPath, `# written by pullfrog ${featuresSection} @@ -139120,7 +139170,7 @@ var codex = agent({ install: installCodex, run: async (ctx) => { const cliPath = await installCodex(); - const configDir = join8(ctx.tmpdir, ".config", "codex"); + const configDir = join9(ctx.tmpdir, ".config", "codex"); mkdirSync3(configDir, { recursive: true }); const codexDir = writeCodexConfig(ctx); process.env.HOME = ctx.tmpdir; @@ -139265,9 +139315,9 @@ var messageHandlers2 = { // agents/cursor.ts import { spawn as spawn5 } from "node:child_process"; -import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs"; +import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs"; import { homedir as homedir2 } from "node:os"; -import { join as join9 } from "node:path"; +import { join as join10 } from "node:path"; var cursorEffortModels = { mini: null, // use default (auto) @@ -139292,7 +139342,7 @@ var cursor = agent({ const cliPath = await installCursor(); configureCursorMcpServers(ctx); configureCursorTools(ctx); - const projectCliConfigPath = join9(process.cwd(), ".cursor", "cli.json"); + const projectCliConfigPath = join10(process.cwd(), ".cursor", "cli.json"); let modelOverride = null; if (existsSync6(projectCliConfigPath)) { try { @@ -139457,21 +139507,21 @@ var cursor = agent({ } }); function getCursorConfigDir() { - return join9(homedir2(), ".cursor"); + return join10(homedir2(), ".cursor"); } function configureCursorMcpServers(ctx) { const cursorConfigDir = getCursorConfigDir(); - const mcpConfigPath = join9(cursorConfigDir, "mcp.json"); + const mcpConfigPath = join10(cursorConfigDir, "mcp.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const mcpServers = { [ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl } }; - writeFileSync5(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8"); + writeFileSync6(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8"); log.info(`\xBB MCP config written to ${mcpConfigPath}`); } function configureCursorTools(ctx) { const cursorConfigDir = getCursorConfigDir(); - const cliConfigPath = join9(cursorConfigDir, "cli-config.json"); + const cliConfigPath = join10(cursorConfigDir, "cli-config.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const bash = ctx.payload.bash; const deny = []; @@ -139490,14 +139540,14 @@ function configureCursorTools(ctx) { networkAccess: "allowlist" }; } - writeFileSync5(cliConfigPath, JSON.stringify(config4, null, 2), "utf-8"); + writeFileSync6(cliConfigPath, JSON.stringify(config4, null, 2), "utf-8"); log.info(`\xBB CLI config written to ${cliConfigPath}`, JSON.stringify(config4, null, 2)); } // agents/gemini.ts -import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "node:fs"; +import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync7 } from "node:fs"; import { homedir as homedir3 } from "node:os"; -import { join as join10 } from "node:path"; +import { join as join11 } from "node:path"; var geminiEffortConfig = { // https://ai.google.dev/gemini-api/docs/models // the docs mention needing to enable preview features for these models but if you @@ -139668,8 +139718,8 @@ function configureGeminiSettings(ctx) { const { model, thinkingLevel } = geminiEffortConfig[ctx.payload.effort]; log.info(`\xBB using model: ${model}, thinkingLevel: ${thinkingLevel}`); const realHome = homedir3(); - const geminiConfigDir = join10(realHome, ".gemini"); - const settingsPath = join10(geminiConfigDir, "settings.json"); + const geminiConfigDir = join11(realHome, ".gemini"); + const settingsPath = join11(geminiConfigDir, "settings.json"); mkdirSync5(geminiConfigDir, { recursive: true }); let existingSettings = {}; try { @@ -139706,7 +139756,7 @@ function configureGeminiSettings(ctx) { // v0.3.0+ nested format ...exclude.length > 0 && { tools: { exclude } } }; - writeFileSync6(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8"); + writeFileSync7(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8"); log.info(`\xBB Gemini settings written to ${settingsPath}`); if (exclude.length > 0) { log.info(`\xBB excluded tools: ${exclude.join(", ")}`); @@ -139715,8 +139765,8 @@ function configureGeminiSettings(ctx) { } // agents/opencode.ts -import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "node:fs"; -import { join as join11 } from "node:path"; +import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "node:fs"; +import { join as join12 } from "node:path"; async function installOpencode() { return await installFromNpmTarball({ packageName: "opencode-ai", @@ -139731,7 +139781,7 @@ var opencode = agent({ run: async (ctx) => { const cliPath = await installOpencode(); const tempHome = ctx.tmpdir; - const configDir = join11(tempHome, ".config", "opencode"); + const configDir = join12(tempHome, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); configureOpenCode(ctx); const args3 = ["run", ctx.instructions.full, "--format", "json"]; @@ -139739,7 +139789,7 @@ var opencode = agent({ const env3 = { ...process.env, HOME: tempHome, - XDG_CONFIG_HOME: join11(tempHome, ".config"), + XDG_CONFIG_HOME: join12(tempHome, ".config"), // set GOOGLE_GENERATIVE_AI_API_KEY alias for Google provider compatibility (if not already set) GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY }; @@ -139842,9 +139892,9 @@ var opencode = agent({ } }); function configureOpenCode(ctx) { - const configDir = join11(ctx.tmpdir, ".config", "opencode"); + const configDir = join12(ctx.tmpdir, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); - const configPath = join11(configDir, "opencode.json"); + const configPath = join12(configDir, "opencode.json"); const opencodeMcpServers = { [ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl } }; @@ -139862,7 +139912,7 @@ function configureOpenCode(ctx) { }; const configJson = JSON.stringify(config4, null, 2); try { - writeFileSync7(configPath, configJson, "utf-8"); + writeFileSync8(configPath, configJson, "utf-8"); } catch (error50) { log.error( `failed to write OpenCode config to ${configPath}: ${error50 instanceof Error ? error50.message : String(error50)}` @@ -140573,9 +140623,9 @@ async function handleAgentResult(result) { import { execSync as execSync2 } from "node:child_process"; import { mkdtempSync } from "node:fs"; import { tmpdir as tmpdir2 } from "node:os"; -import { join as join12 } from "node:path"; +import { join as join13 } from "node:path"; function createTempDirectory() { - const sharedTempDir = mkdtempSync(join12(tmpdir2(), "pullfrog-")); + const sharedTempDir = mkdtempSync(join13(tmpdir2(), "pullfrog-")); process.env.PULLFROG_TEMP_DIR = sharedTempDir; log.info(`\xBB created temp dir at ${sharedTempDir}`); return sharedTempDir; diff --git a/mcp/commitInfo.ts b/mcp/commitInfo.ts new file mode 100644 index 0000000..fd02577 --- /dev/null +++ b/mcp/commitInfo.ts @@ -0,0 +1,60 @@ +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { type } from "arktype"; +import { log } from "../utils/cli.ts"; +import { formatFilesWithLineNumbers } from "./checkout.ts"; +import type { ToolContext } from "./server.ts"; +import { execute, tool } from "./shared.ts"; + +export const CommitInfo = type({ + sha: type.string.describe("the commit SHA (full or abbreviated) to fetch"), +}); + +export function CommitInfoTool(ctx: ToolContext) { + return tool({ + name: "get_commit_info", + description: + "Retrieve commit metadata and diff via GitHub API. Use this instead of git show for reviewing commits - " + + "it works with shallow clones and shows the actual changes in the commit. Returns diffPath pointing to formatted diff file.", + parameters: CommitInfo, + execute: execute(async ({ sha }) => { + const response = await ctx.octokit.rest.repos.getCommit({ + owner: ctx.repo.owner, + repo: ctx.repo.name, + ref: sha, + }); + + const data = response.data; + const files = data.files ?? []; + + // format diff with line numbers and write to file + const diffContent = formatFilesWithLineNumbers(files); + const tempDir = process.env.PULLFROG_TEMP_DIR; + if (!tempDir) { + throw new Error( + "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context" + ); + } + const diffFile = join(tempDir, `commit-${sha.slice(0, 7)}.diff`); + writeFileSync(diffFile, diffContent); + log.debug(`wrote commit diff to ${diffFile} (${diffContent.length} bytes)`); + + return { + sha: data.sha, + message: data.commit.message, + author: data.author?.login ?? null, + committer: data.committer?.login ?? null, + date: data.commit.author?.date ?? data.commit.committer?.date ?? "", + url: data.html_url, + parents: data.parents.map((p) => p.sha), + stats: { + additions: data.stats?.additions ?? 0, + deletions: data.stats?.deletions ?? 0, + total: data.stats?.total ?? 0, + }, + fileCount: files.length, + diffFile, + }; + }), + }); +} diff --git a/mcp/server.ts b/mcp/server.ts index 1356ac9..70e29a2 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -89,6 +89,7 @@ import { GetIssueEventsTool } from "./issueEvents.ts"; import { IssueInfoTool } from "./issueInfo.ts"; import { AddLabelsTool } from "./labels.ts"; import { CreatePullRequestTool } from "./pr.ts"; +import { CommitInfoTool } from "./commitInfo.ts"; import { PullRequestInfoTool } from "./prInfo.ts"; import { CreatePullRequestReviewTool } from "./review.ts"; import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts"; @@ -171,6 +172,7 @@ export async function startMcpHttpServer( CreatePullRequestTool(ctx), CreatePullRequestReviewTool(ctx), PullRequestInfoTool(ctx), + CommitInfoTool(ctx), CheckoutPrTool(ctx), GetReviewCommentsTool(ctx), ListPullRequestReviewsTool(ctx),