Compare commits

...

8 Commits

Author SHA1 Message Date
David Blass b30cc166e3 bump version 2025-11-20 14:52:55 -05:00
David Blass cbcf87f50d fix mcp name 2025-11-20 14:52:42 -05:00
David Blass f596d6d995 fix huge mistake 2025-11-20 14:09:14 -05:00
David Blass 917b8804c0 improve agents external integration 2025-11-20 13:54:29 -05:00
Colin McDonnell 9c51c450bc Update builds 2025-11-20 00:35:16 -08:00
Colin McDonnell 85f8fbfaf5 Add additional tools 2025-11-20 00:34:03 -08:00
Colin McDonnell 098df15764 Add get_check_suite_logs tools 2025-11-19 23:27:24 -08:00
Colin McDonnell fe35e9e274 Updates 2025-11-19 21:25:51 -08:00
22 changed files with 553 additions and 58 deletions
+1 -1
View File
@@ -6,7 +6,6 @@ import { agent, installFromNpmTarball } from "./shared.ts";
export const claude = agent({
name: "claude",
inputKeys: ["anthropic_api_key"],
install: async () => {
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
return await installFromNpmTarball({
@@ -19,6 +18,7 @@ export const claude = agent({
process.env.ANTHROPIC_API_KEY = apiKey;
const prompt = addInstructions(payload);
console.log(prompt);
const queryInstance = query({
prompt,
-1
View File
@@ -6,7 +6,6 @@ import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./
export const codex = agent({
name: "codex",
inputKeys: ["openai_api_key"],
install: async () => {
return await installFromNpmTarball({
packageName: "@openai/codex",
-1
View File
@@ -7,7 +7,6 @@ import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared
export const cursor = agent({
name: "cursor",
inputKeys: ["cursor_api_key"],
install: async () => {
return await installFromCurl({
installUrl: "https://cursor.com/install",
-1
View File
@@ -6,7 +6,6 @@ import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./
export const gemini = agent({
name: "gemini",
inputKeys: ["google_api_key", "gemini_api_key"],
install: async () => {
return await installFromNpmTarball({
packageName: "@google/gemini-cli",
-2
View File
@@ -11,5 +11,3 @@ export const agents = {
cursor,
gemini,
} satisfies Record<AgentName, Agent>;
export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number];
+1 -1
View File
@@ -62,4 +62,4 @@ ${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`)
${payload.prompt}
${JSON.stringify(payload.event, null, 2)}`;
${typeof payload.event === "string" ? payload.event : JSON.stringify(payload.event, null, 2)}`;
+13 -7
View File
@@ -3,7 +3,8 @@ import { chmodSync, createWriteStream, existsSync } from "node:fs";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { Payload } from "../external.ts";
import type { show } from "@ark/util";
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
import { log } from "../utils/cli.ts";
/**
@@ -232,13 +233,18 @@ export async function installFromCurl({
return cliPath;
}
export const agent = <const agent extends Agent>(agent: agent): agent => {
return agent;
export const agent = <const input extends AgentInput>(input: input): defineAgent<input> => {
return { ...input, ...agentsManifest[input.name] } as never;
};
export type Agent = {
name: string;
inputKeys: string[];
export interface AgentInput {
name: AgentName;
install: () => Promise<string>;
run: (config: AgentConfig) => Promise<AgentResult>;
};
}
export interface Agent extends AgentInput, AgentManifest {}
type agentManifest<name extends AgentName> = (typeof agentsManifest)[name];
type defineAgent<input extends AgentInput> = show<input & agentManifest<input["name"]>>;
+31 -17
View File
@@ -38572,7 +38572,7 @@ function query({
// package.json
var package_default = {
name: "@pullfrog/action",
version: "0.0.110",
version: "0.0.111",
type: "module",
files: [
"index.js",
@@ -38956,10 +38956,28 @@ function formatToolCall({
}
// external.ts
var ghPullfrogMcpName = "gh-pullfrog";
var ghPullfrogMcpName = "gh_pullfrog";
var agentsManifest = {
claude: {
displayName: "Claude Code",
apiKeyNames: ["anthropic_api_key"]
},
codex: {
displayName: "Codex CLI",
apiKeyNames: ["openai_api_key"]
},
cursor: {
displayName: "Cursor CLI",
apiKeyNames: ["cursor_api_key"]
},
gemini: {
displayName: "Gemini CLI",
apiKeyNames: ["google_api_key", "gemini_api_key"]
}
};
// modes.ts
var initialCommentInstruction = `Use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment that contains a SENTENCE FRAGMENT that casually describes the actions you're about to perform. It must be of the form "Starting work on this issue...". You MUST use an -ing verb!`;
var initialCommentInstruction = `Use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment with a conversational description of what work you are about to perform.`;
var modes = [
{
name: "Plan",
@@ -38979,7 +38997,6 @@ var modes = [
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:
1. ${initialCommentInstruction}
2. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
3. Understand the requirements and any existing plan
4. Make the necessary code changes
@@ -39073,7 +39090,7 @@ ${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`)
${payload.prompt}
${JSON.stringify(payload.event, null, 2)}`;
${typeof payload.event === "string" ? payload.event : JSON.stringify(payload.event, null, 2)}`;
// agents/shared.ts
import { spawnSync } from "node:child_process";
@@ -39201,14 +39218,13 @@ async function installFromCurl({
log.info(`\u2713 ${executableName} installed at ${cliPath}`);
return cliPath;
}
var agent = (agent2) => {
return agent2;
var agent = (input) => {
return { ...input, ...agentsManifest[input.name] };
};
// agents/claude.ts
var claude = agent({
name: "claude",
inputKeys: ["anthropic_api_key"],
install: async () => {
const versionRange = package_default.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
return await installFromNpmTarball({
@@ -39220,6 +39236,7 @@ var claude = agent({
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
process.env.ANTHROPIC_API_KEY = apiKey;
const prompt = addInstructions(payload);
console.log(prompt);
const queryInstance = query({
prompt,
options: {
@@ -39660,7 +39677,6 @@ var Codex = class {
// agents/codex.ts
var codex = agent({
name: "codex",
inputKeys: ["openai_api_key"],
install: async () => {
return await installFromNpmTarball({
packageName: "@openai/codex",
@@ -39809,7 +39825,6 @@ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:f
import { join as join6 } from "node:path";
var cursor = agent({
name: "cursor",
inputKeys: ["cursor_api_key"],
install: async () => {
return await installFromCurl({
installUrl: "https://cursor.com/install",
@@ -39988,7 +40003,6 @@ async function spawn4(options) {
// agents/gemini.ts
var gemini = agent({
name: "gemini",
inputKeys: ["google_api_key", "gemini_api_key"],
install: async () => {
return await installFromNpmTarball({
packageName: "@google/gemini-cli",
@@ -48128,11 +48142,11 @@ function setupGitAuth(githubToken, repoContext) {
// main.ts
var AgentName = type.enumerated(...Object.values(agents).map((agent2) => agent2.name));
var AgentInputKey = type.enumerated(
...Object.values(agents).flatMap((agent2) => agent2.inputKeys)
...Object.values(agents).flatMap((agent2) => agent2.apiKeyNames)
);
var keyInputDefs = flatMorph(
agents,
(_, agent2) => agent2.inputKeys.map((inputKey) => [inputKey, "string | undefined?"])
(_, agent2) => agent2.apiKeyNames.map((inputKey) => [inputKey, "string | undefined?"])
);
var Inputs = type({
prompt: "string",
@@ -48178,7 +48192,7 @@ function throwMissingApiKeyError({
const githubRepoUrl = `https://github.com/${repoContext.owner}/${repoContext.name}`;
const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
const availableAgents = Object.values(agents).filter(
(agent2) => agent2.inputKeys.some((inputKey) => inputs[inputKey])
(agent2) => agent2.apiKeyNames.some((inputKey) => inputs[inputKey])
);
let message = `Pullfrog is configured to use ${agentName}, but the associated API key was not provided.
@@ -48271,13 +48285,13 @@ async function installAgentCli(ctx) {
ctx.cliPath = await ctx.agent.install();
}
function validateApiKey(ctx) {
const matchingInputKey = ctx.agent.inputKeys.find(
const matchingInputKey = ctx.agent.apiKeyNames.find(
(inputKey) => ctx.inputs[inputKey]
);
if (!matchingInputKey) {
throwMissingApiKeyError({
agentName: ctx.agentName,
inputKeys: ctx.agent.inputKeys,
inputKeys: ctx.agent.apiKeyNames,
repoContext: ctx.repoContext,
inputs: ctx.inputs
});
@@ -48332,7 +48346,7 @@ async function run() {
agent: core3.getInput("agent") ? AgentName.assert(core3.getInput("agent")) : void 0,
...flatMorph(
agents,
(_, agent2) => agent2.inputKeys.map((inputKey) => [inputKey, core3.getInput(inputKey)])
(_, agent2) => agent2.apiKeyNames.map((inputKey) => [inputKey, core3.getInput(inputKey)])
)
};
const result = await main(inputs);
+1 -1
View File
@@ -24,7 +24,7 @@ async function run(): Promise<void> {
prompt: core.getInput("prompt", { required: true }),
agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined,
...flatMorph(agents, (_, agent) =>
agent.inputKeys.map((inputKey) => [inputKey, core.getInput(inputKey)])
agent.apiKeyNames.map((inputKey) => [inputKey, core.getInput(inputKey)])
),
};
+19 -11
View File
@@ -7,31 +7,38 @@
import type { Mode } from "./modes.ts";
// mcp name constant
export const ghPullfrogMcpName = "gh-pullfrog";
export const ghPullfrogMcpName = "gh_pullfrog";
export interface AgentManifest {
displayName: string;
apiKeyNames: string[];
}
// agent manifest - static metadata about available agents
export const agentsManifest = {
claude: {
name: "claude",
apiKeys: ["anthropic_api_key"],
displayName: "Claude Code",
apiKeyNames: ["anthropic_api_key"],
},
codex: {
name: "codex",
apiKeys: ["openai_api_key"],
displayName: "Codex CLI",
apiKeyNames: ["openai_api_key"],
},
cursor: {
name: "cursor",
apiKeys: ["cursor_api_key"],
displayName: "Cursor CLI",
apiKeyNames: ["cursor_api_key"],
},
gemini: {
name: "gemini",
apiKeys: ["google_api_key", "gemini_api_key"],
displayName: "Gemini CLI",
apiKeyNames: ["google_api_key", "gemini_api_key"],
},
} as const;
} as const satisfies Record<string, AgentManifest>;
// agent name type - union of agent slugs
export type AgentName = keyof typeof agentsManifest;
export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number];
// payload type for agent execution
export type Payload = {
"~pullfrog": true;
@@ -48,8 +55,9 @@ export type Payload = {
/**
* Event data from webhook payload.
* Can be an object (will be JSON.stringify'd) or a string (used as-is).
*/
readonly event: object;
readonly event: object | string;
/**
* Execution mode configuration
+5 -5
View File
@@ -24,12 +24,12 @@ import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
export const AgentName = type.enumerated(...Object.values(agents).map((agent) => agent.name));
export const AgentInputKey = type.enumerated(
...Object.values(agents).flatMap((agent) => agent.inputKeys)
...Object.values(agents).flatMap((agent) => agent.apiKeyNames)
);
export type AgentInputKey = typeof AgentInputKey.infer;
const keyInputDefs = flatMorph(agents, (_, agent) =>
agent.inputKeys.map((inputKey) => [inputKey, "string | undefined?"] as const)
agent.apiKeyNames.map((inputKey) => [inputKey, "string | undefined?"] as const)
);
export const Inputs = type({
@@ -102,7 +102,7 @@ function throwMissingApiKeyError({
// Find which agents have inputKeys that match the provided inputs
const availableAgents = Object.values(agents).filter((agent) =>
agent.inputKeys.some((inputKey) => inputs[inputKey])
agent.apiKeyNames.some((inputKey) => inputs[inputKey])
);
let message = `Pullfrog is configured to use ${agentName}, but the associated API key was not provided.
@@ -230,13 +230,13 @@ async function installAgentCli(ctx: MainContext): Promise<void> {
}
function validateApiKey(ctx: MainContext): void {
const matchingInputKey = ctx.agent.inputKeys.find(
const matchingInputKey = ctx.agent.apiKeyNames.find(
(inputKey: string) => ctx.inputs[inputKey as AgentInputKey]
);
if (!matchingInputKey) {
throwMissingApiKeyError({
agentName: ctx.agentName,
inputKeys: ctx.agent.inputKeys,
inputKeys: ctx.agent.apiKeyNames,
repoContext: ctx.repoContext,
inputs: ctx.inputs,
});
+159 -3
View File
@@ -95049,6 +95049,9 @@ var FastMCP = class extends FastMCPEventEmitter {
}
};
// external.ts
var ghPullfrogMcpName = "gh_pullfrog";
// node_modules/.pnpm/@ark+util@0.53.0/node_modules/@ark/util/out/arrays.js
var liftArray = (data) => Array.isArray(data) ? data : [data];
var spliterate = (arr, predicate) => {
@@ -107231,6 +107234,87 @@ var handleToolError = (error41) => {
};
};
// mcp/checkSuite.ts
var GetCheckSuiteLogs = type({
check_suite_id: type.number.describe("the id from check_suite.id")
});
var GetCheckSuiteLogsTool = tool({
name: "get_check_suite_logs",
description: "get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
parameters: GetCheckSuiteLogs,
execute: contextualize(async ({ check_suite_id }, ctx) => {
const workflowRuns = await ctx.octokit.paginate(
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
{
owner: ctx.owner,
repo: ctx.name,
check_suite_id,
per_page: 100
}
);
const failedRuns = workflowRuns.filter((run) => run.conclusion === "failure");
if (failedRuns.length === 0) {
return {
check_suite_id,
message: "no failed workflow runs found for this check suite",
workflow_runs: []
};
}
const logsForRuns = await Promise.all(
failedRuns.map(async (run) => {
const jobs = await ctx.octokit.paginate(ctx.octokit.rest.actions.listJobsForWorkflowRun, {
owner: ctx.owner,
repo: ctx.name,
run_id: run.id
});
const jobLogs = await Promise.all(
jobs.map(async (job) => {
try {
const logsResponse = await ctx.octokit.rest.actions.downloadJobLogsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
job_id: job.id
});
const logsUrl = logsResponse.url;
const logsText = await fetch(logsUrl).then((r) => r.text());
return {
job_id: job.id,
job_name: job.name,
status: job.status,
conclusion: job.conclusion,
started_at: job.started_at,
completed_at: job.completed_at,
logs: logsText
};
} catch (error41) {
return {
job_id: job.id,
job_name: job.name,
status: job.status,
conclusion: job.conclusion,
started_at: job.started_at,
completed_at: job.completed_at,
error: `failed to fetch logs: ${error41}`
};
}
})
);
return {
workflow_run_id: run.id,
workflow_name: run.name,
html_url: run.html_url,
conclusion: run.conclusion,
jobs: jobLogs
};
})
);
return {
check_suite_id,
workflow_runs: logsForRuns
};
})
});
// mcp/comment.ts
var Comment = type({
issueNumber: type.number.describe("the issue number to comment on"),
@@ -107430,7 +107514,7 @@ var PullRequestInfoTool = tool({
log.info(`Fetching PR branch: origin/${headBranch}`);
execSync2(`git fetch origin ${headBranch}`, { stdio: "inherit" });
log.info(`Checking out PR branch: origin/${headBranch}`);
execSync2(`git checkout origin/${headBranch}`, { stdio: "inherit" });
execSync2(`git checkout -B ${headBranch} origin/${headBranch}`, { stdio: "inherit" });
return {
number: data.number,
url: data.html_url,
@@ -107512,6 +107596,74 @@ var ReviewTool = tool({
})
});
// mcp/reviewComments.ts
var GetReviewComments = type({
pull_number: type.number.describe("The pull request number"),
review_id: type.number.describe("The review ID to get comments for")
});
var GetReviewCommentsTool = tool({
name: "get_review_comments",
description: "Get all review comments for a specific pull request review. Returns line-by-line comments that were left on specific code locations.",
parameters: GetReviewComments,
execute: contextualize(async ({ pull_number, review_id }, ctx) => {
const comments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listCommentsForReview, {
owner: ctx.owner,
repo: ctx.name,
pull_number,
review_id
});
return {
review_id,
pull_number,
comments: comments.map((comment) => ({
id: comment.id,
body: comment.body,
path: comment.path,
line: comment.line,
side: comment.side,
start_line: comment.start_line,
start_side: comment.start_side,
user: typeof comment.user === "string" ? comment.user : comment.user?.login,
created_at: comment.created_at,
updated_at: comment.updated_at,
html_url: comment.html_url,
in_reply_to_id: comment.in_reply_to_id,
diff_hunk: comment.diff_hunk,
reactions: comment.reactions
})),
count: comments.length
};
})
});
var ListPullRequestReviews = type({
pull_number: type.number.describe("The pull request number to list reviews for")
});
var ListPullRequestReviewsTool = tool({
name: "list_pull_request_reviews",
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
parameters: ListPullRequestReviews,
execute: contextualize(async ({ pull_number }, ctx) => {
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
owner: ctx.owner,
repo: ctx.name,
pull_number
});
return {
pull_number,
reviews: reviews.map((review) => ({
id: review.id,
body: review.body,
state: review.state,
user: review.user?.login,
commit_id: review.commit_id,
submitted_at: review.submitted_at,
html_url: review.html_url
})),
count: reviews.length
};
})
});
// mcp/selectMode.ts
function getModes() {
const modesJson = process.env.PULLFROG_MODES;
@@ -107558,7 +107710,7 @@ var SelectModeTool = tool({
// mcp/server.ts
var server = new FastMCP({
name: "gh-pullfrog",
name: ghPullfrogMcpName,
version: "0.0.1"
});
addTools(server, [
@@ -107568,9 +107720,13 @@ addTools(server, [
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
// ListFilesTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool
PullRequestInfoTool,
GetReviewCommentsTool,
ListPullRequestReviewsTool,
GetCheckSuiteLogsTool
]);
server.start();
/*! Bundled license information:
+92
View File
@@ -0,0 +1,92 @@
# gh-pullfrog MCP Tools
this directory contains the mcp (model context protocol) server tools for interacting with github.
## available tools
### check suite tools
#### `get_check_suite_logs`
get workflow run logs for a failed check suite.
**parameters:**
- `check_suite_id` (number): the id from check_suite.id in the webhook payload
**replaces:** `gh run list` and `gh run view --log`
**returns:**
all logs from all failed workflow runs in the check suite, including:
- workflow run details (id, name, html_url, conclusion)
- job details for each workflow run (id, name, status, conclusion, logs)
**example:**
```typescript
// when handling a check_suite_completed webhook
await mcp.call("gh-pullfrog/get_check_suite_logs", {
check_suite_id: check_suite.id
});
```
### review tools
#### `get_review_comments`
get all line-by-line comments for a specific pull request review.
**parameters:**
- `pull_number` (number): the pull request number
- `review_id` (number): the id from review.id in the webhook payload
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments`
**returns:**
array of review comments including:
- file path, line number, comment body
- side (LEFT/RIGHT) and position in diff
- user, timestamps, html_url
- in_reply_to_id for threaded comments
**example:**
```typescript
// when handling a pull_request_review_submitted webhook
await mcp.call("gh-pullfrog/get_review_comments", {
pull_number: 47,
review_id: review.id
});
```
#### `list_pull_request_reviews`
list all reviews for a pull request.
**parameters:**
- `pull_number` (number): the pull request number
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews`
**returns:**
array of reviews with:
- review id, body, state (approved/changes_requested/commented)
- user, commit_id, submitted_at, html_url
**example:**
```typescript
await mcp.call("gh-pullfrog/list_pull_request_reviews", {
pull_number: 47
});
```
### other tools
see individual files for documentation on other tools:
- `comment.ts` - create, edit, and update comments
- `issue.ts` - create issues
- `pr.ts` - create pull requests
- `prInfo.ts` - get pull request information
- `review.ts` - create pull request reviews
- `selectMode.ts` - select execution mode
## usage in agents
agents should never use the `gh` cli. instead, they should use the mcp tools provided by this server.
the agent instructions automatically include guidance on using these tools.
+94
View File
@@ -0,0 +1,94 @@
import { type } from "arktype";
import { contextualize, tool } from "./shared.ts";
export const GetCheckSuiteLogs = type({
check_suite_id: type.number.describe("the id from check_suite.id"),
});
export const GetCheckSuiteLogsTool = tool({
name: "get_check_suite_logs",
description:
"get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
parameters: GetCheckSuiteLogs,
execute: contextualize(async ({ check_suite_id }, ctx) => {
// get workflow runs for this specific check suite
const workflowRuns = await ctx.octokit.paginate(
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
{
owner: ctx.owner,
repo: ctx.name,
check_suite_id,
per_page: 100,
}
);
const failedRuns = workflowRuns.filter((run) => run.conclusion === "failure");
if (failedRuns.length === 0) {
return {
check_suite_id,
message: "no failed workflow runs found for this check suite",
workflow_runs: [],
};
}
// get logs for each failed run
const logsForRuns = await Promise.all(
failedRuns.map(async (run) => {
const jobs = await ctx.octokit.paginate(ctx.octokit.rest.actions.listJobsForWorkflowRun, {
owner: ctx.owner,
repo: ctx.name,
run_id: run.id,
});
const jobLogs = await Promise.all(
jobs.map(async (job) => {
try {
const logsResponse = await ctx.octokit.rest.actions.downloadJobLogsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
job_id: job.id,
});
const logsUrl = logsResponse.url;
const logsText = await fetch(logsUrl).then((r) => r.text());
return {
job_id: job.id,
job_name: job.name,
status: job.status,
conclusion: job.conclusion,
started_at: job.started_at,
completed_at: job.completed_at,
logs: logsText,
};
} catch (error) {
return {
job_id: job.id,
job_name: job.name,
status: job.status,
conclusion: job.conclusion,
started_at: job.started_at,
completed_at: job.completed_at,
error: `failed to fetch logs: ${error}`,
};
}
})
);
return {
workflow_run_id: run.id,
workflow_name: run.name,
html_url: run.html_url,
conclusion: run.conclusion,
jobs: jobLogs,
};
})
);
return {
check_suite_id,
workflow_runs: logsForRuns,
};
}),
});
+43
View File
@@ -0,0 +1,43 @@
import { execSync } from "node:child_process";
import { type } from "arktype";
import { contextualize, tool } from "./shared.ts";
export const ListFiles = type({
path: type.string
.describe("The path to list files from (defaults to current directory)")
.default("."),
});
export const ListFilesTool = tool({
name: "list_files",
description:
"List files in the repository using git ls-files. Useful for discovering the file structure and locating files.",
parameters: ListFiles,
execute: contextualize(async ({ path }) => {
try {
// Use git ls-files to list tracked files
// This respects .gitignore and gives a clean list of source files
const command = path === "." ? "git ls-files" : `git ls-files ${path}`;
const output = execSync(command, { encoding: "utf-8" });
const files = output.split("\n").filter((f) => f.trim() !== "");
if (files.length === 0) {
// Fallback for non-git environments or untracked files
const findCmd = `find ${path} -maxdepth 3 -not -path '*/.*' -type f`;
const findOutput = execSync(findCmd, { encoding: "utf-8" });
return {
files: findOutput.split("\n").filter((f) => f.trim() !== ""),
method: "find",
};
}
return { files, method: "git" };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
error: `Failed to list files: ${errorMessage}`,
hint: "Try using a specific path if the repository root is not the current directory.",
};
}
}),
});
+2 -1
View File
@@ -36,7 +36,8 @@ export const PullRequestInfoTool = tool({
execSync(`git fetch origin ${headBranch}`, { stdio: "inherit" });
log.info(`Checking out PR branch: origin/${headBranch}`);
execSync(`git checkout origin/${headBranch}`, { stdio: "inherit" });
// check out a local branch tracking the remote branch so we can push changes
execSync(`git checkout -B ${headBranch} origin/${headBranch}`, { stdio: "inherit" });
return {
number: data.number,
+76
View File
@@ -0,0 +1,76 @@
import { type } from "arktype";
import { contextualize, tool } from "./shared.ts";
export const GetReviewComments = type({
pull_number: type.number.describe("The pull request number"),
review_id: type.number.describe("The review ID to get comments for"),
});
export const GetReviewCommentsTool = tool({
name: "get_review_comments",
description:
"Get all review comments for a specific pull request review. Returns line-by-line comments that were left on specific code locations.",
parameters: GetReviewComments,
execute: contextualize(async ({ pull_number, review_id }, ctx) => {
const comments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listCommentsForReview, {
owner: ctx.owner,
repo: ctx.name,
pull_number,
review_id,
});
return {
review_id,
pull_number,
comments: comments.map((comment) => ({
id: comment.id,
body: comment.body,
path: comment.path,
line: comment.line,
side: comment.side,
start_line: comment.start_line,
start_side: comment.start_side,
user: typeof comment.user === "string" ? comment.user : comment.user?.login,
created_at: comment.created_at,
updated_at: comment.updated_at,
html_url: comment.html_url,
in_reply_to_id: comment.in_reply_to_id,
diff_hunk: comment.diff_hunk,
reactions: comment.reactions,
})),
count: comments.length,
};
}),
});
export const ListPullRequestReviews = type({
pull_number: type.number.describe("The pull request number to list reviews for"),
});
export const ListPullRequestReviewsTool = tool({
name: "list_pull_request_reviews",
description:
"List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
parameters: ListPullRequestReviews,
execute: contextualize(async ({ pull_number }, ctx) => {
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
owner: ctx.owner,
repo: ctx.name,
pull_number,
});
return {
pull_number,
reviews: reviews.map((review) => ({
id: review.id,
body: review.body,
state: review.state,
user: review.user?.login,
commit_id: review.commit_id,
submitted_at: review.submitted_at,
html_url: review.html_url,
})),
count: reviews.length,
};
}),
});
+8 -1
View File
@@ -1,4 +1,6 @@
import { FastMCP } from "fastmcp";
import { ghPullfrogMcpName } from "../external.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
import {
CreateCommentTool,
CreateWorkingCommentTool,
@@ -9,11 +11,12 @@ import { IssueTool } from "./issue.ts";
import { PullRequestTool } from "./pr.ts";
import { PullRequestInfoTool } from "./prInfo.ts";
import { ReviewTool } from "./review.ts";
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
import { SelectModeTool } from "./selectMode.ts";
import { addTools } from "./shared.ts";
const server = new FastMCP({
name: "gh-pullfrog",
name: ghPullfrogMcpName,
version: "0.0.1",
});
@@ -24,9 +27,13 @@ addTools(server, [
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
// ListFilesTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
GetReviewCommentsTool,
ListPullRequestReviewsTool,
GetCheckSuiteLogsTool,
]);
server.start();
+1 -2
View File
@@ -6,7 +6,7 @@ export interface Mode {
prompt: string;
}
const initialCommentInstruction = `Use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment that contains a SENTENCE FRAGMENT that casually describes the actions you're about to perform. It must be of the form "Starting work on this issue...". You MUST use an -ing verb!`;
const initialCommentInstruction = `Use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment with a conversational description of what work you are about to perform.`;
export const modes: Mode[] = [
{
@@ -29,7 +29,6 @@ export const modes: Mode[] = [
"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:
1. ${initialCommentInstruction}
2. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
3. Understand the requirements and any existing plan
4. Make the necessary code changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.110",
"version": "0.0.111",
"type": "module",
"files": [
"index.js",
+1 -1
View File
@@ -26,7 +26,7 @@ export async function run(
prompt,
agent: "claude",
...flatMorph(agents, (_, agent) =>
agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]])
agent.apiKeyNames.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]])
),
};
+5 -1
View File
@@ -6,6 +6,8 @@
[] test agent/mode combinations
[] test if home directory mcp.json works if mcp.json is specified in repo
[] add footer to the working comment ("executed by {agent}", link to pullfrog (homepage) w/ small logo?, feedback (create github issue), link to workflow run)- see https://github.com/colinhacks/zod/issues/5459#issuecomment-3548382991
[] avoid passing all of process.env into agents: minimum # of vars
[] toon encode in prompt
## MAYBE
@@ -31,4 +33,6 @@
[x] standardize mcp server
[x] entry.js
[x] split up prompts, load dynamically based on mode
[x] log.txt to stdout
[x] log.txt to stdout
[x] rename mcp to use underscore
[x] external.ts align to agents