Add get_commit_info

This commit is contained in:
Colin McDonnell
2026-01-21 03:22:29 +00:00
committed by pullfrog[bot]
parent 22704dda35
commit 2f3c48edb6
3 changed files with 156 additions and 44 deletions
+94 -44
View File
@@ -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;
+60
View File
@@ -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,
};
}),
});
}
+2
View File
@@ -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),