Compare commits

...

15 Commits

Author SHA1 Message Date
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
18 changed files with 444 additions and 27183 deletions
+2 -15
View File
@@ -23,6 +23,8 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -58,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: |
-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
+9 -8
View File
@@ -10,24 +10,23 @@ 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: "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"
cache-dependency-path: ${{ github.action_path }}/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
shell: bash
@@ -36,7 +35,9 @@ runs:
run: node entry.ts
shell: bash
working-directory: ${{ github.action_path }}
env:
INPUTS_JSON: ${{ toJSON(inputs) }}
branding:
icon: "code"
color: "orange"
color: "green"
+4 -2
View File
@@ -10,6 +10,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,
@@ -21,6 +22,7 @@ export class ClaudeAgent implements Agent {
throw new Error("Claude agent requires an API key");
}
this.apiKey = config.apiKey;
this.githubInstallationToken = config.githubInstallationToken;
}
/**
@@ -90,13 +92,13 @@ export class ClaudeAgent implements Agent {
"bypassPermissions",
];
if (!process.env.GITHUB_INSTALLATION_TOKEN) {
if (!this.githubInstallationToken) {
throw new Error(
"GITHUB_INSTALLATION_TOKEN is required for GitHub integration"
);
}
const mcpConfig = createMcpConfig(process.env.GITHUB_INSTALLATION_TOKEN);
const mcpConfig = createMcpConfig(this.githubInstallationToken);
console.log("📋 MCP Config:", mcpConfig);
args.push("--mcp-config", mcpConfig);
-26452
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
* 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 { type } from "arktype";
import { Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" };
import { setupGitHubInstallationToken } from "./utils/github.ts";
async function run(): Promise<void> {
try {
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) {
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 parsed = type("string.json.parse").assert(inputsJson);
const inputs = Inputs.assert(parsed);
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 result = await main(inputs);
if (!result.success) {
throw new Error(result.error || "Agent execution failed");
@@ -55,7 +32,4 @@ async function run(): Promise<void> {
}
}
run().catch((error) => {
console.error("Action failed:", error);
process.exit(1);
});
await run();
-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 = {
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 -2
View File
@@ -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";
+15 -27
View File
@@ -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,17 @@ 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);
+66
View File
@@ -0,0 +1,66 @@
import { Octokit } from "@octokit/rest";
import { type } from "arktype";
import { resolveRepoContext } from "../utils/repo-context.ts";
import { 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: async ({ issueNumber, body }: { issueNumber: number; body: string }) => {
try {
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
if (!githubInstallationToken) {
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
}
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,
};
}
},
});
-2
View File
@@ -1,8 +1,6 @@
/**
* 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("..");
+6 -84
View File
@@ -1,92 +1,14 @@
#!/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 { addTools } from "./shared.ts";
const server = new McpServer({
const server = new FastMCP({
name: "Minimal GitHub Issue Comment Server",
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]);
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();
});
}
await runServer();
server.start();
+11
View File
@@ -0,0 +1,11 @@
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { FastMCP, Tool } from "fastmcp";
export const tool = <const params>(tool: Tool<{}, StandardSchemaV1<params>>) => tool;
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
for (const tool of tools) {
server.addTool(tool);
}
return server;
};
+11 -22
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.38",
"version": "0.0.50",
"type": "module",
"files": [
"index.js",
@@ -12,40 +12,29 @@
"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"
},
"packageManager": "pnpm@10.18.2",
"dependencies": {
"@ark/fs": "0.49.0",
"@actions/core": "^1.11.1",
"@modelcontextprotocol/sdk": "^1.17.5",
"@ark/fs": "0.49.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",
"table": "^6.9.0",
"zod": "^3.24.4"
"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"
"typescript": "^5.9.3"
},
"repository": {
"type": "git",
@@ -53,7 +42,7 @@
},
"keywords": [],
"author": "",
"license": "ISC",
"license": "MIT",
"bugs": {
"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 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();
@@ -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);
+285 -453
View File
File diff suppressed because it is too large Load Diff
+16 -15
View File
@@ -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
}
}