Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b30cc166e3 | |||
| cbcf87f50d | |||
| f596d6d995 | |||
| 917b8804c0 | |||
| 9c51c450bc | |||
| 85f8fbfaf5 | |||
| 098df15764 | |||
| fe35e9e274 | |||
| d7d2035315 | |||
| c703ecc4f4 | |||
| b05d1bfc53 |
+1
-1
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import type { AgentName } from "../external.ts";
|
||||
import { claude } from "./claude.ts";
|
||||
import { codex } from "./codex.ts";
|
||||
import { cursor } from "./cursor.ts";
|
||||
import { gemini } from "./gemini.ts";
|
||||
import type { Agent } from "./shared.ts";
|
||||
|
||||
export const agents = {
|
||||
claude,
|
||||
codex,
|
||||
cursor,
|
||||
gemini,
|
||||
} as const;
|
||||
|
||||
export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number];
|
||||
} satisfies Record<AgentName, Agent>;
|
||||
|
||||
@@ -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
@@ -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"]>>;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+6124
-284
File diff suppressed because one or more lines are too long
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
+1
-1
@@ -77,7 +77,7 @@ export const CreateWorkingCommentTool = tool({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
issue_number: issueNumber,
|
||||
body: intent,
|
||||
body: `${intent} <img src="https://pullfrog.ai/party-parrot.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />`,
|
||||
});
|
||||
|
||||
workingCommentId = result.data.id;
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
|
||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { fromHere } from "@ark/fs";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import type { Mode } from "../modes.ts";
|
||||
import { parseRepoContext } from "../utils/github.ts";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
|
||||
export type McpName = typeof ghPullfrogMcpName;
|
||||
|
||||
@@ -16,9 +16,9 @@ export function createMcpConfigs(githubInstallationToken: string, modes: Mode[])
|
||||
const repoContext = parseRepoContext();
|
||||
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||
|
||||
// In production (GitHub Actions), mcp-server.js is in same directory as entry.js (where this is bundled)
|
||||
// In production (GitHub Actions), mcp-server is in same directory as entry.js (where this is bundled)
|
||||
// In development, server.ts is in the same directory as this file (config.ts)
|
||||
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server.js") : fromHere("server.ts");
|
||||
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server") : fromHere("server.ts");
|
||||
|
||||
return {
|
||||
[ghPullfrogMcpName]: {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -6,13 +6,16 @@ export interface Mode {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
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[] = [
|
||||
{
|
||||
name: "Plan",
|
||||
description:
|
||||
"Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
|
||||
prompt: `Follow these steps:
|
||||
1. Create initial response comment using ${ghPullfrogMcpName}/create_working_comment saying "I'll {summary of request}"
|
||||
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. Analyze the request and break it down into clear, actionable tasks
|
||||
4. Consider dependencies, potential challenges, and implementation order
|
||||
@@ -25,7 +28,7 @@ export const modes: Mode[] = [
|
||||
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. Create initial response comment using ${ghPullfrogMcpName}/create_working_comment saying "I'll {summary of request}"
|
||||
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
|
||||
@@ -38,7 +41,8 @@ export const modes: Mode[] = [
|
||||
description:
|
||||
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||
prompt: `Follow these steps:
|
||||
1. Create initial response comment using ${ghPullfrogMcpName}/create_working_comment saying "I'll review this"
|
||||
1. ${initialCommentInstruction}
|
||||
|
||||
2. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
|
||||
3. View diff: git diff origin/<base>...origin/<head> (use line numbers from this for inline comments, replace <base> and <head> with 'base' and 'head' from PR info)
|
||||
4. Read files from the checked-out PR branch to understand the implementation
|
||||
@@ -52,7 +56,8 @@ export const modes: Mode[] = [
|
||||
description:
|
||||
"Fallback for tasks that don't fit other workflows, direct prompts via comments, or requests requiring general assistance without a specific workflow pattern",
|
||||
prompt: `Follow these steps:
|
||||
1. Create an initial "Working Comment" using ${ghPullfrogMcpName}/create_working_comment saying "I'll {summary of request}"
|
||||
1. ${initialCommentInstruction}
|
||||
|
||||
2. Perform the requested task. Only take action if you have high confidence that you understand what is being asked. If you are not sure, ask for clarification. Take stock of the tools at your disposal.
|
||||
3. As your work progresses, update your Working Comment to share progress and results using ${ghPullfrogMcpName}/update_working_comment. Do not create additional comments unless you are explicitly asked to do so.
|
||||
4. When you finish the task, update the Working Comment a final time with a summary of the results and links to any created issues, PRs, etc.`,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.109",
|
||||
"version": "0.0.111",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -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()]])
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { appendFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import { table } from "table";
|
||||
|
||||
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
||||
@@ -160,6 +161,43 @@ async function summaryTable(
|
||||
core.info(`\n${tableText}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a formatted table using the table package
|
||||
* Also logs to console and GitHub Actions summary
|
||||
*/
|
||||
async function printTable(
|
||||
rows: Array<Array<{ data: string; header?: boolean } | string>>,
|
||||
options?: {
|
||||
title?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const { title } = options || {};
|
||||
|
||||
// Convert rows to string arrays for the table package
|
||||
const tableData = rows.map((row) =>
|
||||
row.map((cell) => {
|
||||
if (typeof cell === "string") {
|
||||
return cell;
|
||||
}
|
||||
return cell.data;
|
||||
})
|
||||
);
|
||||
|
||||
const formatted = table(tableData);
|
||||
|
||||
if (title) {
|
||||
core.info(`\n${title}`);
|
||||
}
|
||||
core.info(`\n${formatted}\n`);
|
||||
|
||||
if (isGitHubActions) {
|
||||
if (title) {
|
||||
core.summary.addRaw(`**${title}**\n\n`);
|
||||
}
|
||||
core.summary.addRaw(`\`\`\`\n${formatted}\n\`\`\`\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a separator line
|
||||
*/
|
||||
@@ -240,6 +278,11 @@ export const log = {
|
||||
*/
|
||||
summaryTable,
|
||||
|
||||
/**
|
||||
* Print a formatted table using the table package
|
||||
*/
|
||||
table: printTable,
|
||||
|
||||
/**
|
||||
* Print a separator line
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user