Compare commits

..

28 Commits

Author SHA1 Message Date
David Blass 706ce04895 bump version 2025-10-23 15:37:04 -04:00
David Blass 09be8e3068 try adding github token to env 2025-10-23 15:35:36 -04:00
David Blass c6c1210fa0 refactor tool implementation 2025-10-23 15:21:08 -04:00
David Blass 0368512b9e add pr and issue creation support 2025-10-23 10:24:32 -04:00
David Blass 9fb6135fd2 bump 2025-10-17 22:27:59 -04:00
David Blass bb78e5f94b update lockfile 2025-10-17 22:27:24 -04:00
David Blass c668578c6f refactor mcp and add instructions prefix 2025-10-17 22:26:24 -04:00
ssalbdivad 7f1566d9c2 update lockfile 2025-10-15 17:25:54 -04:00
ssalbdivad dd482566c2 bump version 2025-10-15 17:24:58 -04:00
ssalbdivad 57029c32a3 remove zod3 2025-10-15 17:24:50 -04:00
ssalbdivad 757d336475 switch to fastmcp 2025-10-15 17:24:29 -04:00
David Blass d03debab4b bump version 2025-10-14 15:56:27 -04:00
David Blass a05829f781 fix type errors 2025-10-14 14:58:46 -04:00
David Blass c8ba7940e3 fix installation token propagation 2025-10-13 17:21:14 -04:00
David Blass 710fdd0fa4 bump version 2025-10-13 17:09:19 -04:00
David Blass 4f5ee28b8a update publish to reflect no build 2025-10-13 17:08:59 -04:00
David Blass 806458b95a fix install loop 2025-10-13 17:06:34 -04:00
David Blass 2c856e3337 remove husky 2025-10-13 17:04:59 -04:00
David Blass a93c34e61b refactor action to use INPUTS_JSON object 2025-10-13 16:57:02 -04:00
David Blass cd20491d22 fix pnpm caching 2025-10-13 15:35:01 -04:00
David Blass 1a6ce6728c bump version 2025-10-13 15:30:48 -04:00
David Blass 3b39f2c8d8 move pnpm version specifier to actions 2025-10-13 15:30:41 -04:00
David Blass ec0eeb1d18 add packageManager to action package.json 2025-10-13 15:27:46 -04:00
David Blass 8ef805b9fc remove pnpm version from publish action 2025-10-13 15:27:05 -04:00
David Blass 6e93fd9a72 specify packageManager 2025-10-13 15:24:04 -04:00
David Blass 9567d84442 setup pnpm first 2025-10-13 15:21:09 -04:00
David Blass d79564db5e add pnpm setup 2025-10-13 15:14:51 -04:00
David Blass a7a0e87fd8 setup deps 2025-10-13 15:12:38 -04:00
25 changed files with 643 additions and 27260 deletions
+1 -16
View File
@@ -29,7 +29,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "24"
cache: "pnpm" cache: "pnpm"
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
@@ -60,21 +60,6 @@ jobs:
echo "✅ Tag ${{ steps.version.outputs.tag }} does not exist - will create release" echo "✅ Tag ${{ steps.version.outputs.tag }} does not exist - will create release"
fi fi
- name: Verify built files are up to date
if: steps.check_tag.outputs.exists == 'false'
run: |
# Check if there are any uncommitted changes
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Error: There are uncommitted changes. Built files should be committed via pre-commit hook."
git status
exit 1
fi
echo "✅ All built files are up to date"
- name: Build for npm with zshy
if: steps.check_tag.outputs.exists == 'false'
run: pnpm build:npm
- name: Create and push tags - name: Create and push tags
if: steps.check_tag.outputs.exists == 'false' if: steps.check_tag.outputs.exists == 'false'
run: | run: |
-10
View File
@@ -1,10 +0,0 @@
# Ensure lockfile is up to date
echo "🔒 Updating lockfile..."
pnpm install --lockfile-only
# Build the action before committing
echo "🔨 Building action..."
npm run build
# Add the built files and lockfile to the commit
git add entry.cjs pnpm-lock.yaml
+17 -8
View File
@@ -10,25 +10,34 @@ inputs:
anthropic_api_key: anthropic_api_key:
description: "Anthropic API key for Claude Code authentication" description: "Anthropic API key for Claude Code authentication"
required: false required: false
github_token:
description: "GitHub token for repository access"
required: false
github_installation_token:
description: "GitHub App installation token"
required: false
runs: runs:
using: "composite" using: "composite"
steps: 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 - name: Setup Node.js 24
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "24" node-version: "24"
- name: Run TypeScript entry point cache: "pnpm"
- name: Install dependencies
run: pnpm install
shell: bash
working-directory: ${{ github.action_path }}
- name: Run agent
run: node entry.ts run: node entry.ts
shell: bash shell: bash
working-directory: ${{ github.action_path }} working-directory: ${{ github.action_path }}
env:
INPUTS_JSON: ${{ toJSON(inputs) }}
branding: branding:
icon: "code" icon: "code"
color: "orange" color: "green"
+30 -41
View File
@@ -3,6 +3,7 @@ import * as core from "@actions/core";
import { createMcpConfig } from "../mcp/config.ts"; import { createMcpConfig } from "../mcp/config.ts";
import { spawn } from "../utils/subprocess.ts"; import { spawn } from "../utils/subprocess.ts";
import { boxString, tableString } from "../utils/table.ts"; import { boxString, tableString } from "../utils/table.ts";
import { instructions } from "./shared.ts";
import type { Agent, AgentConfig, AgentResult } from "./types.ts"; import type { Agent, AgentConfig, AgentResult } from "./types.ts";
/** /**
@@ -10,6 +11,7 @@ import type { Agent, AgentConfig, AgentResult } from "./types.ts";
*/ */
export class ClaudeAgent implements Agent { export class ClaudeAgent implements Agent {
private apiKey: string; private apiKey: string;
private githubInstallationToken: string;
public runStats = { public runStats = {
toolsUsed: 0, toolsUsed: 0,
turns: 0, turns: 0,
@@ -17,10 +19,8 @@ export class ClaudeAgent implements Agent {
}; };
constructor(config: AgentConfig) { constructor(config: AgentConfig) {
if (!config.apiKey) {
throw new Error("Claude agent requires an API key");
}
this.apiKey = config.apiKey; this.apiKey = config.apiKey;
this.githubInstallationToken = config.githubInstallationToken;
} }
/** /**
@@ -49,10 +49,7 @@ export class ClaudeAgent implements Agent {
try { try {
const result = await spawn({ const result = await spawn({
cmd: "bash", cmd: "bash",
args: [ args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"],
"-c",
"curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93",
],
env: { ANTHROPIC_API_KEY: this.apiKey }, env: { ANTHROPIC_API_KEY: this.apiKey },
timeout: 120000, // 2 minute timeout timeout: 120000, // 2 minute timeout
onStdout: () => {}, onStdout: () => {},
@@ -60,9 +57,7 @@ export class ClaudeAgent implements Agent {
}); });
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
throw new Error( throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`);
`Installation failed with exit code ${result.exitCode}: ${result.stderr}`
);
} }
core.info("Claude Code installed successfully"); core.info("Claude Code installed successfully");
@@ -79,7 +74,17 @@ export class ClaudeAgent implements Agent {
try { try {
const claudePath = `${process.env.HOME}/.local/bin/claude`; const claudePath = `${process.env.HOME}/.local/bin/claude`;
const env = {
ANTHROPIC_API_KEY: this.apiKey,
GITHUB_TOKEN: this.githubInstallationToken,
};
console.log(boxString(prompt, { title: "Prompt" })); console.log(boxString(prompt, { title: "Prompt" }));
const mcpConfig = createMcpConfig(this.githubInstallationToken);
console.log("📋 MCP Config:", mcpConfig);
const args = [ const args = [
"--print", "--print",
"--output-format", "--output-format",
@@ -88,22 +93,10 @@ export class ClaudeAgent implements Agent {
"--debug", "--debug",
"--permission-mode", "--permission-mode",
"bypassPermissions", "bypassPermissions",
"--mcp-config",
mcpConfig,
]; ];
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
throw new Error(
"GITHUB_INSTALLATION_TOKEN is required for GitHub integration"
);
}
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
console.log("📋 MCP Config:", mcpConfig);
args.push("--mcp-config", mcpConfig);
const env = {
ANTHROPIC_API_KEY: this.apiKey,
};
core.startGroup("🔄 Run details"); core.startGroup("🔄 Run details");
this.runStats = { this.runStats = {
@@ -119,7 +112,7 @@ export class ClaudeAgent implements Agent {
cmd: claudePath, cmd: claudePath,
args, args,
env, env,
input: prompt, input: `${instructions} ${prompt}`,
timeout: 10 * 60 * 1000, // 10 minutes timeout: 10 * 60 * 1000, // 10 minutes
onStdout: (_chunk) => { onStdout: (_chunk) => {
processJSONChunk(_chunk, this); processJSONChunk(_chunk, this);
@@ -159,8 +152,7 @@ export class ClaudeAgent implements Agent {
try { try {
core.endGroup(); core.endGroup();
} catch {} } catch {}
const errorMessage = const errorMessage = error instanceof Error ? error.message : "Unknown error";
error instanceof Error ? error.message : "Unknown error";
return { return {
success: false, success: false,
error: `Failed to execute Claude Code: ${errorMessage}`, error: `Failed to execute Claude Code: ${errorMessage}`,
@@ -174,8 +166,15 @@ export class ClaudeAgent implements Agent {
*/ */
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
try { try {
// Skip debug lines that start with [DEBUG] or [debug]
const trimmedChunk = chunk.trim();
if (trimmedChunk.startsWith("[DEBUG]") || trimmedChunk.startsWith("[debug]")) {
console.log(chunk);
return;
}
console.log(chunk); console.log(chunk);
const parsedChunk = JSON.parse(chunk.trim()); const parsedChunk = JSON.parse(trimmedChunk);
switch (parsedChunk.type) { switch (parsedChunk.type) {
case "system": case "system":
@@ -186,12 +185,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
["model", parsedChunk.model], ["model", parsedChunk.model],
["cwd", parsedChunk.cwd], ["cwd", parsedChunk.cwd],
["permission_mode", parsedChunk.permissionMode], ["permission_mode", parsedChunk.permissionMode],
[ ["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"],
"tools",
parsedChunk.tools?.length
? `${parsedChunk.tools.length} tools`
: "none",
],
[ [
"mcp_servers", "mcp_servers",
parsedChunk.mcp_servers?.length parsedChunk.mcp_servers?.length
@@ -218,9 +212,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
for (const content of parsedChunk.message.content) { for (const content of parsedChunk.message.content) {
if (content.type === "text") { if (content.type === "text") {
if (content.text.trim()) { if (content.text.trim()) {
core.info( core.info(boxString(content.text.trim(), { title: "Claude Code" }));
boxString(content.text.trim(), { title: "Claude Code" })
);
} }
} else if (content.type === "tool_use") { } else if (content.type === "tool_use") {
if (agent) { if (agent) {
@@ -305,10 +297,7 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
if (parsedChunk.subtype === "success") { if (parsedChunk.subtype === "success") {
core.info( core.info(
tableString([ tableString([
[ ["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
"Cost",
`$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`,
],
["Input Tokens", parsedChunk.usage?.input_tokens || 0], ["Input Tokens", parsedChunk.usage?.input_tokens || 0],
["Output Tokens", parsedChunk.usage?.output_tokens || 0], ["Output Tokens", parsedChunk.usage?.output_tokens || 0],
["Duration", `${parsedChunk.duration_ms}ms`], ["Duration", `${parsedChunk.duration_ms}ms`],
+8
View File
@@ -0,0 +1,8 @@
import { mcpServerName } from "../mcp/config.ts";
export const instructions = `- use the ${mcpServerName} MCP server to interact with github
- if ${mcpServerName} 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
`;
+2 -2
View File
@@ -29,6 +29,6 @@ export interface AgentResult {
* Configuration for agent creation * Configuration for agent creation
*/ */
export interface AgentConfig { export interface AgentConfig {
apiKey?: string; apiKey: string;
[key: string]: any; githubInstallationToken: string;
} }
-26451
View File
File diff suppressed because one or more lines are too long
+9 -35
View File
@@ -2,49 +2,26 @@
/** /**
* Entry point for GitHub Action * Entry point for GitHub Action
* This file is bundled to entry.cjs and called directly by GitHub Actions
*/ */
import * as core from "@actions/core"; import * as core from "@actions/core";
import { type ExecutionInputs, type MainParams, main } from "./main.ts"; import { type } from "arktype";
import { Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" }; import packageJson from "./package.json" with { type: "json" };
import { setupGitHubInstallationToken } from "./utils/github.ts";
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
console.log(`🐸 Running pullfrog/action@${packageJson.version}...`); console.log(`🐸 Running pullfrog/action@${packageJson.version}...`);
const prompt = core.getInput("prompt", { required: true });
const anthropic_api_key = core.getInput("anthropic_api_key");
if (!prompt) { const inputsJson = process.env.INPUTS_JSON;
throw new Error("prompt is required"); if (!inputsJson) {
throw new Error("INPUTS_JSON environment variable not found");
} }
const inputs: ExecutionInputs = { const parsed = type("string.json.parse").assert(inputsJson);
prompt, const inputs = Inputs.assert(parsed);
anthropic_api_key,
};
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN; const result = await main(inputs);
if (githubToken) {
inputs.github_token = githubToken;
}
const githubInstallationToken =
core.getInput("github_installation_token") || process.env.GITHUB_INSTALLATION_TOKEN;
if (githubInstallationToken) {
inputs.github_installation_token = githubInstallationToken;
} else {
await setupGitHubInstallationToken();
}
const params: MainParams = {
inputs,
env: {},
cwd: process.cwd(),
};
const result = await main(params);
if (!result.success) { if (!result.success) {
throw new Error(result.error || "Agent execution failed"); throw new Error(result.error || "Agent execution failed");
@@ -55,7 +32,4 @@ async function run(): Promise<void> {
} }
} }
run().catch((error) => { await run();
console.error("Action failed:", error);
process.exit(1);
});
-16
View File
@@ -1,16 +0,0 @@
import { build } from "esbuild";
// Build the GitHub Action bundle only
// For npm package builds, use zshy (pnpm build:npm)
await build({
entryPoints: ["./entry.ts"],
bundle: true,
outfile: "./entry.cjs",
format: "cjs",
platform: "node",
target: "node20",
minify: false,
sourcemap: false,
});
console.log("✅ Build completed successfully!");
+5 -9
View File
@@ -1,13 +1,9 @@
import type { MainParams } from "../main.ts"; import type { Inputs } from "../main.ts";
const testParams = { const testParams = {
inputs: { prompt:
prompt: "List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.",
"List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.", anthropic_api_key: "sk-test-key",
anthropic_api_key: "sk-test-key", } satisfies Inputs;
},
env: {},
cwd: process.cwd(),
} satisfies MainParams;
export default testParams; export default testParams;
+1 -3
View File
@@ -1,3 +1 @@
Use the MCP GitHub comment tool to add a comment containing your best frog joke to GitHub issue https://github.com/pullfrogai/scratch/issues/2. create a PR implementing bogosort to https://github.com/pullfrogai/scratch/
Do not use the gh cli. If the mcp tool does not work, bail.
+1 -2
View File
@@ -6,8 +6,7 @@
export { ClaudeAgent } from "./agents/claude.ts"; export { ClaudeAgent } from "./agents/claude.ts";
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts"; export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
export { export {
type ExecutionInputs, type Inputs as ExecutionInputs,
type MainParams,
type MainResult, type MainResult,
main, main,
} from "./main.ts"; } from "./main.ts";
+16 -27
View File
@@ -1,24 +1,14 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { type } from "arktype";
import { ClaudeAgent } from "./agents/claude.ts"; import { ClaudeAgent } from "./agents/claude.ts";
import { setupGitHubInstallationToken } from "./utils/github.ts";
export const EXPECTED_INPUTS: string[] = [ export const Inputs = type({
"ANTHROPIC_API_KEY", prompt: "string",
"GITHUB_TOKEN", "anthropic_api_key?": "string | undefined",
"GITHUB_INSTALLATION_TOKEN", });
];
export interface ExecutionInputs { export type Inputs = typeof Inputs.infer;
prompt: string;
anthropic_api_key: string;
github_token?: string;
github_installation_token?: string;
}
export interface MainParams {
inputs: ExecutionInputs;
env: Record<string, string>;
cwd: string;
}
export interface MainResult { export interface MainResult {
success: boolean; success: boolean;
@@ -26,19 +16,18 @@ export interface MainResult {
error?: string | undefined; error?: string | undefined;
} }
export async function main(params: MainParams): Promise<MainResult> { export async function main(inputs: Inputs): Promise<MainResult> {
try { try {
const { inputs, env, cwd } = params;
if (cwd !== process.cwd()) {
process.chdir(cwd);
}
Object.assign(process.env, env);
core.info(`→ Starting agent run with Claude Code`); core.info(`→ Starting agent run with Claude Code`);
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key }); // Setup GitHub installation token
const githubInstallationToken = await setupGitHubInstallationToken();
const agent = new ClaudeAgent({
apiKey: inputs.anthropic_api_key!,
githubInstallationToken,
});
await agent.install(); await agent.install();
const result = await agent.execute(inputs.prompt); const result = await agent.execute(inputs.prompt);
+28
View File
@@ -0,0 +1,28 @@
import { type } from "arktype";
import { contextualize, tool } from "./shared.ts";
export const Comment = type({
issueNumber: type.number.describe("the issue number to comment on"),
body: type.string.describe("the comment body content"),
});
export const CommentTool = tool({
name: "create_issue_comment",
description: "Create a comment on a GitHub issue",
parameters: Comment,
execute: contextualize(async ({ issueNumber, body }, ctx) => {
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
repo: ctx.name,
issue_number: issueNumber,
body: body,
});
return {
success: true,
commentId: result.data.id,
url: result.data.html_url,
body: result.data.body,
};
}),
});
+3 -3
View File
@@ -1,12 +1,12 @@
/** /**
* Simple MCP configuration helper for adding our minimal GitHub comment server * Simple MCP configuration helper for adding our minimal GitHub comment server
*/ */
// const actionPath = process.env.GITHUB_ACTION_PATH || process.cwd();
import { fromHere } from "@ark/fs"; import { fromHere } from "@ark/fs";
const actionPath = fromHere(".."); const actionPath = fromHere("..");
export const mcpServerName = "gh-pullfrog";
export function createMcpConfig(githubInstallationToken: string) { export function createMcpConfig(githubInstallationToken: string) {
const githubRepository = process.env.GITHUB_REPOSITORY; const githubRepository = process.env.GITHUB_REPOSITORY;
if (!githubRepository) { if (!githubRepository) {
@@ -18,7 +18,7 @@ export function createMcpConfig(githubInstallationToken: string) {
return JSON.stringify( return JSON.stringify(
{ {
mcpServers: { mcpServers: {
minimal_github_comment: { [mcpServerName]: {
command: "node", command: "node",
args: [`${actionPath}/mcp/server.ts`], args: [`${actionPath}/mcp/server.ts`],
env: { env: {
+42
View File
@@ -0,0 +1,42 @@
import { type } from "arktype";
import { contextualize, tool } from "./shared.ts";
export const Issue = type({
title: type.string.describe("the title of the issue"),
body: type.string.describe("the body content of the issue"),
labels: type.string
.array()
.describe("optional array of label names to apply to the issue")
.optional(),
assignees: type.string
.array()
.describe("optional array of usernames to assign to the issue")
.optional(),
});
export const IssueTool = tool({
name: "create_issue",
description: "Create a new GitHub issue",
parameters: Issue,
execute: contextualize(async ({ title, body, labels, assignees }, ctx) => {
const result = await ctx.octokit.rest.issues.create({
owner: ctx.owner,
repo: ctx.name,
title: title,
body: body,
labels: labels ?? [],
assignees: assignees ?? [],
});
return {
success: true,
issueId: result.data.id,
number: result.data.number,
url: result.data.html_url,
title: result.data.title,
state: result.data.state,
labels: result.data.labels?.map((label) => (typeof label === "string" ? label : label.name)),
assignees: result.data.assignees?.map((assignee) => assignee.login),
};
}),
});
+42
View File
@@ -0,0 +1,42 @@
import { execSync } from "node:child_process";
import { type } from "arktype";
import { contextualize, tool } from "./shared.ts";
export const PullRequest = type({
title: type.string.describe("the title of the pull request"),
body: type.string.describe("the body content of the pull request"),
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
});
export const PullRequestTool = tool({
name: "create_pull_request",
description: "Create a pull request from the current branch",
parameters: PullRequest,
execute: contextualize(async ({ title, body, base }, ctx) => {
// Get the current branch name
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
encoding: "utf8",
}).trim();
console.log(`Current branch: ${currentBranch}`);
const result = await ctx.octokit.rest.pulls.create({
owner: ctx.owner,
repo: ctx.name,
title: title,
body: body,
head: currentBranch,
base: base,
});
return {
success: true,
pullRequestId: result.data.id,
number: result.data.number,
url: result.data.html_url,
title: result.data.title,
head: result.data.head.ref,
base: result.data.base.ref,
};
}),
});
+9 -85
View File
@@ -1,92 +1,16 @@
#!/usr/bin/env node #!/usr/bin/env node
// Minimal GitHub Issue Comment MCP Server // Minimal GitHub Issue Comment MCP Server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { FastMCP } from "fastmcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CommentTool } from "./comment.ts";
import { Octokit } from "@octokit/rest"; import { IssueTool } from "./issue.ts";
import { type } from "arktype"; import { PullRequestTool } from "./pr.ts";
import { z } from "zod"; import { addTools } from "./shared.ts";
import { resolveRepoContext } from "../utils/repo-context.ts";
const server = new McpServer({ const server = new FastMCP({
name: "Minimal GitHub Issue Comment Server", name: "gh-pullfrog",
version: "0.0.1", version: "0.0.1",
}); });
// Define the schema for creating issue comments addTools(server, [CommentTool, IssueTool, PullRequestTool]);
const Comment = type({
issueNumber: type.number.describe("the issue number to comment on"),
body: type.string.describe("the comment body content"),
});
server.tool( server.start();
"create_issue_comment",
"Create a comment on a GitHub issue",
{
issueNumber: z.number().describe("the issue number to comment on"),
body: z.string().describe("the comment body content"),
},
async ({ issueNumber, body }) => {
try {
Comment.assert({ issueNumber, body });
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
if (!githubInstallationToken) {
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
}
// Resolve repository context from environment
const repoContext = resolveRepoContext();
const octokit = new Octokit({
auth: githubInstallationToken,
});
const result = await octokit.rest.issues.createComment({
owner: repoContext.owner,
repo: repoContext.name,
issue_number: issueNumber,
body: body,
});
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
commentId: result.data.id,
url: result.data.html_url,
body: result.data.body,
},
null,
2
),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error creating comment: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
}
);
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
process.on("exit", () => {
server.close();
});
}
await runServer();
+78
View File
@@ -0,0 +1,78 @@
import { cached } from "@ark/util";
import { Octokit } from "@octokit/rest";
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { FastMCP, Tool } from "fastmcp";
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
export interface ToolResult {
content: {
type: "text";
text: string;
}[];
error?: string;
isError?: boolean;
}
export const getMcpContext = cached((): McpContext => {
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
if (!githubInstallationToken) {
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
}
return {
...parseRepoContext(),
octokit: new Octokit({
auth: githubInstallationToken,
}),
};
});
export interface McpContext extends RepoContext {
octokit: Octokit;
}
export const tool = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => tool;
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
for (const tool of tools) {
server.addTool(tool);
}
return server;
};
export const contextualize =
<T>(executor: (params: T, ctx: McpContext) => Promise<Record<string, any>>) =>
async (params: T): Promise<ToolResult> => {
try {
const ctx = getMcpContext();
const result = await executor(params, ctx);
return handleToolSuccess(result);
} catch (error) {
return handleToolError(error);
}
};
const handleToolSuccess = (data: Record<string, any>): ToolResult => {
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
};
};
const handleToolError = (error: unknown): ToolResult => {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
};
+12 -21
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.32", "version": "0.0.55",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
@@ -12,39 +12,30 @@
"main.js", "main.js",
"main.d.ts" "main.d.ts"
], ],
"directories": {
"example": "examples"
},
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "node esbuild.config.js",
"build:npm": "zshy",
"build:dev": "node esbuild.config.js",
"prepare": "husky",
"play": "node play.ts", "play": "node play.ts",
"upDeps": "pnpm up --latest", "upDeps": "pnpm up --latest",
"createLockfile": "pnpm --ignore-workspace install" "lock": "pnpm --ignore-workspace install"
}, },
"dependencies": { "dependencies": {
"@ark/fs": "0.49.0",
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@modelcontextprotocol/sdk": "^1.17.5", "@ark/fs": "0.50.0",
"@ark/util": "0.50.0",
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1", "@octokit/webhooks-types": "^7.6.1",
"arktype": "^2.1.22", "@standard-schema/spec": "1.0.0",
"dotenv": "^17.2.2", "arktype": "^2.1.23",
"dotenv": "^17.2.3",
"execa": "^9.6.0", "execa": "^9.6.0",
"table": "^6.9.0", "fastmcp": "^3.20.0",
"zod": "^3.24.4" "table": "^6.9.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.0", "@types/node": "^24.7.2",
"arg": "^5.0.2", "arg": "^5.0.2",
"esbuild": "^0.25.9", "typescript": "^5.9.3"
"husky": "^9.0.0",
"typescript": "^5.3.0",
"zshy": "^0.4.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -52,7 +43,7 @@
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/pullfrog/action/issues" "url": "https://github.com/pullfrog/action/issues"
}, },
+4 -31
View File
@@ -4,10 +4,9 @@ import { pathToFileURL } from "node:url";
import { fromHere } from "@ark/fs"; import { fromHere } from "@ark/fs";
import arg from "arg"; import arg from "arg";
import { config } from "dotenv"; import { config } from "dotenv";
import { main } from "./main.ts"; import { type Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" }; import packageJson from "./package.json" with { type: "json" };
import { runAct } from "./utils/act.ts"; import { runAct } from "./utils/act.ts";
import { setupGitHubInstallationToken } from "./utils/github.ts";
import { setupTestRepo } from "./utils/setup.ts"; import { setupTestRepo } from "./utils/setup.ts";
config(); config();
@@ -36,38 +35,12 @@ export async function run(
console.log(prompt); console.log(prompt);
console.log("─".repeat(50)); console.log("─".repeat(50));
const { EXPECTED_INPUTS } = await import("./main.ts"); const inputs: Inputs = {
EXPECTED_INPUTS.forEach((inputName) => {
const value = process.env[inputName];
if (value) {
process.env[`INPUT_${inputName.toLowerCase()}`] = value;
}
});
const inputs: any = {
prompt, prompt,
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "", anthropic_api_key: process.env.ANTHROPIC_API_KEY,
}; };
if (process.env.GITHUB_TOKEN) { const result = await main(inputs);
inputs.github_token = process.env.GITHUB_TOKEN;
}
console.log("🔑 Setting up GitHub installation token...");
const installationToken = await setupGitHubInstallationToken();
inputs.github_installation_token = installationToken;
console.log("✅ GitHub installation token setup successfully");
const envWithToken = {
...process.env,
GITHUB_INSTALLATION_TOKEN: installationToken,
} as Record<string, string>;
const result = await main({
inputs,
env: envWithToken,
cwd: process.cwd(),
});
process.chdir(originalCwd); process.chdir(originalCwd);
+293 -458
View File
File diff suppressed because it is too large Load Diff
+16 -15
View File
@@ -2,21 +2,22 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"module": "NodeNext", "module": "NodeNext",
"target": "ESNext", "target": "ESNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"lib": ["ESNext"], "lib": ["ESNext"],
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true, "rewriteRelativeImportExtensions": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"declaration": true, "declaration": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"exactOptionalPropertyTypes": true, "exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"stripInternal": true, "stripInternal": true,
"moduleDetection": "force" "moduleDetection": "force",
"useUnknownInCatchVariables": true
} }
} }
+23 -2
View File
@@ -1,6 +1,5 @@
import { createSign } from "node:crypto"; import { createSign } from "node:crypto";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { resolveRepoContext } from "./repo-context.ts";
export interface InstallationToken { export interface InstallationToken {
token: string; token: string;
@@ -208,7 +207,7 @@ const findInstallationId = async (
}; };
async function acquireTokenViaGitHubApp(): Promise<string> { async function acquireTokenViaGitHubApp(): Promise<string> {
const repoContext = resolveRepoContext(); const repoContext = parseRepoContext();
const config: GitHubAppConfig = { const config: GitHubAppConfig = {
appId: process.env.GITHUB_APP_ID!, appId: process.env.GITHUB_APP_ID!,
@@ -250,3 +249,25 @@ export async function setupGitHubInstallationToken(): Promise<string> {
return token; return token;
} }
export interface RepoContext {
owner: string;
name: string;
}
/**
* Parse repository context from GITHUB_REPOSITORY environment variable.
*/
export function parseRepoContext(): RepoContext {
const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) {
throw new Error("GITHUB_REPOSITORY environment variable is required");
}
const [owner, name] = githubRepo.split("/");
if (!owner || !name) {
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
}
return { owner, name };
}
-22
View File
@@ -1,22 +0,0 @@
export interface RepoContext {
owner: string;
name: string;
}
/**
* Resolve repository context from GITHUB_REPOSITORY environment variable.
* Throws if not available.
*/
export function resolveRepoContext(): RepoContext {
const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) {
throw new Error('GITHUB_REPOSITORY environment variable is required');
}
const [owner, name] = githubRepo.split('/');
if (!owner || !name) {
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
}
return { owner, name };
}