Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0f31415a3 | |||
| 706ce04895 | |||
| 09be8e3068 | |||
| c6c1210fa0 | |||
| 0368512b9e | |||
| 9fb6135fd2 | |||
| bb78e5f94b | |||
| c668578c6f | |||
| 7f1566d9c2 | |||
| dd482566c2 | |||
| 57029c32a3 | |||
| 757d336475 | |||
| d03debab4b | |||
| a05829f781 | |||
| c8ba7940e3 | |||
| 710fdd0fa4 | |||
| 4f5ee28b8a | |||
| 806458b95a | |||
| 2c856e3337 | |||
| a93c34e61b | |||
| cd20491d22 | |||
| 1a6ce6728c | |||
| 3b39f2c8d8 | |||
| ec0eeb1d18 | |||
| 8ef805b9fc | |||
| 6e93fd9a72 | |||
| 9567d84442 | |||
| d79564db5e | |||
| a7a0e87fd8 | |||
| 7050b8de75 | |||
| 2fc3ddee16 | |||
| 284d9733dd | |||
| 94e2b5f6e0 | |||
| 03810d574e | |||
| f52e94c612 | |||
| 9444a0e208 | |||
| 2296060d04 | |||
| 458bfe18a0 | |||
| 4cfb9b5008 | |||
| 06542e382a | |||
| bcdf6ab5fb |
@@ -29,12 +29,12 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get package version
|
||||
id: version
|
||||
@@ -60,21 +60,6 @@ jobs:
|
||||
echo "✅ Tag ${{ steps.version.outputs.tag }} does not exist - will create release"
|
||||
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
|
||||
if: steps.check_tag.outputs.exists == 'false'
|
||||
run: |
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Build the action before committing
|
||||
echo "🔨 Building action..."
|
||||
npm run build
|
||||
|
||||
# Add the built files to the commit
|
||||
git add entry.cjs
|
||||
+26
-9
@@ -10,17 +10,34 @@ inputs:
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key for Claude Code authentication"
|
||||
required: false
|
||||
github_token:
|
||||
description: "GitHub token for repository access"
|
||||
required: false
|
||||
github_installation_token:
|
||||
description: "GitHub App installation token"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "node20"
|
||||
main: "entry.cjs"
|
||||
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: node entry.ts
|
||||
shell: bash
|
||||
working-directory: ${{ github.action_path }}
|
||||
env:
|
||||
INPUTS_JSON: ${{ toJSON(inputs) }}
|
||||
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "orange"
|
||||
color: "green"
|
||||
|
||||
+26
-17
@@ -3,6 +3,7 @@ import * as core from "@actions/core";
|
||||
import { createMcpConfig } from "../mcp/config.ts";
|
||||
import { spawn } from "../utils/subprocess.ts";
|
||||
import { boxString, tableString } from "../utils/table.ts";
|
||||
import { instructions } from "./shared.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 {
|
||||
private apiKey: string;
|
||||
private githubInstallationToken: string;
|
||||
public runStats = {
|
||||
toolsUsed: 0,
|
||||
turns: 0,
|
||||
@@ -17,10 +19,8 @@ export class ClaudeAgent implements Agent {
|
||||
};
|
||||
|
||||
constructor(config: AgentConfig) {
|
||||
if (!config.apiKey) {
|
||||
throw new Error("Claude agent requires an API key");
|
||||
}
|
||||
this.apiKey = config.apiKey;
|
||||
this.githubInstallationToken = config.githubInstallationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,28 +74,29 @@ export class ClaudeAgent implements Agent {
|
||||
|
||||
try {
|
||||
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" }));
|
||||
|
||||
const mcpConfig = createMcpConfig(this.githubInstallationToken);
|
||||
console.log("📋 MCP Config:", mcpConfig);
|
||||
|
||||
const args = [
|
||||
"--print",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--debug",
|
||||
"--permission-mode",
|
||||
"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");
|
||||
|
||||
this.runStats = {
|
||||
@@ -111,8 +112,9 @@ export class ClaudeAgent implements Agent {
|
||||
cmd: claudePath,
|
||||
args,
|
||||
env,
|
||||
input: prompt,
|
||||
input: `${instructions} ${prompt}`,
|
||||
timeout: 10 * 60 * 1000, // 10 minutes
|
||||
cwd: process.env.GITHUB_WORKSPACE || process.cwd(),
|
||||
onStdout: (_chunk) => {
|
||||
processJSONChunk(_chunk, this);
|
||||
},
|
||||
@@ -165,8 +167,15 @@ export class ClaudeAgent implements Agent {
|
||||
*/
|
||||
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
||||
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);
|
||||
const parsedChunk = JSON.parse(chunk.trim());
|
||||
const parsedChunk = JSON.parse(trimmedChunk);
|
||||
|
||||
switch (parsedChunk.type) {
|
||||
case "system":
|
||||
|
||||
@@ -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
@@ -29,6 +29,6 @@ export interface AgentResult {
|
||||
* Configuration for agent creation
|
||||
*/
|
||||
export interface AgentConfig {
|
||||
apiKey?: string;
|
||||
[key: string]: any;
|
||||
apiKey: string;
|
||||
githubInstallationToken: string;
|
||||
}
|
||||
|
||||
@@ -2,48 +2,26 @@
|
||||
|
||||
/**
|
||||
* 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 { type ExecutionInputs, type MainParams, main } from "./main.ts";
|
||||
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
||||
import { type } from "arktype";
|
||||
import { Inputs, main } from "./main.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const prompt = core.getInput("prompt", { required: true });
|
||||
const anthropic_api_key = core.getInput("anthropic_api_key");
|
||||
console.log(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
|
||||
if (!prompt) {
|
||||
throw new Error("prompt is required");
|
||||
const inputsJson = process.env.INPUTS_JSON;
|
||||
if (!inputsJson) {
|
||||
throw new Error("INPUTS_JSON environment variable not found");
|
||||
}
|
||||
|
||||
const inputs: ExecutionInputs = {
|
||||
prompt,
|
||||
anthropic_api_key,
|
||||
};
|
||||
|
||||
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN;
|
||||
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);
|
||||
const parsed = type("string.json.parse").assert(inputsJson);
|
||||
const inputs = Inputs.assert(parsed);
|
||||
|
||||
const result = await main(inputs);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Agent execution failed");
|
||||
@@ -54,7 +32,4 @@ async function run(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Action failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
await run();
|
||||
|
||||
@@ -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
@@ -1,13 +1,9 @@
|
||||
import type { MainParams } from "../main.ts";
|
||||
import type { Inputs } from "../main.ts";
|
||||
|
||||
const testParams = {
|
||||
inputs: {
|
||||
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.",
|
||||
anthropic_api_key: "sk-test-key",
|
||||
},
|
||||
env: {},
|
||||
cwd: process.cwd(),
|
||||
} satisfies MainParams;
|
||||
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.",
|
||||
anthropic_api_key: "sk-test-key",
|
||||
} satisfies Inputs;
|
||||
|
||||
export default testParams;
|
||||
|
||||
+1
-3
@@ -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.
|
||||
|
||||
Do not use the gh cli. If the mcp tool does not work, bail.
|
||||
create a PR implementing bogosort to https://github.com/pullfrogai/scratch/
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
export { ClaudeAgent } from "./agents/claude.ts";
|
||||
export type { Agent, AgentConfig, AgentResult } from "./agents/types.ts";
|
||||
export {
|
||||
type ExecutionInputs,
|
||||
type MainParams,
|
||||
type Inputs as ExecutionInputs,
|
||||
type MainResult,
|
||||
main,
|
||||
} from "./main.ts";
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import * as core from "@actions/core";
|
||||
import { type } from "arktype";
|
||||
import { ClaudeAgent } from "./agents/claude.ts";
|
||||
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
||||
|
||||
export const EXPECTED_INPUTS: string[] = [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GITHUB_TOKEN",
|
||||
"GITHUB_INSTALLATION_TOKEN",
|
||||
];
|
||||
export const Inputs = type({
|
||||
prompt: "string",
|
||||
"anthropic_api_key?": "string | undefined",
|
||||
});
|
||||
|
||||
export interface ExecutionInputs {
|
||||
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 type Inputs = typeof Inputs.infer;
|
||||
|
||||
export interface MainResult {
|
||||
success: boolean;
|
||||
@@ -26,19 +16,18 @@ export interface MainResult {
|
||||
error?: string | undefined;
|
||||
}
|
||||
|
||||
export async function main(params: MainParams): Promise<MainResult> {
|
||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
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`);
|
||||
|
||||
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();
|
||||
|
||||
const result = await agent.execute(inputs.prompt);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
+6
-2
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
const actionPath = fromHere("..");
|
||||
|
||||
export const mcpServerName = "gh-pullfrog";
|
||||
|
||||
export function createMcpConfig(githubInstallationToken: string) {
|
||||
const githubRepository = process.env.GITHUB_REPOSITORY;
|
||||
@@ -14,7 +18,7 @@ export function createMcpConfig(githubInstallationToken: string) {
|
||||
return JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
minimal_github_comment: {
|
||||
[mcpServerName]: {
|
||||
command: "node",
|
||||
args: [`${actionPath}/mcp/server.ts`],
|
||||
env: {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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
@@ -1,92 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
// Minimal GitHub Issue Comment MCP Server
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { type } from "arktype";
|
||||
import { z } from "zod";
|
||||
import { resolveRepoContext } from "../utils/repo-context.ts";
|
||||
import { FastMCP } from "fastmcp";
|
||||
import { CommentTool } from "./comment.ts";
|
||||
import { IssueTool } from "./issue.ts";
|
||||
import { PullRequestTool } from "./pr.ts";
|
||||
import { addTools } from "./shared.ts";
|
||||
|
||||
const server = new McpServer({
|
||||
name: "Minimal GitHub Issue Comment Server",
|
||||
const server = new FastMCP({
|
||||
name: "gh-pullfrog",
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
// Define the schema for creating issue comments
|
||||
const Comment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
body: type.string.describe("the comment body content"),
|
||||
});
|
||||
addTools(server, [CommentTool, IssueTool, PullRequestTool]);
|
||||
|
||||
server.tool(
|
||||
"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();
|
||||
});
|
||||
}
|
||||
|
||||
runServer().catch(console.error);
|
||||
server.start();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
+11
-19
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.56",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
@@ -12,38 +12,30 @@
|
||||
"main.js",
|
||||
"main.d.ts"
|
||||
],
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "node esbuild.config.js",
|
||||
"build:npm": "zshy",
|
||||
"build:dev": "node esbuild.config.js",
|
||||
"prepare": "husky",
|
||||
"play": "node play.ts",
|
||||
"upDeps": "pnpm up --latest",
|
||||
"createLockfile": "pnpm --ignore-workspace install"
|
||||
"lock": "pnpm --ignore-workspace install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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/webhooks-types": "^7.6.1",
|
||||
"arktype": "^2.1.22",
|
||||
"dotenv": "^17.2.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"arktype": "^2.1.23",
|
||||
"dotenv": "^17.2.3",
|
||||
"execa": "^9.6.0",
|
||||
"fastmcp": "^3.20.0",
|
||||
"table": "^6.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/node": "^24.7.2",
|
||||
"arg": "^5.0.2",
|
||||
"esbuild": "^0.25.9",
|
||||
"husky": "^9.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"zshy": "^0.4.1",
|
||||
"zod": "^3.24.4"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -51,7 +43,7 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pullfrog/action/issues"
|
||||
},
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, extname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { extname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { fromHere } from "@ark/fs";
|
||||
import arg from "arg";
|
||||
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 { runAct } from "./utils/act.ts";
|
||||
import { setupGitHubInstallationToken } from "./utils/github.ts";
|
||||
import { setupTestRepo } from "./utils/setup.ts";
|
||||
|
||||
config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export async function run(
|
||||
prompt: string,
|
||||
options: { act?: boolean } = {}
|
||||
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
|
||||
try {
|
||||
console.log(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
if (options.act) {
|
||||
console.log("🐳 Running with Docker/act...");
|
||||
runAct(prompt);
|
||||
@@ -36,38 +35,12 @@ export async function run(
|
||||
console.log(prompt);
|
||||
console.log("─".repeat(50));
|
||||
|
||||
const { EXPECTED_INPUTS } = await import("./main.ts");
|
||||
EXPECTED_INPUTS.forEach((inputName) => {
|
||||
const value = process.env[inputName];
|
||||
if (value) {
|
||||
process.env[`INPUT_${inputName.toLowerCase()}`] = value;
|
||||
}
|
||||
});
|
||||
|
||||
const inputs: any = {
|
||||
const inputs: Inputs = {
|
||||
prompt,
|
||||
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
|
||||
anthropic_api_key: process.env.ANTHROPIC_API_KEY,
|
||||
};
|
||||
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
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(),
|
||||
});
|
||||
const result = await main(inputs);
|
||||
|
||||
process.chdir(originalCwd);
|
||||
|
||||
@@ -129,7 +102,7 @@ Examples:
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
let resolvedPath: string;
|
||||
|
||||
const fixturesPath = join(__dirname, "fixtures", filePath);
|
||||
const fixturesPath = fromHere("fixtures", filePath);
|
||||
if (existsSync(fixturesPath)) {
|
||||
resolvedPath = fixturesPath;
|
||||
} else if (existsSync(filePath)) {
|
||||
|
||||
Generated
+295
-452
File diff suppressed because it is too large
Load Diff
+16
-15
@@ -2,21 +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,
|
||||
"moduleDetection": "force"
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -1,6 +1,5 @@
|
||||
import { createSign } from "node:crypto";
|
||||
import * as core from "@actions/core";
|
||||
import { resolveRepoContext } from "./repo-context.ts";
|
||||
|
||||
export interface InstallationToken {
|
||||
token: string;
|
||||
@@ -55,7 +54,7 @@ function isGitHubActionsEnvironment(): boolean {
|
||||
|
||||
async function acquireTokenViaOIDC(): Promise<string> {
|
||||
core.info("Generating OIDC token...");
|
||||
|
||||
|
||||
const oidcToken = await core.getIDToken("pullfrog-api");
|
||||
core.info("OIDC token generated successfully");
|
||||
|
||||
@@ -208,7 +207,7 @@ const findInstallationId = async (
|
||||
};
|
||||
|
||||
async function acquireTokenViaGitHubApp(): Promise<string> {
|
||||
const repoContext = resolveRepoContext();
|
||||
const repoContext = parseRepoContext();
|
||||
|
||||
const config: GitHubAppConfig = {
|
||||
appId: process.env.GITHUB_APP_ID!,
|
||||
@@ -244,9 +243,31 @@ export async function setupGitHubInstallationToken(): Promise<string> {
|
||||
}
|
||||
|
||||
const token = await acquireNewToken();
|
||||
|
||||
|
||||
core.setSecret(token);
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = 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 };
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
+3
-1
@@ -6,6 +6,7 @@ export interface SpawnOptions {
|
||||
env?: Record<string, string>;
|
||||
input?: string;
|
||||
timeout?: number;
|
||||
cwd?: string;
|
||||
onStdout?: (chunk: string) => void;
|
||||
onStderr?: (chunk: string) => void;
|
||||
}
|
||||
@@ -21,7 +22,7 @@ export interface SpawnResult {
|
||||
* Spawn a subprocess with streaming callbacks and buffered results
|
||||
*/
|
||||
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||
const { cmd, args, env, input, timeout, onStdout, onStderr } = options;
|
||||
const { cmd, args, env, input, timeout, cwd, onStdout, onStderr } = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
let stdoutBuffer = "";
|
||||
@@ -31,6 +32,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||
const child = nodeSpawn(cmd, args, {
|
||||
env: env ? { ...process.env, ...env } : process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
cwd: cwd || process.cwd(),
|
||||
});
|
||||
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
Reference in New Issue
Block a user