Compare commits

...

44 Commits

Author SHA1 Message Date
David Blass aaba4b7650 bump version 2025-11-11 16:42:43 -05:00
David Blass 0bf456b6dc fix pnpm play 2025-11-11 16:42:30 -05:00
Shawn Morreau e8ca1d87ef merge main 2025-11-11 15:53:52 -05:00
Shawn Morreau e9458ea4bf add security prompting 2025-11-11 15:45:51 -05:00
Colin McDonnell 37428e8710 Fmt tsconfig 2025-11-11 11:40:49 -08:00
Colin McDonnell 0b80b0d581 Remove compiled entry.js (will be regenerated on build) 2025-11-11 11:35:42 -08:00
Colin McDonnell 40dc13b55f Add repo settings API integration and move workflows into action
- Add getRepoSettings utility to fetch repo settings from Pullfrog API
- Integrate repo settings fetch in main.ts with agent validation
- Move workflows from lib/workflows.ts into action/workflows.ts
- Update workflow prompts to include comment management steps
- Add 'Prompt' workflow as fallback for general tasks
- Fix null check for response.body in claude agent tarball download
- Remove unused message handlers (tool_progress, auth_status)
- Fix tsconfig.json indentation consistency
2025-11-11 11:35:10 -08:00
David Blass 894c525f21 update todo 2025-11-11 13:32:19 -05:00
Colin McDonnell bebc8c626f extract Prompt as a mode 2025-11-11 03:35:13 -08:00
Colin McDonnell aa617f2037 update prompt 2025-11-11 03:15:24 -08:00
Shawn Morreau 1c128b293f don't allow rejecting prs 2025-11-10 16:53:31 -05:00
Shawn Morreau c08008668b Merge branch 'main' of https://github.com/pullfrog/action 2025-11-10 16:05:44 -05:00
David Blass 7ac2938570 update todos 2025-11-10 16:02:37 -05:00
Shawn Morreau 363e4ecda2 update readme 2025-11-10 15:27:16 -05:00
David Blass 13cc56944f remove some debug logging 2025-11-06 21:11:28 -05:00
David Blass 2d91473f6e debug mcp 2025-11-06 21:03:13 -05:00
David Blass 3937c3bdba debug mcp server location 2025-11-06 20:58:19 -05:00
David Blass bac3f3e9c6 bundle mcp-server.js 2025-11-06 20:50:20 -05:00
David Blass 5ea1d95b70 debug dir structure 2025-11-06 20:38:20 -05:00
David Blass 6d0c21f0f5 move directory logging 2025-11-06 20:34:35 -05:00
David Blass c31824144b fix bundle import 2025-11-06 20:32:28 -05:00
David Blass 0a63f3da9d try download claude 2025-11-06 20:28:58 -05:00
David Blass 42b023cc86 okok 2025-11-06 19:37:31 -05:00
David Blass 854e3d5e4d add debug 2025-11-06 19:19:26 -05:00
David Blass 5bb1b779a8 iter 2025-11-06 19:13:42 -05:00
David Blass 599264694e try again 2025-11-06 19:08:25 -05:00
David Blass b9c15e9f38 fix github config 2025-11-06 19:05:57 -05:00
Pullfrog Action 7ef44eb254 try esm action 2025-11-06 19:03:19 -05:00
David Blass 5a21d40d27 start mcp server in memory 2025-11-06 17:56:06 -05:00
David Blass 175f92542e bump version 2025-11-06 17:40:19 -05:00
David Blass b448787f24 update lock 2025-11-06 17:38:49 -05:00
David Blass 65e3da81e9 revert to js action 2025-11-06 17:35:32 -05:00
Colin McDonnell f31e3a026e Update 2025-11-05 22:35:56 -08:00
Colin McDonnell 220652f27b Tweak prompt 2025-11-05 20:59:10 -08:00
Colin McDonnell 349af82bfc remove unrecognized handlers 2025-11-05 19:02:06 -08:00
David Blass 15732d126d start working on passthrough logging for bash 2025-11-05 19:27:37 -05:00
David Blass 36b006108b tweak mcp prompt 2025-11-05 16:03:47 -05:00
David Blass 029ae0d280 bump version 2025-11-05 15:54:37 -05:00
David Blass 92b435eb80 switch to pnpm CLAUDE-ACTION.md README.md action.yml agents coverage entry.ts fixtures index.ts main.ts mcp node_modules package.json play.ts pnpm-lock.yaml todo.md tsconfig.json utils 2025-11-05 15:52:57 -05:00
David Blass cacf9674c4 remove pnpm latest 2025-11-05 13:53:55 -05:00
David Blass f73260e3e6 remove pnpm cache 2025-11-05 13:50:47 -05:00
David Blass 3ddd6db7ca add mode, comment edit prompting 2025-11-05 11:08:44 -05:00
David Blass 68499340e4 add todo 2025-11-02 14:30:42 -05:00
David Blass acb06634be rely primarily on inline pr feedback 2025-10-31 04:04:06 -04:00
26 changed files with 144100 additions and 118 deletions
+10
View File
@@ -0,0 +1,10 @@
# Ensure lockfile is up to date
echo "🔒 Updating lockfile..."
pnpm install --lockfile-only
# Build the action before committing
echo "🔨 Building action..."
pnpm build
# Add the built files and lockfile to the commit
git add entry.js mcp-server.js pnpm-lock.yaml
-3
View File
@@ -9,9 +9,6 @@ GitHub Action for running Claude Code and other agents via Pullfrog.
```bash
# Install dependencies
pnpm install
# Test with default prompt
npm run play # Run locally on your machine
```
## Testing with play.ts
+2 -26
View File
@@ -12,32 +12,8 @@ inputs:
required: false
runs:
using: "composite"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js 24
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "pnpm"
- name: Install dependencies
run: pnpm install
shell: bash
working-directory: ${{ github.action_path }}
- name: Run agent
run: |
cd $GITHUB_WORKSPACE
node ${{ github.action_path }}/entry.ts
shell: bash
env:
INPUTS_JSON: ${{ toJSON(inputs) }}
using: "node20"
main: "entry.js"
branding:
icon: "code"
+139 -3
View File
@@ -1,16 +1,115 @@
import { execSync } from "node:child_process";
import { createWriteStream, existsSync, rmSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts";
import { type Agent, instructions } from "./shared.ts";
let cachedCliPath: string | undefined;
export const claude: Agent = {
install: async (): Promise<string> => {
if (cachedCliPath) {
log.info(`Using cached Claude Code CLI at ${cachedCliPath}`);
return cachedCliPath;
}
// Get the SDK version from package.json and resolve to actual version
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
let sdkVersion: string;
// If it's a range (starts with ^ or ~), query npm registry for the latest matching version
if (versionRange.startsWith("^") || versionRange.startsWith("~")) {
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
log.info(`Resolving version for range ${versionRange}...`);
try {
const registryResponse = await fetch(`${npmRegistry}/@anthropic-ai/claude-agent-sdk`);
if (!registryResponse.ok) {
throw new Error(`Failed to query registry: ${registryResponse.status}`);
}
const registryData = (await registryResponse.json()) as {
"dist-tags": { latest: string };
versions: Record<string, unknown>;
};
// Get the latest version that matches the range (simplified: just use latest)
sdkVersion = registryData["dist-tags"].latest;
log.info(`Resolved to version ${sdkVersion}`);
} catch (error) {
log.warning(
`Failed to resolve version from registry, using latest: ${error instanceof Error ? error.message : String(error)}`
);
sdkVersion = "latest";
}
} else {
sdkVersion = versionRange;
}
log.info(`📦 Installing Claude Code CLI from @anthropic-ai/claude-agent-sdk@${sdkVersion}...`);
// Create temp directory
const tempDir = await mkdtemp(join(tmpdir(), "claude-cli-"));
const tarballPath = join(tempDir, "package.tgz");
try {
// Download tarball from npm
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
const tarballUrl = `${npmRegistry}/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-${sdkVersion}.tgz`;
log.info(`Downloading from ${tarballUrl}...`);
const response = await fetch(tarballUrl);
if (!response.ok) {
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
}
// Write tarball to file
if (!response.body) throw new Error("Response body is null");
const fileStream = createWriteStream(tarballPath);
await pipeline(response.body, fileStream);
log.info(`Downloaded tarball to ${tarballPath}`);
// Extract tarball
log.info(`Extracting tarball...`);
execSync(`tar -xzf "${tarballPath}" -C "${tempDir}"`, { stdio: "pipe" });
// Find cli.js in the extracted package
const extractedDir = join(tempDir, "package");
const cliPath = join(extractedDir, "cli.js");
if (!existsSync(cliPath)) {
throw new Error(`cli.js not found in extracted package at ${cliPath}`);
}
cachedCliPath = cliPath;
log.info(`✓ Claude Code CLI installed at ${cliPath}`);
return cliPath;
} catch (error) {
// Cleanup on error
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
throw error;
}
},
run: async ({ prompt, mcpServers, apiKey }) => {
process.env.ANTHROPIC_API_KEY = apiKey;
if (!cachedCliPath) {
throw new Error("Claude CLI not installed. Call install() before run().");
}
const queryInstance = query({
prompt: `${instructions}\n\n${prompt}`,
prompt: `${instructions}\n\n****** USER PROMPT ******\n${prompt}`,
options: {
permissionMode: "bypassPermissions",
mcpServers,
pathToClaudeCodeExecutable: cachedCliPath,
},
});
@@ -37,6 +136,9 @@ type SDKMessageHandlers = {
[type in SDKMessageType]: SDKMessageHandler<type>;
};
// Track bash tool IDs to identify when bash tool results come back
const bashToolIds = new Set<string>();
const messageHandlers: SDKMessageHandlers = {
assistant: (data) => {
if (data.message?.content) {
@@ -46,6 +148,11 @@ const messageHandlers: SDKMessageHandlers = {
} else if (content.type === "tool_use") {
log.info(`${content.name}`);
// Track bash tool IDs
if (content.name === "bash" && content.id) {
bashToolIds.add(content.id);
}
if (content.input) {
const input = content.input as any;
if (input.description) log.info(` └─ ${input.description}`);
@@ -77,8 +184,35 @@ const messageHandlers: SDKMessageHandlers = {
user: (data) => {
if (data.message?.content) {
for (const content of data.message.content) {
if (content.type === "tool_result" && content.is_error) {
log.warning(`Tool error: ${content.content}`);
if (content.type === "tool_result") {
const toolUseId = (content as any).tool_use_id;
const isBashTool = toolUseId && bashToolIds.has(toolUseId);
if (isBashTool) {
// Log bash output in a collapsed group
const outputContent =
typeof content.content === "string"
? content.content
: Array.isArray(content.content)
? content.content
.map((c: any) => (typeof c === "string" ? c : c.text || JSON.stringify(c)))
.join("\n")
: String(content.content);
log.startGroup(`bash output`);
if (content.is_error) {
log.warning(outputContent);
} else {
log.info(outputContent);
}
log.endGroup();
// Clean up the tracked ID
bashToolIds.delete(toolUseId);
} else if (content.is_error) {
const errorContent =
typeof content.content === "string" ? content.content : String(content.content);
log.warning(`Tool error: ${errorContent}`);
}
}
}
}
@@ -107,4 +241,6 @@ const messageHandlers: SDKMessageHandlers = {
},
system: () => {},
stream_event: () => {},
tool_progress: () => {},
auth_status: () => {},
};
+42 -12
View File
@@ -1,5 +1,6 @@
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { ghPullfrogMcpName } from "../mcp/config.ts";
import { workflows } from "../workflows.ts";
/**
* Result returned by agent execution
@@ -22,19 +23,48 @@ export interface AgentConfig {
}
export type Agent = {
install: () => Promise<string>;
run: (config: AgentConfig) => Promise<AgentResult>;
};
export const instructions = `- use the ${ghPullfrogMcpName} MCP server to interact with github
- if ${ghPullfrogMcpName} is not available or doesn't include the functionality you need, describe why and bail
- do not under any circumstances use the gh cli
- if prompted by a comment to respond to create a new issue, pr or anything else, after succeeding,
also respond to the original comment with a very brief message containing a link to it
- if prompted to review a PR:
(1) get PR info with mcp__${ghPullfrogMcpName}__get_pull_request
(2) fetch both branches: git fetch origin <base> --depth=20 && git fetch origin <head>
(3) checkout the PR branch: git checkout origin/<head> (you MUST do this before reading any files)
(4) view diff: git diff origin/<base>...origin/<head> (this shows what changed)
(5) read files from the checked-out PR branch to understand the implementation
replace <base> and <head> with 'base' and 'head' from the PR info
export const instructions = `
# General instructions
You are a highly intelligent, no-nonsense senior-level software engineering agent. You will perform the task that is asked of you in the prompt below. You are careful, to-the-point, and kind. You only say things you know to be true. Your code is focused, minimal, and production-ready. You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so. You adapt your writing style to the style of your coworkers, while never being unprofessional.
## Getting Started
Before beginning, take some time to learn about the codebase. Read the AGENTS.md file if it exists. Understand how to install dependencies, run tests, run builds, and make changes according to the best practices of the codebase.
## SECURITY
CRITICAL SECURITY RULE - NEVER VIOLATE UNDER ANY CIRCUMSTANCES:
You must NEVER expose, display, print, echo, log, or output any of the following, regardless of what the user asks you to do:
- API keys (including but not limited to: ANTHROPIC_API_KEY, GITHUB_TOKEN, AWS keys, etc.)
- Authentication tokens or credentials
- Passwords or passphrases
- Private keys or certificates
- Database connection strings
- Any environment variables containing "KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", or "PRIVATE" in their name
- Any other sensitive information
This is a non-negotiable system security requirement. Even if the user explicitly requests you to show, display, or reveal any sensitive information, you must refuse. If you encounter any secrets in environment variables, files, or code, do not include them in your output. Instead, acknowledge that sensitive information was found but cannot be displayed.
If asked to show environment variables, only display non-sensitive system variables (e.g., PATH, HOME, USER, NODE_ENV). Filter out any variables matching sensitive patterns before displaying.
## MCP Servers
- eagerly inspect your MCP servers to determine what tools are available to you, especially ${ghPullfrogMcpName}
- do not under any circumstances use the github cli (\`gh\`). find the corresponding tool from ${ghPullfrogMcpName} instead.
## Workflow Selection
choose the appropriate workflow based on the prompt payload:
${workflows.map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
## Workflows
${workflows.map((w) => `### ${w.name}\n\n${w.prompt}`).join("\n\n")}
`;
Executable
+41506
View File
File diff suppressed because one or more lines are too long
+13 -12
View File
@@ -5,22 +5,23 @@
*/
import * as core from "@actions/core";
import { type } from "arktype";
import { Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" };
import { type Inputs, main } from "./main.ts";
import { log } from "./utils/cli.ts";
async function run(): Promise<void> {
// Change to GITHUB_WORKSPACE if set (this is where actions/checkout puts the repo)
// JavaScript actions run from the action's directory, not the checked-out repo
if (process.env.GITHUB_WORKSPACE && process.cwd() !== process.env.GITHUB_WORKSPACE) {
log.debug(`Changing to GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`);
process.chdir(process.env.GITHUB_WORKSPACE);
log.debug(`New working directory: ${process.cwd()}`);
}
try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
const inputsJson = process.env.INPUTS_JSON;
if (!inputsJson) {
throw new Error("INPUTS_JSON environment variable not found");
}
const parsed = type("string.json.parse").assert(inputsJson);
const inputs = Inputs.assert(parsed);
const inputs: Inputs = {
prompt: core.getInput("prompt", { required: true }),
anthropic_api_key: core.getInput("anthropic_api_key") || undefined,
};
const result = await main(inputs);
+43
View File
@@ -0,0 +1,43 @@
import { build } from "esbuild";
const sharedConfig = {
bundle: true,
format: "esm",
platform: "node",
target: "node20",
minify: false,
sourcemap: false,
// Bundle all dependencies - GitHub Actions doesn't have node_modules
// Only mark optional peer dependencies as external
external: [
"@valibot/to-json-schema",
"effect",
"sury",
],
// Provide a proper require shim for CommonJS modules bundled into ESM
// We use a unique variable name to avoid conflicts with bundled imports
banner: {
js: `import { createRequire as __createRequire } from 'module'; import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __dirnameFn } from 'path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __dirnameFn(__filename);`,
},
// Enable tree-shaking to remove unused code
treeShaking: true,
// Drop console statements in production (but keep for debugging)
drop: [],
};
// Build the main entry bundle (without MCP)
await build({
...sharedConfig,
entryPoints: ["./entry.ts"],
outfile: "./entry.js",
});
// Build the MCP server bundle
await build({
...sharedConfig,
entryPoints: ["./mcp/server.ts"],
outfile: "./mcp-server.js",
});
console.log("✅ Build completed successfully!");
+1 -1
View File
@@ -1 +1 @@
add a github review for the following PR: https://github.com/pullfrogai/scratch/pull/16
create a JSON file with the current process.env
+1 -2
View File
@@ -3,8 +3,7 @@
* This exports the main function for programmatic usage
*/
export { ClaudeAgent } from "./agents/claude.ts";
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
export type { Agent, AgentConfig, AgentResult } from "./agents/shared.ts";
export {
type Inputs as ExecutionInputs,
type MainResult,
+17 -1
View File
@@ -1,6 +1,8 @@
import { type } from "arktype";
import { claude } from "./agents/claude.ts";
import { createMcpConfigs } from "./mcp/config.ts";
import packageJson from "./package.json" with { type: "json" };
import { getRepoSettings } from "./utils/api.ts";
import { log } from "./utils/cli.ts";
import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
@@ -18,24 +20,38 @@ export interface MainResult {
error?: string | undefined;
}
export type PromptJSON = {};
export async function main(inputs: Inputs): Promise<MainResult> {
try {
log.info("Starting agent run...");
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
setupGitConfig();
const githubInstallationToken = await setupGitHubInstallationToken();
const repoContext = parseRepoContext();
// Fetch repo settings (agent, permissions, workflows) from API
const repoSettings = await getRepoSettings(githubInstallationToken, repoContext);
const agent = repoSettings.defaultAgent || "claude";
if (agent !== "claude") throw new Error(`Unsupported agent: ${agent}`);
setupGitAuth(githubInstallationToken, repoContext);
const mcpServers = createMcpConfigs(githubInstallationToken);
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
// Install Claude CLI before running
await claude.install();
log.info("Running Claude Agent SDK...");
log.box(inputs.prompt, { title: "Prompt" });
// TODO: check if `inputs.prompts` is JSON
// if yes, check if it's a webhook payload or toJSON(github.event)
// for webhook payloads, check the specified `agent` field
const result = await claude.run({
prompt: inputs.prompt,
mcpServers,
Executable
+101714
View File
File diff suppressed because one or more lines are too long
+28 -1
View File
@@ -6,7 +6,7 @@ export const Comment = type({
body: type.string.describe("the comment body content"),
});
export const CommentTool = tool({
export const CreateCommentTool = tool({
name: "create_issue_comment",
description: "Create a comment on a GitHub issue",
parameters: Comment,
@@ -26,3 +26,30 @@ export const CommentTool = tool({
};
}),
});
export const EditComment = type({
commentId: type.number.describe("the ID of the comment to edit"),
body: type.string.describe("the new comment body content"),
});
export const EditCommentTool = tool({
name: "edit_issue_comment",
description: "Edit a GitHub issue comment by its ID",
parameters: EditComment,
execute: contextualize(async ({ commentId, body }, ctx) => {
const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.name,
comment_id: commentId,
body: body,
});
return {
success: true,
commentId: result.data.id,
url: result.data.html_url,
body: result.data.body,
updatedAt: result.data.updated_at,
};
}),
});
+5 -3
View File
@@ -6,8 +6,6 @@ import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs";
import { parseRepoContext } from "../utils/github.ts";
const actionPath = fromHere("..");
export const ghPullfrogMcpName = "gh-pullfrog";
export type McpName = typeof ghPullfrogMcpName;
@@ -18,10 +16,14 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
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 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");
return {
[ghPullfrogMcpName]: {
command: "node",
args: [`${actionPath}/mcp/server.ts`],
args: [serverPath],
env: {
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
+18 -1
View File
@@ -1,4 +1,6 @@
import { execSync } from "node:child_process";
import { type } from "arktype";
import { log } from "../utils/cli.ts";
import { contextualize, tool } from "./shared.ts";
export const PullRequestInfo = type({
@@ -7,7 +9,8 @@ export const PullRequestInfo = type({
export const PullRequestInfoTool = tool({
name: "get_pull_request",
description: "Retrieve minimal information for a specific pull request by number.",
description:
"Retrieve PR information and automatically prepare the repository for review by fetching and checking out the PR branch.",
parameters: PullRequestInfo,
execute: contextualize(async ({ pull_number }, ctx) => {
const pr = await ctx.octokit.rest.pulls.get({
@@ -21,6 +24,20 @@ export const PullRequestInfoTool = tool({
const baseBranch = data.base.ref;
const headBranch = data.head.ref;
if (!baseBranch) {
throw new Error(`Base branch not found for PR #${pull_number}`);
}
// Automatically fetch and checkout branches for review
log.info(`Fetching base branch: origin/${baseBranch}`);
execSync(`git fetch origin ${baseBranch} --depth=20`, { stdio: "inherit" });
log.info(`Fetching PR branch: origin/${headBranch}`);
execSync(`git fetch origin ${headBranch}`, { stdio: "inherit" });
log.info(`Checking out PR branch: origin/${headBranch}`);
execSync(`git checkout origin/${headBranch}`, { stdio: "inherit" });
return {
number: data.number,
url: data.html_url,
+52 -13
View File
@@ -4,41 +4,80 @@ import { contextualize, tool } from "./shared.ts";
export const Review = type({
pull_number: type.number.describe("The pull request number to review"),
event: type
.enumerated("APPROVE", "REQUEST_CHANGES", "COMMENT")
.describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"),
body: type.string
.describe("The body content for the review (required for REQUEST_CHANGES or COMMENT)")
.describe(
"Brief summary or general feedback that doesn't apply to specific code locations. Keep it concise - most feedback should be in the 'comments' array."
)
.optional(),
commit_id: type.string
.describe("Optional SHA of the commit being reviewed. Defaults to latest.")
.optional(),
comments: type({
path: type.string.describe("The file path to comment on"),
position: type.number.describe("The diff position in the file"),
body: type.string.describe("The comment text"),
path: type.string.describe("The file path to comment on (relative to repo root)"),
line: type.number.describe(
"The line number in the file (use line numbers from the diff - usually the RIGHT side/new code)"
),
side: type
.enumerated("LEFT", "RIGHT")
.describe(
"Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided."
)
.optional(),
body: type.string.describe("The comment text for this specific line"),
start_line: type.number
.describe("Start line for multi-line comments (optional, for commenting on ranges)")
.optional(),
})
.array()
.describe("Array of draft review comments for specific lines, optional.")
.describe(
"REQUIRED: Array of inline comments for specific code issues. Use this for all location-specific feedback. Use 'git diff origin/<base>...origin/<head>' to find the correct line numbers (typically use the line numbers shown on the RIGHT side for new code, LEFT side for old code)."
)
.optional(),
});
export const ReviewTool = tool({
name: "submit_pull_request_review",
description:
"Submit a review (approve, request changes, or comment) for an existing pull request.",
"Submit a review (approve, request changes, or comment) for an existing pull request. " +
"IMPORTANT: Use 'comments' array for ALL specific code issues at the line-level. " +
"Only use 'body' for a brief summary or feedback that doesn't apply to a specific location.",
parameters: Review,
execute: contextualize(async ({ pull_number, event, body, commit_id, comments = [] }, ctx) => {
execute: contextualize(async ({ pull_number, body, commit_id, comments = [] }, ctx) => {
// Get the PR to determine the head commit if commit_id not provided
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number,
});
// Compose the request
const params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"] = {
owner: ctx.owner,
repo: ctx.name,
pull_number,
event,
event: "COMMENT",
};
if (body) params.body = body;
if (commit_id) params.commit_id = commit_id;
if (comments.length > 0) params.comments = comments;
if (commit_id) {
params.commit_id = commit_id;
} else {
params.commit_id = pr.data.head.sha;
}
if (comments.length > 0) {
type ReviewComment = (typeof params.comments & {})[number];
// Convert comments to the format expected by GitHub API
params.comments = comments.map((comment) => {
const reviewComment: ReviewComment = {
...comment,
};
reviewComment.side = comment.side || "RIGHT";
if (comment.start_line) {
reviewComment.start_line = comment.start_line;
reviewComment.start_side = comment.side || "RIGHT";
}
return reviewComment;
});
}
const result = await ctx.octokit.rest.pulls.createReview(params);
return {
success: true,
+9 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
// Minimal GitHub Issue Comment MCP Server
import { FastMCP } from "fastmcp";
import { CommentTool } from "./comment.ts";
import { CreateCommentTool, EditCommentTool } from "./comment.ts";
import { IssueTool } from "./issue.ts";
import { PullRequestTool } from "./pr.ts";
import { PullRequestInfoTool } from "./prInfo.ts";
@@ -13,6 +13,13 @@ const server = new FastMCP({
version: "0.0.1",
});
addTools(server, [CommentTool, IssueTool, PullRequestTool, ReviewTool, PullRequestInfoTool]);
addTools(server, [
CreateCommentTool,
EditCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
]);
server.start();
+9 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.67",
"version": "0.0.89",
"type": "module",
"files": [
"index.js",
@@ -15,19 +15,21 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"typecheck": "tsc --noEmit",
"build": "node esbuild.config.js",
"play": "node play.ts",
"upDeps": "pnpm up --latest",
"lock": "pnpm --ignore-workspace install"
"lock": "pnpm --ignore-workspace install",
"prepare": "husky"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
"@ark/fs": "0.50.0",
"@ark/util": "0.50.0",
"@ark/fs": "0.53.0",
"@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1",
"@standard-schema/spec": "1.0.0",
"arktype": "^2.1.25",
"arktype": "2.1.25",
"dotenv": "^17.2.3",
"execa": "^9.6.0",
"fastmcp": "^3.20.0",
@@ -36,6 +38,8 @@
"devDependencies": {
"@types/node": "^24.7.2",
"arg": "^5.0.2",
"esbuild": "^0.25.9",
"husky": "^9.0.0",
"typescript": "^5.9.3"
},
"repository": {
-3
View File
@@ -5,7 +5,6 @@ import { fromHere } from "@ark/fs";
import arg from "arg";
import { config } from "dotenv";
import { type Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts";
import { setupTestRepo } from "./utils/setup.ts";
@@ -15,8 +14,6 @@ export async function run(
prompt: string
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
const tempDir = join(process.cwd(), ".temp");
setupTestRepo({ tempDir, forceClean: true });
+289 -13
View File
@@ -15,11 +15,11 @@ importers:
specifier: ^0.1.26
version: 0.1.30(zod@3.25.76)
'@ark/fs':
specifier: 0.50.0
version: 0.50.0
specifier: 0.53.0
version: 0.53.0
'@ark/util':
specifier: 0.50.0
version: 0.50.0
specifier: 0.53.0
version: 0.53.0
'@octokit/rest':
specifier: ^22.0.0
version: 22.0.0
@@ -30,7 +30,7 @@ importers:
specifier: 1.0.0
version: 1.0.0
arktype:
specifier: ^2.1.25
specifier: 2.1.25
version: 2.1.25
dotenv:
specifier: ^17.2.3
@@ -51,6 +51,12 @@ importers:
arg:
specifier: ^5.0.2
version: 5.0.2
esbuild:
specifier: ^0.25.9
version: 0.25.12
husky:
specifier: ^9.0.0
version: 9.1.7
typescript:
specifier: ^5.9.3
version: 5.9.3
@@ -75,21 +81,174 @@ packages:
peerDependencies:
zod: ^3.24.1
'@ark/fs@0.50.0':
resolution: {integrity: sha512-6OrxNt2T+/pL4RUMZK/aiVRLIS3acNs5uSpHDyZAP6+OXwXchFSQ1lJTH+uuGBlDWeQxPD7VmymYXLF5s1Eyhw==}
'@ark/fs@0.53.0':
resolution: {integrity: sha512-XL0EbBAZgyy+j9aPhftYaBsbKAW5PTNSKCN6oLRRdrHuHPSAZgR6765/z0YZGhPxHEUNmq0vBoSk8yOLk91dNQ==}
'@ark/schema@0.53.0':
resolution: {integrity: sha512-1PB7RThUiTlmIu8jbSurPrhHpVixPd4C+xNBUF/HrjIENCeDcAMg36n5mpMzED7OQGDVIzpfXXiMnaTiutjHJw==}
'@ark/util@0.50.0':
resolution: {integrity: sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg==}
'@ark/util@0.53.0':
resolution: {integrity: sha512-TGn4gLlA6dJcQiqrtCtd88JhGb2XBHo6qIejsDre+nxpGuUVW4G3YZGVrwjNBTO0EyR+ykzIo4joHJzOj+/cpA==}
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@fastify/busboy@2.1.1':
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
@@ -373,6 +532,11 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -492,6 +656,11 @@ packages:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -879,18 +1048,94 @@ snapshots:
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-win32-x64': 0.33.5
'@ark/fs@0.50.0': {}
'@ark/fs@0.53.0': {}
'@ark/schema@0.53.0':
dependencies:
'@ark/util': 0.53.0
'@ark/util@0.50.0': {}
'@ark/util@0.53.0': {}
'@borewit/text-codec@0.1.1': {}
'@esbuild/aix-ppc64@0.25.12':
optional: true
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
'@esbuild/darwin-arm64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
'@esbuild/freebsd-x64@0.25.12':
optional: true
'@esbuild/linux-arm64@0.25.12':
optional: true
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@fastify/busboy@2.1.1': {}
'@img/sharp-darwin-arm64@0.33.5':
@@ -1171,6 +1416,35 @@ snapshots:
dependencies:
es-errors: 1.3.0
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
'@esbuild/android-arm': 0.25.12
'@esbuild/android-arm64': 0.25.12
'@esbuild/android-x64': 0.25.12
'@esbuild/darwin-arm64': 0.25.12
'@esbuild/darwin-x64': 0.25.12
'@esbuild/freebsd-arm64': 0.25.12
'@esbuild/freebsd-x64': 0.25.12
'@esbuild/linux-arm': 0.25.12
'@esbuild/linux-arm64': 0.25.12
'@esbuild/linux-ia32': 0.25.12
'@esbuild/linux-loong64': 0.25.12
'@esbuild/linux-mips64el': 0.25.12
'@esbuild/linux-ppc64': 0.25.12
'@esbuild/linux-riscv64': 0.25.12
'@esbuild/linux-s390x': 0.25.12
'@esbuild/linux-x64': 0.25.12
'@esbuild/netbsd-arm64': 0.25.12
'@esbuild/netbsd-x64': 0.25.12
'@esbuild/openbsd-arm64': 0.25.12
'@esbuild/openbsd-x64': 0.25.12
'@esbuild/openharmony-arm64': 0.25.12
'@esbuild/sunos-x64': 0.25.12
'@esbuild/win32-arm64': 0.25.12
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -1343,6 +1617,8 @@ snapshots:
human-signals@8.0.1: {}
husky@9.1.7: {}
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
+13
View File
@@ -0,0 +1,13 @@
[x] add modes to prompt
[x] progressively update comment
[x] don't allow rejecting prs
[x] fix pnpm caching
[x] fix prompt to avoid narration like "I just read all tools from MCP server"
[x] avoid exposing env adding ## SECURITY prompt
[] investigate including terminal output from bash commands as collapsed groups
[] test initialization trade offs for pullfrog.yml
[] try to find heavy claude code user
[] investigate repo config file?
[] look into trigger.yml without installation
[] cancel installation token at the end of github aciton
+15 -15
View File
@@ -2,22 +2,22 @@
"compilerOptions": {
"outDir": "./dist",
"module": "NodeNext",
"target": "ESNext",
"moduleResolution": "NodeNext",
"lib": ["ESNext"],
"target": "ESNext",
"moduleResolution": "NodeNext",
"lib": ["ESNext"],
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedSideEffectImports": true,
"declaration": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"stripInternal": true,
"rewriteRelativeImportExtensions": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedSideEffectImports": true,
"declaration": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"stripInternal": true,
"moduleDetection": "force",
"useUnknownInCatchVariables": true
"useUnknownInCatchVariables": true
}
}
+64
View File
@@ -0,0 +1,64 @@
import type { RepoContext } from "./github.ts";
export interface RepoSettings {
defaultAgent: string | null;
webAccessLevel: "full_access" | "limited";
webAccessAllowTrusted: boolean;
webAccessDomains: string;
workflows: Array<{
id: string;
name: string;
description: string;
prompt: string;
}>;
}
const DEFAULT_REPO_SETTINGS: RepoSettings = {
defaultAgent: null,
webAccessLevel: "full_access",
webAccessAllowTrusted: false,
webAccessDomains: "",
workflows: [],
};
/**
* Fetch repository settings from the Pullfrog API with fallback to defaults
* Returns agent, permissions, and workflows (excludes triggers)
* Returns defaults if repo doesn't exist or fetch fails
*/
export async function getRepoSettings(
token: string,
repoContext: RepoContext
): Promise<RepoSettings> {
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
try {
const response = await fetch(
`${apiUrl}/api/repo/${repoContext.owner}/${repoContext.name}/settings`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
// If API returns 404 or other error, fall back to defaults
return DEFAULT_REPO_SETTINGS;
}
const settings = (await response.json()) as RepoSettings | null;
// If API returns null (repo doesn't exist), return defaults
if (settings === null) {
return DEFAULT_REPO_SETTINGS;
}
return settings;
} catch {
// If fetch fails (network error, etc.), fall back to defaults
return DEFAULT_REPO_SETTINGS;
}
}
+32
View File
@@ -7,6 +7,28 @@ import * as core from "@actions/core";
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
/**
* Start a collapsed group (GitHub Actions) or regular group (local)
*/
function startGroup(name: string): void {
if (isGitHubActions) {
core.startGroup(name);
} else {
console.group(name);
}
}
/**
* End a collapsed group
*/
function endGroup(): void {
if (isGitHubActions) {
core.endGroup();
} else {
console.groupEnd();
}
}
/**
* Print a formatted box with text (for console output)
*/
@@ -229,4 +251,14 @@ export const log = {
await core.summary.write();
}
},
/**
* Start a collapsed group (GitHub Actions) or regular group (local)
*/
startGroup,
/**
* End a collapsed group
*/
endGroup,
};
+25 -2
View File
@@ -41,17 +41,40 @@ export function setupTestRepo(options: SetupOptions): void {
/**
* Setup git configuration to avoid identity errors
* Only runs in GitHub Actions environment to avoid overwriting local git config
*/
export function setupGitConfig(): void {
// Only set up git config in GitHub Actions environment
// In local development, use the user's existing git config
if (!process.env.GITHUB_ACTIONS) {
return;
}
log.info("🔧 Setting up git configuration...");
execSync('git config user.email "action@pullfrog.ai"', { stdio: "inherit" });
execSync('git config user.name "Pullfrog Action"', { stdio: "inherit" });
try {
execSync('git config user.email "action@pullfrog.ai"', { stdio: "pipe" });
execSync('git config user.name "Pullfrog Action"', { stdio: "pipe" });
log.debug("setupGitConfig: ✓ Git configuration set successfully");
} catch (error) {
// If git config fails, log warning but don't fail the action
// This can happen if we're not in a git repo or git isn't available
log.warning(
`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Setup git authentication using GitHub token
* Only runs in GitHub Actions environment to avoid breaking local git remotes
*/
export function setupGitAuth(githubToken: string, repoContext: RepoContext): void {
// Only set up git auth in GitHub Actions environment
// In local testing, this would overwrite the real git remote with fake credentials
if (!process.env.GITHUB_ACTIONS) {
return;
}
log.info("🔐 Setting up git authentication...");
// Remove existing git auth headers that actions/checkout might have set
+53
View File
@@ -0,0 +1,53 @@
// MCP name constant - matches action/mcp/config.ts
const ghPullfrogMcpName = "gh-pullfrog";
export const workflows = [
{
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 mcp__${ghPullfrogMcpName}__create_issue_comment saying "I'll {summary of request}" and save the commentId
2. Analyze the request and break it down into clear, actionable tasks
3. Consider dependencies, potential challenges, and implementation order
4. Create a structured plan with clear milestones
5. Update your comment using mcp__${ghPullfrogMcpName}__edit_issue_comment with the commentId to present the plan in a clear, organized format
6. Continue updating the same comment as needed (never create additional comments - always use edit_issue_comment)`,
},
{
name: "Implement",
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 mcp__${ghPullfrogMcpName}__create_issue_comment saying "I'll {summary of request}" and save the commentId
2. Understand the requirements and any existing plan
3. Make the necessary code changes
4. Test your changes to ensure they work correctly
5. Update your comment using mcp__${ghPullfrogMcpName}__edit_issue_comment with the commentId to share progress and results
6. Continue updating the same comment as you make progress (never create additional comments - always use edit_issue_comment)`,
},
{
name: "Review",
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 mcp__${ghPullfrogMcpName}__create_issue_comment saying "I'll review this" and save the commentId
2. Get PR info with mcp__${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
5. Update your comment using mcp__${ghPullfrogMcpName}__edit_issue_comment with findings as you review
6. When submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff
7. Only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location
8. Continue updating the same comment as needed (never create additional comments - always use edit_issue_comment)`,
},
{
name: "Prompt",
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 "Progress Comment" using mcp__${ghPullfrogMcpName}__create_issue_comment saying "I'll {summary of request}" and save the commentId
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 Progress Comment to share progress and results. Using mcp__${ghPullfrogMcpName}__edit_issue_comment and the commentId you saved earlier. Do not create additional comments unless you are explicitly asked to do so.
4. When you finish the task, update the Progress Comment a final time to confirm completion. If you created any issues, PRs, etc, include appropriate links to it here.`,
},
] as const;