+
+Once added, you can start triggering agent runs.
- **Tag `@pullfrog`** — Tag `@pullfrog` in a comment anywhere in your repo. It will pull in any relevant context using the action's internal MCP server and perform the appropriate task.
- **Prompt from the web** — Trigger arbitrary tasks from the Pullfrog dashboard
@@ -47,7 +53,6 @@ Install the Pullfrog GitHub App on your personal or organization account. During
[Add to GitHub ➜](https://github.com/apps/pullfrog/installations/new)
-
Manual setup instructions
From cd930fef8e389833e50edbac2acd14f1720cf2a1 Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 14:17:14 -0800
Subject: [PATCH 08/17] format button
---
README.md | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 7d19a7c..3244581 100644
--- a/README.md
+++ b/README.md
@@ -18,11 +18,9 @@
Pullfrog is a GitHub bot that brings the full power of your favorite coding agents into GitHub. It's open source and powered by GitHub Actions.
-
+
+
+
Once added, you can start triggering agent runs.
From b2b75bacc0966e35588e9899972ae1989c55e206 Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 14:17:53 -0800
Subject: [PATCH 09/17] format button
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 3244581..ecad080 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
Pullfrog is a GitHub bot that brings the full power of your favorite coding agents into GitHub. It's open source and powered by GitHub Actions.
-
+
Once added, you can start triggering agent runs.
From 55a51650661442dd9ab4ec616341d46c56e1c5d7 Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 14:20:07 -0800
Subject: [PATCH 10/17] format button
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index ecad080..0f5406b 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,8 @@ Pullfrog is a GitHub bot that brings the full power of your favorite coding agen
+
+
Once added, you can start triggering agent runs.
- **Tag `@pullfrog`** — Tag `@pullfrog` in a comment anywhere in your repo. It will pull in any relevant context using the action's internal MCP server and perform the appropriate task.
From f1626f9aa7ba7f23d7f43cd7aaa4b4c982d55ebf Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 14:23:16 -0800
Subject: [PATCH 11/17] format button
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0f5406b..2574b52 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Pullfrog is a GitHub bot that brings the full power of your favorite coding agen
-
+
Once added, you can start triggering agent runs.
From e54e7f1353a609b39fcf1a1f70c4d6ddac9be10e Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 14:23:40 -0800
Subject: [PATCH 12/17] format button
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 2574b52..0f5406b 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Pullfrog is a GitHub bot that brings the full power of your favorite coding agen
-
+
Once added, you can start triggering agent runs.
From 007bc8a6114040731024cbdc92f8749acd0448fe Mon Sep 17 00:00:00 2001
From: David Blass
Date: Wed, 26 Nov 2025 17:24:37 -0500
Subject: [PATCH 13/17] add get_issue tools
---
fixtures/basic.txt | 2 +-
mcp/issueComments.ts | 35 +++++++++++++++++
mcp/issueEvents.ts | 93 ++++++++++++++++++++++++++++++++++++++++++++
mcp/issueInfo.ts | 55 ++++++++++++++++++++++++++
mcp/server.ts | 6 +++
5 files changed, 190 insertions(+), 1 deletion(-)
create mode 100644 mcp/issueComments.ts
create mode 100644 mcp/issueEvents.ts
create mode 100644 mcp/issueInfo.ts
diff --git a/fixtures/basic.txt b/fixtures/basic.txt
index b0ea008..bc823bf 100644
--- a/fixtures/basic.txt
+++ b/fixtures/basic.txt
@@ -1 +1 @@
-use debug_shell_command tool to run 'git status' and explain the result
\ No newline at end of file
+summarize https://github.com/pullfrogai/scratch/issues/56, its status, and pertinent discussion on the issue
\ No newline at end of file
diff --git a/mcp/issueComments.ts b/mcp/issueComments.ts
new file mode 100644
index 0000000..6c2acbd
--- /dev/null
+++ b/mcp/issueComments.ts
@@ -0,0 +1,35 @@
+import { type } from "arktype";
+import { contextualize, tool } from "./shared.ts";
+
+export const GetIssueComments = type({
+ issue_number: type.number.describe("The issue number to get comments for"),
+});
+
+export const GetIssueCommentsTool = 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: contextualize(async ({ issue_number }, ctx) => {
+ const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
+ owner: ctx.owner,
+ repo: ctx.name,
+ issue_number,
+ });
+
+ return {
+ issue_number,
+ comments: comments.map((comment) => ({
+ 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,
+ };
+ }),
+});
+
diff --git a/mcp/issueEvents.ts b/mcp/issueEvents.ts
new file mode 100644
index 0000000..c4e5d32
--- /dev/null
+++ b/mcp/issueEvents.ts
@@ -0,0 +1,93 @@
+import { type } from "arktype";
+import { contextualize, tool } from "./shared.ts";
+
+export const GetIssueEvents = type({
+ issue_number: type.number.describe("The issue number to get events for"),
+});
+
+export const GetIssueEventsTool = 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: contextualize(async ({ issue_number }, ctx) => {
+ const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
+ owner: ctx.owner,
+ repo: ctx.name,
+ issue_number,
+ });
+
+ // Only include events not reflected in current issue state (get_issue already has labels, assignees, state, etc.)
+ // Keep only relationship/reference events that show connections to other issues/PRs/commits
+ const relevantEventTypes = new Set(["cross_referenced", "referenced"]);
+
+ const parsedEvents = events.flatMap((event) => {
+ // Filter to only events with an 'event' property and relevant types
+ if (!("event" in event) || !relevantEventTypes.has(event.event)) {
+ return [];
+ }
+
+ const baseEvent: Record = {
+ event: event.event,
+ };
+
+ // Common fields
+ if ("id" in event) {
+ baseEvent.id = event.id;
+ }
+ if ("actor" in event && event.actor) {
+ baseEvent.actor = event.actor.login;
+ } else if ("user" in event && event.user) {
+ baseEvent.actor = event.user.login;
+ }
+ if ("created_at" in event) {
+ baseEvent.created_at = event.created_at;
+ }
+
+ // Event-specific data
+ if (event.event === "cross_referenced") {
+ if ("source" in event && event.source) {
+ const source = event.source as {
+ type?: string;
+ issue?: { number: number; title: string; html_url: string };
+ pull_request?: { number: number; title: string; html_url: string };
+ };
+ baseEvent.source = {
+ type: source.type,
+ issue: source.issue
+ ? {
+ number: source.issue.number,
+ title: source.issue.title,
+ html_url: source.issue.html_url,
+ }
+ : null,
+ pull_request: source.pull_request
+ ? {
+ number: source.pull_request.number,
+ title: source.pull_request.title,
+ html_url: source.pull_request.html_url,
+ }
+ : null,
+ };
+ }
+ }
+
+ if (event.event === "referenced") {
+ if ("commit_id" in event) {
+ baseEvent.commit_id = event.commit_id;
+ }
+ if ("commit_url" in event) {
+ baseEvent.commit_url = event.commit_url;
+ }
+ }
+
+ return [baseEvent];
+ });
+
+ return {
+ issue_number,
+ events: parsedEvents,
+ count: parsedEvents.length,
+ };
+ }),
+});
diff --git a/mcp/issueInfo.ts b/mcp/issueInfo.ts
new file mode 100644
index 0000000..e8a6f5a
--- /dev/null
+++ b/mcp/issueInfo.ts
@@ -0,0 +1,55 @@
+import { type } from "arktype";
+import { contextualize, tool } from "./shared.ts";
+
+export const IssueInfo = type({
+ issue_number: type.number.describe("The issue number to fetch"),
+});
+
+export const IssueInfoTool = tool({
+ name: "get_issue",
+ description: "Retrieve GitHub issue information by issue number",
+ parameters: IssueInfo,
+ execute: contextualize(async ({ issue_number }, ctx) => {
+ const issue = await ctx.octokit.rest.issues.get({
+ owner: ctx.owner,
+ repo: ctx.name,
+ issue_number,
+ });
+
+ const data = issue.data;
+
+ const hints: string[] = [];
+ if (data.comments > 0) {
+ hints.push("use get_issue_comments to retrieve all comments for this issue");
+ }
+ hints.push(
+ "use get_issue_events to retrieve cross-references and commit references (relationships not reflected in current state)"
+ );
+
+ return {
+ number: data.number,
+ url: data.html_url,
+ title: data.title,
+ body: data.body,
+ state: data.state,
+ locked: data.locked,
+ labels: data.labels?.map((label) => (typeof label === "string" ? label : label.name)),
+ assignees: data.assignees?.map((assignee) => assignee.login),
+ user: data.user?.login,
+ created_at: data.created_at,
+ updated_at: data.updated_at,
+ closed_at: data.closed_at,
+ comments: data.comments,
+ milestone: data.milestone?.title,
+ pull_request: data.pull_request
+ ? {
+ url: data.pull_request.url,
+ html_url: data.pull_request.html_url,
+ diff_url: data.pull_request.diff_url,
+ patch_url: data.pull_request.patch_url,
+ }
+ : null,
+ hints,
+ };
+ }),
+});
diff --git a/mcp/server.ts b/mcp/server.ts
index e116ddc..7fad9c0 100644
--- a/mcp/server.ts
+++ b/mcp/server.ts
@@ -12,6 +12,9 @@ import {
} from "./comment.ts";
import { DebugShellCommandTool } from "./debug.ts";
import { IssueTool } from "./issue.ts";
+import { GetIssueCommentsTool } from "./issueComments.ts";
+import { GetIssueEventsTool } from "./issueEvents.ts";
+import { IssueInfoTool } from "./issueInfo.ts";
import { PullRequestTool } from "./pr.ts";
import { PullRequestInfoTool } from "./prInfo.ts";
import { ReviewTool } from "./review.ts";
@@ -64,6 +67,9 @@ export async function startMcpHttpServer(): Promise<{ url: string; close: () =>
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
+ IssueInfoTool,
+ GetIssueCommentsTool,
+ GetIssueEventsTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
From fd5e9c283881024a163e768bb4430787000515b0 Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 16:33:29 -0800
Subject: [PATCH 14/17] update action w setup instructions
---
.github/workflows/pullfrog.yml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml
index 29dccdd..22a5a6b 100644
--- a/.github/workflows/pullfrog.yml
+++ b/.github/workflows/pullfrog.yml
@@ -23,6 +23,11 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 1
+
+ # optionally, setup your repo here
+ # the agent can figure this out itself, but pre-setup is more efficient
+ # - uses: actions/setup-node@v6
+
- name: Run agent
uses: pullfrog/action@v0
with:
From bddadfa70fbf6350a65ee03331ace903b6deca50 Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 19:18:35 -0800
Subject: [PATCH 15/17] update img hrefs
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0f5406b..8bea6ee 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-
+
Pullfrog
From 2ed4d445f736c5fc343206939647c911fa8fcceb Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 23:03:09 -0800
Subject: [PATCH 16/17] make codex yolo
---
agents/codex.ts | 3 +-
entry | 765 ++++++++++++++++++++++++++----------------------
2 files changed, 421 insertions(+), 347 deletions(-)
diff --git a/agents/codex.ts b/agents/codex.ts
index 83938fa..b2386eb 100644
--- a/agents/codex.ts
+++ b/agents/codex.ts
@@ -42,7 +42,8 @@ export const codex = agent({
const codex = new Codex(codexOptions);
const thread = codex.startThread({
approvalPolicy: "never",
- sandboxMode: "workspace-write",
+ // use danger-full-access to allow git operations (workspace-write blocks .git directory writes)
+ sandboxMode: "danger-full-access",
networkAccessEnabled: true,
});
diff --git a/entry b/entry
index 5dc8835..099c90b 100755
--- a/entry
+++ b/entry
@@ -18153,7 +18153,7 @@ var require_summary = __commonJS({
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
var os_1 = __require("os");
var fs_1 = __require("fs");
- var { access, appendFile, writeFile: writeFile2 } = fs_1.promises;
+ var { access, appendFile, writeFile } = fs_1.promises;
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
var Summary = class {
@@ -18211,7 +18211,7 @@ var require_summary = __commonJS({
return __awaiter(this, void 0, void 0, function* () {
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
const filePath = yield this.filePath();
- const writeFunc = overwrite ? writeFile2 : appendFile;
+ const writeFunc = overwrite ? writeFile : appendFile;
yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
return this.emptyBuffer();
});
@@ -47775,7 +47775,7 @@ var require_snapshot_utils = __commonJS({
var require_snapshot_recorder = __commonJS({
"../node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
"use strict";
- var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises");
+ var { writeFile, readFile, mkdir } = __require("node:fs/promises");
var { dirname: dirname2, resolve } = __require("node:path");
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
var { InvalidArgumentError, UndiciError } = require_errors2();
@@ -48005,7 +48005,7 @@ var require_snapshot_recorder = __commonJS({
hash,
snapshot: snapshot2
}));
- await writeFile2(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
+ await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
}
/**
* Clears all recorded snapshots
@@ -83934,8 +83934,6 @@ var package_default = {
// utils/cli.ts
var core = __toESM(require_core(), 1);
var import_table = __toESM(require_src(), 1);
-import { appendFileSync as appendFileSync2, existsSync as existsSync2 } from "node:fs";
-import { join as join4 } from "node:path";
var isGitHubActions = !!process.env.GITHUB_ACTIONS;
var isDebugEnabled = () => process.env.LOG_LEVEL === "debug";
function startGroup2(name) {
@@ -84179,31 +84177,8 @@ var log = {
output += formatIndentedField("input", inputFormatted);
}
log.info(output.trimEnd());
- },
- /**
- * Log MCP tool call information to mcpLog.txt in the temp directory
- */
- toolCallToFile: ({
- toolName,
- request: request2,
- result,
- error: error41
- }) => {
- const logPath = getMcpLogPath();
- const params = { toolName, request: request2 };
- if (error41) {
- params.error = error41;
- } else if (result) {
- params.result = result;
- }
- const logEntry = formatToolCall(params);
- appendFileSync2(logPath, logEntry, "utf-8");
}
};
-function getMcpLogPath() {
- const tempDir = process.env.PULLFROG_TEMP_DIR;
- return join4(tempDir, "mcpLog.txt");
-}
function formatJsonValue(value2) {
const compact = JSON.stringify(value2);
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value2, null, 2) : compact;
@@ -84222,39 +84197,6 @@ function formatIndentedField(label, content) {
}
return formatted;
}
-function formatToolInput(request2) {
- const requestFormatted = formatJsonValue(request2);
- if (requestFormatted === "{}") {
- return "";
- }
- return formatIndentedField("input", requestFormatted);
-}
-function formatToolResult(result) {
- try {
- const parsed2 = JSON.parse(result);
- const formatted = formatJsonValue(parsed2);
- return formatIndentedField("result", formatted);
- } catch {
- return formatIndentedField("result", result);
- }
-}
-function formatToolCall({
- toolName,
- request: request2,
- result,
- error: error41
-}) {
- let logEntry = `\u2192 ${toolName}
-`;
- logEntry += formatToolInput(request2);
- if (error41) {
- logEntry += formatIndentedField("error", error41);
- } else if (result) {
- logEntry += formatToolResult(result);
- }
- logEntry += "\n";
- return logEntry;
-}
// ../node_modules/.pnpm/@toon-format+toon@1.0.0/node_modules/@toon-format/toon/dist/index.js
var LIST_ITEM_MARKER = "-";
@@ -92415,11 +92357,216 @@ ${encode(payload.event)}`;
// agents/shared.ts
import { spawnSync } from "node:child_process";
-import { chmodSync, createWriteStream as createWriteStream2, existsSync as existsSync3 } from "node:fs";
+import { chmodSync, createWriteStream as createWriteStream2, existsSync as existsSync2 } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
-import { join as join5 } from "node:path";
+import { join as join4 } from "node:path";
import { pipeline } from "node:stream/promises";
+
+// utils/github.ts
+var core2 = __toESM(require_core(), 1);
+import { createSign } from "node:crypto";
+function isGitHubActionsEnvironment() {
+ return Boolean(process.env.GITHUB_ACTIONS);
+}
+async function acquireTokenViaOIDC() {
+ log.info("Generating OIDC token...");
+ const oidcToken = await core2.getIDToken("pullfrog-api");
+ log.info("OIDC token generated successfully");
+ const apiUrl = process.env.API_URL || "https://pullfrog.ai";
+ log.info("Exchanging OIDC token for installation token...");
+ const timeoutMs = 5e3;
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
+ try {
+ const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${oidcToken}`,
+ "Content-Type": "application/json"
+ },
+ signal: controller.signal
+ });
+ clearTimeout(timeoutId);
+ if (!tokenResponse.ok) {
+ throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
+ }
+ const tokenData = await tokenResponse.json();
+ log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
+ return tokenData.token;
+ } catch (error41) {
+ clearTimeout(timeoutId);
+ if (error41 instanceof Error && error41.name === "AbortError") {
+ throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
+ }
+ throw error41;
+ }
+}
+var base64UrlEncode = (str) => {
+ return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+};
+var generateJWT = (appId, privateKey) => {
+ const now = Math.floor(Date.now() / 1e3);
+ const payload = {
+ iat: now - 60,
+ exp: now + 5 * 60,
+ iss: appId
+ };
+ const header = {
+ alg: "RS256",
+ typ: "JWT"
+ };
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
+ const signaturePart = `${encodedHeader}.${encodedPayload}`;
+ const signature = createSign("RSA-SHA256").update(signaturePart).sign(privateKey, "base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+ return `${signaturePart}.${signature}`;
+};
+var githubRequest = async (path3, options = {}) => {
+ const { method = "GET", headers = {}, body } = options;
+ const url2 = `https://api.github.com${path3}`;
+ const requestHeaders = {
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
+ ...headers
+ };
+ const response = await fetch(url2, {
+ method,
+ headers: requestHeaders,
+ ...body && { body }
+ });
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(
+ `GitHub API request failed: ${response.status} ${response.statusText}
+${errorText}`
+ );
+ }
+ return response.json();
+};
+var checkRepositoryAccess = async (token, repoOwner, repoName) => {
+ try {
+ const response = await githubRequest("/installation/repositories", {
+ headers: { Authorization: `token ${token}` }
+ });
+ return response.repositories.some(
+ (repo) => repo.owner.login === repoOwner && repo.name === repoName
+ );
+ } catch {
+ return false;
+ }
+};
+var createInstallationToken = async (jwt, installationId) => {
+ const response = await githubRequest(
+ `/app/installations/${installationId}/access_tokens`,
+ {
+ method: "POST",
+ headers: { Authorization: `Bearer ${jwt}` }
+ }
+ );
+ return response.token;
+};
+var findInstallationId = async (jwt, repoOwner, repoName) => {
+ const installations = await githubRequest("/app/installations", {
+ headers: { Authorization: `Bearer ${jwt}` }
+ });
+ for (const installation of installations) {
+ try {
+ const tempToken = await createInstallationToken(jwt, installation.id);
+ const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
+ if (hasAccess) {
+ return installation.id;
+ }
+ } catch {
+ }
+ }
+ throw new Error(
+ `No installation found with access to ${repoOwner}/${repoName}. Ensure the GitHub App is installed on the target repository.`
+ );
+};
+async function acquireTokenViaGitHubApp() {
+ const repoContext = parseRepoContext();
+ const config2 = {
+ appId: process.env.GITHUB_APP_ID,
+ privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
+ repoOwner: repoContext.owner,
+ repoName: repoContext.name
+ };
+ const jwt = generateJWT(config2.appId, config2.privateKey);
+ const installationId = await findInstallationId(jwt, config2.repoOwner, config2.repoName);
+ const token = await createInstallationToken(jwt, installationId);
+ return token;
+}
+async function acquireNewToken() {
+ if (isGitHubActionsEnvironment()) {
+ return await acquireTokenViaOIDC();
+ } else {
+ return await acquireTokenViaGitHubApp();
+ }
+}
+var githubInstallationToken;
+async function setupGitHubInstallationToken() {
+ const acquiredToken = await acquireNewToken();
+ core2.setSecret(acquiredToken);
+ githubInstallationToken = acquiredToken;
+ return acquiredToken;
+}
+function getGitHubInstallationToken() {
+ if (!githubInstallationToken) {
+ throw new Error("GitHub installation token not set. Call setupGitHubInstallationToken first.");
+ }
+ return githubInstallationToken;
+}
+async function revokeGitHubInstallationToken() {
+ if (!githubInstallationToken) {
+ return;
+ }
+ const token = githubInstallationToken;
+ githubInstallationToken = void 0;
+ const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
+ try {
+ await fetch(`${apiUrl}/installation/token`, {
+ method: "DELETE",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+ });
+ log.info("Installation token revoked");
+ } catch (error41) {
+ log.warning(
+ `Failed to revoke installation token: ${error41 instanceof Error ? error41.message : String(error41)}`
+ );
+ }
+}
+function parseRepoContext() {
+ const githubRepo = process.env.GITHUB_REPOSITORY;
+ if (!githubRepo) {
+ throw new Error("GITHUB_REPOSITORY environment variable is required");
+ }
+ const [owner, name] = githubRepo.split("/");
+ if (!owner || !name) {
+ throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
+ }
+ return { owner, name };
+}
+
+// agents/shared.ts
+function createAgentEnv(agentSpecificVars) {
+ return {
+ PATH: process.env.PATH,
+ HOME: process.env.HOME,
+ LOG_LEVEL: process.env.LOG_LEVEL,
+ NODE_ENV: process.env.NODE_ENV,
+ GITHUB_TOKEN: getGitHubInstallationToken(),
+ ...agentSpecificVars
+ // values could be undefined but will be ignored
+ };
+}
+function setupProcessAgentEnv(agentSpecificVars) {
+ Object.assign(process.env, createAgentEnv(agentSpecificVars));
+}
async function installFromNpmTarball({
packageName,
version: version2,
@@ -92447,7 +92594,7 @@ async function installFromNpmTarball({
}
log.info(`\u{1F4E6} Installing ${packageName}@${resolvedVersion}...`);
const tempDir = process.env.PULLFROG_TEMP_DIR;
- const tarballPath = join5(tempDir, "package.tgz");
+ const tarballPath = join4(tempDir, "package.tgz");
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
let tarballUrl;
if (packageName.startsWith("@")) {
@@ -92476,9 +92623,9 @@ async function installFromNpmTarball({
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
);
}
- const extractedDir = join5(tempDir, "package");
- const cliPath = join5(extractedDir, executablePath);
- if (!existsSync3(cliPath)) {
+ const extractedDir = join4(tempDir, "package");
+ const cliPath = join4(extractedDir, executablePath);
+ if (!existsSync2(cliPath)) {
throw new Error(`Executable not found in extracted package at ${cliPath}`);
}
if (installDependencies) {
@@ -92524,10 +92671,10 @@ async function installFromGithub({
const assetUrl = asset.browser_download_url;
log.info(`Downloading asset from ${assetUrl}...`);
const tempDirPrefix = `${owner}-${repo}-github-`;
- const tempDir = await mkdtemp(join5(tmpdir(), tempDirPrefix));
+ const tempDir = await mkdtemp(join4(tmpdir(), tempDirPrefix));
const urlPath = new URL(assetUrl).pathname;
const fileName2 = urlPath.split("/").pop() || "asset";
- const downloadPath = join5(tempDir, fileName2);
+ const downloadPath = join4(tempDir, fileName2);
const assetResponse = await fetch(assetUrl);
if (!assetResponse.ok) {
throw new Error(
@@ -92540,11 +92687,11 @@ async function installFromGithub({
log.info(`Downloaded asset to ${downloadPath}`);
let cliPath;
if (executablePath) {
- cliPath = join5(tempDir, executablePath);
+ cliPath = join4(tempDir, executablePath);
} else {
cliPath = downloadPath;
}
- if (!existsSync3(cliPath)) {
+ if (!existsSync2(cliPath)) {
throw new Error(`Executable not found at ${cliPath}`);
}
chmodSync(cliPath, 493);
@@ -92557,7 +92704,7 @@ async function installFromCurl({
}) {
log.info(`\u{1F4E6} Installing ${executableName}...`);
const tempDir = process.env.PULLFROG_TEMP_DIR;
- const installScriptPath = join5(tempDir, "install.sh");
+ const installScriptPath = join4(tempDir, "install.sh");
log.info(`Downloading install script from ${installUrl}...`);
const installScriptResponse = await fetch(installUrl);
if (!installScriptResponse.ok) {
@@ -92572,12 +92719,11 @@ async function installFromCurl({
const installResult = spawnSync("bash", [installScriptPath], {
cwd: tempDir,
env: {
+ // Run the install script with HOME set to temp directory
+ // ensuring a fresh install for each run
HOME: tempDir,
- // Cursor install script uses HOME for installation path
- PATH: process.env.PATH || "",
- SHELL: process.env.SHELL || "/bin/bash",
- USER: process.env.USER || "",
- TMPDIR: process.env.TMPDIR || "/tmp"
+ SHELL: process.env.SHELL,
+ USER: process.env.USER
},
stdio: "pipe",
encoding: "utf-8"
@@ -92588,8 +92734,8 @@ async function installFromCurl({
`Failed to install ${executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`
);
}
- const cliPath = join5(tempDir, ".local", "bin", executableName);
- if (!existsSync3(cliPath)) {
+ const cliPath = join4(tempDir, ".local", "bin", executableName);
+ if (!existsSync2(cliPath)) {
throw new Error(`Executable not found at ${cliPath}`);
}
chmodSync(cliPath, 493);
@@ -92612,7 +92758,7 @@ var claude = agent({
});
},
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
- process.env.ANTHROPIC_API_KEY = apiKey;
+ setupProcessAgentEnv({ ANTHROPIC_API_KEY: apiKey });
const prompt = addInstructions(payload);
console.log(prompt);
const queryInstance = query({
@@ -92711,7 +92857,7 @@ var messageHandlers = {
// agents/codex.ts
import { spawnSync as spawnSync2 } from "node:child_process";
import { mkdirSync as mkdirSync2 } from "node:fs";
-import { join as join6 } from "node:path";
+import { join as join5 } from "node:path";
// ../node_modules/.pnpm/@openai+codex-sdk@0.58.0/node_modules/@openai/codex-sdk/dist/index.js
import { promises as fs2 } from "fs";
@@ -93046,13 +93192,14 @@ var codex = agent({
executablePath: "bin/codex.js"
});
},
- run: async ({ payload, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
- process.env.OPENAI_API_KEY = apiKey;
- process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
+ run: async ({ payload, mcpServers, apiKey, cliPath }) => {
const tempHome = process.env.PULLFROG_TEMP_DIR;
- const configDir = join6(tempHome, ".config", "codex");
+ const configDir = join5(tempHome, ".config", "codex");
mkdirSync2(configDir, { recursive: true });
- process.env.HOME = tempHome;
+ setupProcessAgentEnv({
+ OPENAI_API_KEY: apiKey,
+ HOME: tempHome
+ });
configureCodexMcpServers({ mcpServers, cliPath });
const codexOptions = {
apiKey,
@@ -93061,7 +93208,8 @@ var codex = agent({
const codex2 = new Codex(codexOptions);
const thread = codex2.startThread({
approvalPolicy: "never",
- sandboxMode: "workspace-write",
+ // use danger-full-access to allow git operations (workspace-write blocks .git directory writes)
+ sandboxMode: "danger-full-access",
networkAccessEnabled: true
});
try {
@@ -93198,7 +93346,7 @@ function configureCodexMcpServers({ mcpServers, cliPath }) {
import { spawn as spawn3 } from "node:child_process";
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
import { homedir as homedir2 } from "node:os";
-import { join as join7 } from "node:path";
+import { join as join6 } from "node:path";
var messageHandlers3 = {
system: (_event) => {
},
@@ -93251,9 +93399,7 @@ var cursor = agent({
executableName: "cursor-agent"
});
},
- run: async ({ payload, apiKey, cliPath, githubInstallationToken, mcpServers }) => {
- process.env.CURSOR_API_KEY = apiKey;
- process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
+ run: async ({ payload, apiKey, cliPath, mcpServers }) => {
configureCursorMcpServers({ mcpServers, cliPath });
try {
const fullPrompt = addInstructions(payload);
@@ -93273,16 +93419,9 @@ var cursor = agent({
],
{
cwd: process.cwd(),
- env: {
- CURSOR_API_KEY: apiKey,
- GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
- LOG_LEVEL: process.env.LOG_LEVEL,
- NODE_ENV: process.env.NODE_ENV,
- HOME: process.env.HOME,
- PATH: process.env.PATH
- // Don't override HOME - Cursor CLI needs access to macOS keychain
- // MCP config is written to tempDir/.cursor/mcp.json which Cursor will find
- },
+ env: createAgentEnv({
+ CURSOR_API_KEY: apiKey
+ }),
stdio: ["ignore", "pipe", "pipe"]
// Ignore stdin, pipe stdout/stderr
}
@@ -93356,8 +93495,8 @@ var cursor = agent({
});
function configureCursorMcpServers({ mcpServers }) {
const realHome = homedir2();
- const cursorConfigDir = join7(realHome, ".cursor");
- const mcpConfigPath = join7(cursorConfigDir, "mcp.json");
+ const cursorConfigDir = join6(realHome, ".cursor");
+ const mcpConfigPath = join6(cursorConfigDir, "mcp.json");
mkdirSync3(cursorConfigDir, { recursive: true });
const cursorMcpServers = {};
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
@@ -93537,13 +93676,11 @@ var gemini = agent({
assetName: "gemini.js"
});
},
- run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
+ run: async ({ payload, apiKey, mcpServers, cliPath }) => {
configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
}
- process.env.GEMINI_API_KEY = apiKey;
- process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
const sessionPrompt = addInstructions(payload);
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
let finalOutput = "";
@@ -93551,15 +93688,9 @@ var gemini = agent({
const result = await spawn4({
cmd: "node",
args: [cliPath, "--yolo", "--output-format=stream-json", "-p", sessionPrompt],
- env: {
- PATH: process.env.PATH || "",
- HOME: process.env.HOME || "",
- TMPDIR: process.env.TMPDIR || "/tmp",
- GEMINI_API_KEY: apiKey,
- GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
- LOG_LEVEL: process.env.LOG_LEVEL,
- NODE_ENV: process.env.NODE_ENV
- },
+ env: createAgentEnv({
+ GEMINI_API_KEY: apiKey
+ }),
timeout: 6e5,
// 10 minutes
onStdout: async (chunk) => {
@@ -93648,10 +93779,9 @@ var agents = {
};
// main.ts
-import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
-import { mkdtemp as mkdtemp2, writeFile } from "node:fs/promises";
+import { mkdtemp as mkdtemp2 } from "node:fs/promises";
import { tmpdir as tmpdir2 } from "node:os";
-import { join as join8 } from "node:path";
+import { join as join7 } from "node:path";
// mcp/config.ts
function createMcpConfigs(mcpServerUrl) {
@@ -121360,183 +121490,6 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
}
);
-// utils/github.ts
-var core2 = __toESM(require_core(), 1);
-import { createSign } from "node:crypto";
-function isGitHubActionsEnvironment() {
- return Boolean(process.env.GITHUB_ACTIONS);
-}
-async function acquireTokenViaOIDC() {
- log.info("Generating OIDC token...");
- const oidcToken = await core2.getIDToken("pullfrog-api");
- log.info("OIDC token generated successfully");
- const apiUrl = process.env.API_URL || "https://pullfrog.ai";
- log.info("Exchanging OIDC token for installation token...");
- const timeoutMs = 5e3;
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
- try {
- const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${oidcToken}`,
- "Content-Type": "application/json"
- },
- signal: controller.signal
- });
- clearTimeout(timeoutId);
- if (!tokenResponse.ok) {
- throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
- }
- const tokenData = await tokenResponse.json();
- log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
- return tokenData.token;
- } catch (error41) {
- clearTimeout(timeoutId);
- if (error41 instanceof Error && error41.name === "AbortError") {
- throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
- }
- throw error41;
- }
-}
-var base64UrlEncode = (str) => {
- return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
-};
-var generateJWT = (appId, privateKey) => {
- const now = Math.floor(Date.now() / 1e3);
- const payload = {
- iat: now - 60,
- exp: now + 5 * 60,
- iss: appId
- };
- const header = {
- alg: "RS256",
- typ: "JWT"
- };
- const encodedHeader = base64UrlEncode(JSON.stringify(header));
- const encodedPayload = base64UrlEncode(JSON.stringify(payload));
- const signaturePart = `${encodedHeader}.${encodedPayload}`;
- const signature = createSign("RSA-SHA256").update(signaturePart).sign(privateKey, "base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
- return `${signaturePart}.${signature}`;
-};
-var githubRequest = async (path3, options = {}) => {
- const { method = "GET", headers = {}, body } = options;
- const url2 = `https://api.github.com${path3}`;
- const requestHeaders = {
- Accept: "application/vnd.github.v3+json",
- "User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
- ...headers
- };
- const response = await fetch(url2, {
- method,
- headers: requestHeaders,
- ...body && { body }
- });
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(
- `GitHub API request failed: ${response.status} ${response.statusText}
-${errorText}`
- );
- }
- return response.json();
-};
-var checkRepositoryAccess = async (token, repoOwner, repoName) => {
- try {
- const response = await githubRequest("/installation/repositories", {
- headers: { Authorization: `token ${token}` }
- });
- return response.repositories.some(
- (repo) => repo.owner.login === repoOwner && repo.name === repoName
- );
- } catch {
- return false;
- }
-};
-var createInstallationToken = async (jwt, installationId) => {
- const response = await githubRequest(
- `/app/installations/${installationId}/access_tokens`,
- {
- method: "POST",
- headers: { Authorization: `Bearer ${jwt}` }
- }
- );
- return response.token;
-};
-var findInstallationId = async (jwt, repoOwner, repoName) => {
- const installations = await githubRequest("/app/installations", {
- headers: { Authorization: `Bearer ${jwt}` }
- });
- for (const installation of installations) {
- try {
- const tempToken = await createInstallationToken(jwt, installation.id);
- const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
- if (hasAccess) {
- return installation.id;
- }
- } catch {
- }
- }
- throw new Error(
- `No installation found with access to ${repoOwner}/${repoName}. Ensure the GitHub App is installed on the target repository.`
- );
-};
-async function acquireTokenViaGitHubApp() {
- const repoContext = parseRepoContext();
- const config2 = {
- appId: process.env.GITHUB_APP_ID,
- privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
- repoOwner: repoContext.owner,
- repoName: repoContext.name
- };
- const jwt = generateJWT(config2.appId, config2.privateKey);
- const installationId = await findInstallationId(jwt, config2.repoOwner, config2.repoName);
- const token = await createInstallationToken(jwt, installationId);
- return token;
-}
-async function acquireNewToken() {
- if (isGitHubActionsEnvironment()) {
- return await acquireTokenViaOIDC();
- } else {
- return await acquireTokenViaGitHubApp();
- }
-}
-async function setupGitHubInstallationToken() {
- const acquiredToken = await acquireNewToken();
- core2.setSecret(acquiredToken);
- process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
- return acquiredToken;
-}
-async function revokeInstallationToken(token) {
- const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
- try {
- await fetch(`${apiUrl}/installation/token`, {
- method: "DELETE",
- headers: {
- Accept: "application/vnd.github+json",
- Authorization: `Bearer ${token}`,
- "X-GitHub-Api-Version": "2022-11-28"
- }
- });
- log.info("Installation token revoked");
- } catch (error41) {
- log.warning(
- `Failed to revoke installation token: ${error41 instanceof Error ? error41.message : String(error41)}`
- );
- }
-}
-function parseRepoContext() {
- const githubRepo = process.env.GITHUB_REPOSITORY;
- if (!githubRepo) {
- throw new Error("GITHUB_REPOSITORY environment variable is required");
- }
- const [owner, name] = githubRepo.split("/");
- if (!owner || !name) {
- throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
- }
- return { owner, name };
-}
-
// mcp/shared.ts
function getPayload() {
const payloadEnv = process.env.PULLFROG_PAYLOAD;
@@ -121552,14 +121505,10 @@ function getPayload() {
}
}
function getMcpContext() {
- const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
- if (!githubInstallationToken) {
- throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
- }
return {
...parseRepoContext(),
octokit: new Octokit2({
- auth: githubInstallationToken
+ auth: getGitHubInstallationToken()
}),
payload: getPayload()
};
@@ -121571,14 +121520,16 @@ var addTools = (server, tools) => {
}
return server;
};
-var contextualize = (executor) => async (params) => {
- try {
- const ctx = getMcpContext();
- const result = await executor(params, ctx);
- return handleToolSuccess(result);
- } catch (error41) {
- return handleToolError(error41);
- }
+var contextualize = (executor) => {
+ return async (params) => {
+ try {
+ const ctx = getMcpContext();
+ const result = await executor(params, ctx);
+ return handleToolSuccess(result);
+ } catch (error41) {
+ return handleToolError(error41);
+ }
+ };
};
var handleToolSuccess = (data) => {
return {
@@ -121933,6 +121884,154 @@ var IssueTool = tool({
})
});
+// mcp/issueComments.ts
+var GetIssueComments = type({
+ issue_number: type.number.describe("The issue number to get comments for")
+});
+var GetIssueCommentsTool = 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: contextualize(async ({ issue_number }, ctx) => {
+ const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
+ owner: ctx.owner,
+ repo: ctx.name,
+ issue_number
+ });
+ return {
+ issue_number,
+ comments: comments.map((comment) => ({
+ 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
+ };
+ })
+});
+
+// mcp/issueEvents.ts
+var GetIssueEvents = type({
+ issue_number: type.number.describe("The issue number to get events for")
+});
+var GetIssueEventsTool = 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: contextualize(async ({ issue_number }, ctx) => {
+ const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
+ owner: ctx.owner,
+ repo: ctx.name,
+ issue_number
+ });
+ const relevantEventTypes = /* @__PURE__ */ new Set(["cross_referenced", "referenced"]);
+ const parsedEvents = events.flatMap((event) => {
+ if (!("event" in event) || !relevantEventTypes.has(event.event)) {
+ return [];
+ }
+ const baseEvent = {
+ event: event.event
+ };
+ if ("id" in event) {
+ baseEvent.id = event.id;
+ }
+ if ("actor" in event && event.actor) {
+ baseEvent.actor = event.actor.login;
+ } else if ("user" in event && event.user) {
+ baseEvent.actor = event.user.login;
+ }
+ if ("created_at" in event) {
+ baseEvent.created_at = event.created_at;
+ }
+ if (event.event === "cross_referenced") {
+ if ("source" in event && event.source) {
+ const source = event.source;
+ baseEvent.source = {
+ type: source.type,
+ issue: source.issue ? {
+ number: source.issue.number,
+ title: source.issue.title,
+ html_url: source.issue.html_url
+ } : null,
+ pull_request: source.pull_request ? {
+ number: source.pull_request.number,
+ title: source.pull_request.title,
+ html_url: source.pull_request.html_url
+ } : null
+ };
+ }
+ }
+ if (event.event === "referenced") {
+ if ("commit_id" in event) {
+ baseEvent.commit_id = event.commit_id;
+ }
+ if ("commit_url" in event) {
+ baseEvent.commit_url = event.commit_url;
+ }
+ }
+ return [baseEvent];
+ });
+ return {
+ issue_number,
+ events: parsedEvents,
+ count: parsedEvents.length
+ };
+ })
+});
+
+// mcp/issueInfo.ts
+var IssueInfo = type({
+ issue_number: type.number.describe("The issue number to fetch")
+});
+var IssueInfoTool = tool({
+ name: "get_issue",
+ description: "Retrieve GitHub issue information by issue number",
+ parameters: IssueInfo,
+ execute: contextualize(async ({ issue_number }, ctx) => {
+ const issue2 = await ctx.octokit.rest.issues.get({
+ owner: ctx.owner,
+ repo: ctx.name,
+ issue_number
+ });
+ const data = issue2.data;
+ const hints = [];
+ if (data.comments > 0) {
+ hints.push("use get_issue_comments to retrieve all comments for this issue");
+ }
+ hints.push(
+ "use get_issue_events to retrieve cross-references and commit references (relationships not reflected in current state)"
+ );
+ return {
+ number: data.number,
+ url: data.html_url,
+ title: data.title,
+ body: data.body,
+ state: data.state,
+ locked: data.locked,
+ labels: data.labels?.map((label) => typeof label === "string" ? label : label.name),
+ assignees: data.assignees?.map((assignee) => assignee.login),
+ user: data.user?.login,
+ created_at: data.created_at,
+ updated_at: data.updated_at,
+ closed_at: data.closed_at,
+ comments: data.comments,
+ milestone: data.milestone?.title,
+ pull_request: data.pull_request ? {
+ url: data.pull_request.url,
+ html_url: data.pull_request.html_url,
+ diff_url: data.pull_request.diff_url,
+ patch_url: data.pull_request.patch_url
+ } : null,
+ hints
+ };
+ })
+});
+
// mcp/pr.ts
var PullRequest = type({
title: type.string.describe("the title of the pull request"),
@@ -122222,6 +122321,9 @@ async function startMcpHttpServer() {
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
+ IssueInfoTool,
+ GetIssueCommentsTool,
+ GetIssueEventsTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
@@ -122374,21 +122476,16 @@ var Inputs = type({
...keyInputDefs
});
async function main(inputs) {
- let pollInterval = null;
let mcpServerClose;
- let githubInstallationToken;
try {
const payload = parsePayload(inputs);
const partialCtx = await initializeContext(inputs, payload);
const ctx = partialCtx;
- githubInstallationToken = ctx.githubInstallationToken;
setupGitAuth({
githubInstallationToken: ctx.githubInstallationToken,
repoContext: ctx.repoContext
});
await setupTempDirectory(ctx);
- setupMcpLogPolling(ctx);
- pollInterval = ctx.pollInterval;
setupGitBranch(ctx.payload);
await startMcpServer(ctx);
mcpServerClose = ctx.mcpServerClose;
@@ -122406,15 +122503,10 @@ async function main(inputs) {
error: errorMessage
};
} finally {
- if (pollInterval) {
- clearInterval(pollInterval);
- }
if (mcpServerClose) {
await mcpServerClose();
}
- if (githubInstallationToken) {
- await revokeInstallationToken(githubInstallationToken);
- }
+ await revokeGitHubInstallationToken();
}
}
function getAvailableAgents(inputs) {
@@ -122462,30 +122554,28 @@ async function initializeContext(inputs, payload) {
log.info(`\u{1F438} Running pullfrog/action@${package_default.version}...`);
Inputs.assert(inputs);
setupGitConfig();
- const githubInstallationToken = await setupGitHubInstallationToken();
+ const githubInstallationToken2 = await setupGitHubInstallationToken();
const repoContext = parseRepoContext();
const { agentName, agent: agent2 } = await resolveAgent(
inputs,
payload,
- githubInstallationToken,
+ githubInstallationToken2,
repoContext
);
const resolvedPayload = { ...payload, agent: agentName };
return {
inputs,
- githubInstallationToken,
+ githubInstallationToken: githubInstallationToken2,
repoContext,
agentName,
agent: agent2,
payload: resolvedPayload,
- sharedTempDir: "",
- mcpLogPath: "",
- pollInterval: null
+ sharedTempDir: ""
};
}
-async function resolveAgent(inputs, payload, githubInstallationToken, repoContext) {
+async function resolveAgent(inputs, payload, githubInstallationToken2, repoContext) {
const repoSettings = await fetchRepoSettings({
- token: githubInstallationToken,
+ token: githubInstallationToken2,
repoContext
});
const agentOverride = process.env.AGENT_OVERRIDE;
@@ -122515,25 +122605,10 @@ async function resolveAgent(inputs, payload, githubInstallationToken, repoContex
return { agentName, agent: agent2 };
}
async function setupTempDirectory(ctx) {
- ctx.sharedTempDir = await mkdtemp2(join8(tmpdir2(), "pullfrog-"));
+ ctx.sharedTempDir = await mkdtemp2(join7(tmpdir2(), "pullfrog-"));
process.env.PULLFROG_TEMP_DIR = ctx.sharedTempDir;
- ctx.mcpLogPath = join8(ctx.sharedTempDir, "mcpLog.txt");
- await writeFile(ctx.mcpLogPath, "", "utf-8");
log.info(`\u{1F4C2} PULLFROG_TEMP_DIR has been created at ${ctx.sharedTempDir}`);
}
-function setupMcpLogPolling(ctx) {
- let lastSize = 0;
- ctx.pollInterval = setInterval(() => {
- if (existsSync4(ctx.mcpLogPath)) {
- const content = readFileSync2(ctx.mcpLogPath, "utf-8");
- if (content.length > lastSize) {
- const newContent = content.slice(lastSize);
- process.stdout.write(newContent);
- lastSize = content.length;
- }
- }
- }, 100);
-}
function parsePayload(inputs) {
try {
const parsedPrompt = JSON.parse(inputs.prompt);
@@ -122557,7 +122632,6 @@ async function startMcpServer(ctx) {
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
const allModes = [...modes, ...ctx.payload.modes || []];
- process.env.GITHUB_INSTALLATION_TOKEN = ctx.githubInstallationToken;
process.env.GITHUB_REPOSITORY = githubRepository;
process.env.PULLFROG_MODES = JSON.stringify(allModes);
process.env.PULLFROG_PAYLOAD = JSON.stringify(ctx.payload);
@@ -122594,7 +122668,6 @@ ${encode(eventWithoutContext)}`;
return ctx.agent.run({
payload: ctx.payload,
mcpServers: ctx.mcpServers,
- githubInstallationToken: ctx.githubInstallationToken,
apiKey: ctx.apiKey,
cliPath: ctx.cliPath
});
From 91f8b551670b889411cf402e85938166ec9856f6 Mon Sep 17 00:00:00 2001
From: Colin McDonnell
Date: Wed, 26 Nov 2025 23:25:36 -0800
Subject: [PATCH 17/17] add Address Reviews mode
---
mcp/README.md | 25 +++++++++++++++++++++++++
mcp/comment.ts | 32 ++++++++++++++++++++++++++++++++
mcp/server.ts | 2 ++
modes.ts | 26 ++++++++++++++++++++++++++
4 files changed, 85 insertions(+)
diff --git a/mcp/README.md b/mcp/README.md
index 0d9eefd..af7ceb9 100644
--- a/mcp/README.md
+++ b/mcp/README.md
@@ -74,6 +74,31 @@ await mcp.call("gh_pullfrog/list_pull_request_reviews", {
});
```
+#### `reply_to_review_comment`
+reply to a PR review comment thread explaining how the feedback was addressed.
+
+**parameters:**
+- `pull_number` (number): the pull request number
+- `comment_id` (number): the ID of the review comment to reply to
+- `body` (string): the reply text explaining how the feedback was addressed
+
+**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies`
+
+**returns:**
+the created reply comment including:
+- comment id, body, html_url
+- in_reply_to_id showing it's a reply to the specified comment
+
+**example:**
+```typescript
+// after addressing a review comment
+await mcp.call("gh_pullfrog/reply_to_review_comment", {
+ pull_number: 47,
+ comment_id: 2567334961,
+ body: "removed the function as requested"
+});
+```
+
### other tools
see individual files for documentation on other tools:
diff --git a/mcp/comment.ts b/mcp/comment.ts
index 70d8618..50db8c0 100644
--- a/mcp/comment.ts
+++ b/mcp/comment.ts
@@ -168,3 +168,35 @@ export const UpdateWorkingCommentTool = tool({
};
}),
});
+
+export const ReplyToReviewComment = type({
+ pull_number: type.number.describe("the pull request number"),
+ comment_id: type.number.describe("the ID of the review comment to reply to"),
+ body: type.string.describe("the reply text explaining how the feedback was addressed"),
+});
+
+export const ReplyToReviewCommentTool = tool({
+ name: "reply_to_review_comment",
+ description:
+ "Reply to a PR review comment thread explaining how the feedback was addressed. Use this after addressing each review comment to provide specific context about the changes made.",
+ parameters: ReplyToReviewComment,
+ execute: contextualize(async ({ pull_number, comment_id, body }, ctx) => {
+ const bodyWithFooter = addFooter(body, ctx.payload);
+
+ const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
+ owner: ctx.owner,
+ repo: ctx.name,
+ pull_number,
+ comment_id,
+ body: bodyWithFooter,
+ });
+
+ return {
+ success: true,
+ commentId: result.data.id,
+ url: result.data.html_url,
+ body: result.data.body,
+ in_reply_to_id: result.data.in_reply_to_id,
+ };
+ }),
+});
diff --git a/mcp/server.ts b/mcp/server.ts
index 7fad9c0..93843dd 100644
--- a/mcp/server.ts
+++ b/mcp/server.ts
@@ -8,6 +8,7 @@ import {
CreateCommentTool,
CreateWorkingCommentTool,
EditCommentTool,
+ ReplyToReviewCommentTool,
UpdateWorkingCommentTool,
} from "./comment.ts";
import { DebugShellCommandTool } from "./debug.ts";
@@ -66,6 +67,7 @@ export async function startMcpHttpServer(): Promise<{ url: string; close: () =>
EditCommentTool,
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
+ ReplyToReviewCommentTool,
IssueTool,
IssueInfoTool,
GetIssueCommentsTool,
diff --git a/modes.ts b/modes.ts
index d2bb1b7..e0661ea 100644
--- a/modes.ts
+++ b/modes.ts
@@ -33,6 +33,32 @@ export const modes: Mode[] = [
9. When you are done, create a final commit. If relevant, indicate which issue the PR addresses somewhere in the commit message (e.g. "Fixes #123"). Create a PR with an informative title and body. If relevant, include links to the issue or comment that triggered the PR.
10. Update the Working Comment one final time with a summary of the results. Include links to any created issues/PRs, e.g. \`[View PR](https://github.com/org/repo/pull/123)\`
+`,
+ },
+ {
+ name: "Address Reviews",
+ description:
+ "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
+ prompt: `Follow these steps:
+1. ${initialCommentInstruction}
+
+2. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically fetches and checks out the PR branch)
+
+3. Review the feedback provided. Understand each review comment and what changes are being requested.
+
+4. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists.
+
+5. Make the necessary code changes to address the feedback. Work through each review comment systematically.
+
+6. After addressing each review comment, use ${ghPullfrogMcpName}/reply_to_review_comment to reply directly to that comment thread explaining what change was made (keep replies concise, 1-2 sentences).
+
+7. Test your changes to ensure they work correctly.
+
+8. Update your working comment using ${ghPullfrogMcpName}/update_working_comment to share overall progress.
+
+9. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one.
+
+10. Update the Working Comment one final time with a summary of all changes made.
`,
},
{