Compare commits

...

12 Commits

Author SHA1 Message Date
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
15 changed files with 143308 additions and 260 deletions
+1 -4
View File
@@ -1,6 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Ensure lockfile is up to date # Ensure lockfile is up to date
echo "🔒 Updating lockfile..." echo "🔒 Updating lockfile..."
pnpm install --lockfile-only pnpm install --lockfile-only
@@ -10,4 +7,4 @@ echo "🔨 Building action..."
pnpm build pnpm build
# Add the built files and lockfile to the commit # Add the built files and lockfile to the commit
git add entry.cjs pnpm-lock.yaml git add entry.js mcp-server.js pnpm-lock.yaml
+1 -1
View File
@@ -13,7 +13,7 @@ inputs:
runs: runs:
using: "node20" using: "node20"
main: "entry.cjs" main: "entry.js"
branding: branding:
icon: "code" icon: "code"
+100 -1
View File
@@ -1,16 +1,113 @@
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 { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts"; import { log } from "../utils/cli.ts";
import { type Agent, instructions } from "./shared.ts"; import { type Agent, instructions } from "./shared.ts";
let cachedCliPath: string | undefined;
export const claude: Agent = { 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
const fileStream = createWriteStream(tarballPath);
await pipeline(response.body as any, 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 }) => { run: async ({ prompt, mcpServers, apiKey }) => {
process.env.ANTHROPIC_API_KEY = apiKey; process.env.ANTHROPIC_API_KEY = apiKey;
if (!cachedCliPath) {
throw new Error("Claude CLI not installed. Call install() before run().");
}
const queryInstance = query({ const queryInstance = query({
prompt: `${instructions}\n\n${prompt}`, prompt: `${instructions}\n\n****** USER PROMPT ******\n${prompt}`,
options: { options: {
permissionMode: "bypassPermissions", permissionMode: "bypassPermissions",
mcpServers, mcpServers,
pathToClaudeCodeExecutable: cachedCliPath,
}, },
}); });
@@ -142,4 +239,6 @@ const messageHandlers: SDKMessageHandlers = {
}, },
system: () => {}, system: () => {},
stream_event: () => {}, stream_event: () => {},
tool_progress: () => {},
auth_status: () => {},
}; };
+1
View File
@@ -22,6 +22,7 @@ export interface AgentConfig {
} }
export type Agent = { export type Agent = {
install: () => Promise<string>;
run: (config: AgentConfig) => Promise<AgentResult>; run: (config: AgentConfig) => Promise<AgentResult>;
}; };
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env node
/**
* Build entry point - wraps entry.ts to avoid top-level await in CJS output
*/
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 { log } from "./utils/cli.ts";
async function run(): Promise<void> {
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 result = await main(inputs);
if (!result.success) {
throw new Error(result.error || "Agent execution failed");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
core.setFailed(`Action failed: ${errorMessage}`);
}
}
// Wrap in IIFE to avoid top-level await in CJS
(async () => {
await run();
})();
-183
View File
File diff suppressed because one or more lines are too long
Executable
+41412
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 * as core from "@actions/core";
import { type } from "arktype"; import { type Inputs, main } from "./main.ts";
import { Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts"; import { log } from "./utils/cli.ts";
async function run(): Promise<void> { 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 { try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`); const inputs: Inputs = {
prompt: core.getInput("prompt", { required: true }),
const inputsJson = process.env.INPUTS_JSON; anthropic_api_key: core.getInput("anthropic_api_key") || undefined,
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 result = await main(inputs); const result = await main(inputs);
+29 -10
View File
@@ -1,23 +1,42 @@
import { build } from "esbuild"; import { build } from "esbuild";
// Build the GitHub Action bundle only const sharedConfig = {
// For npm package builds, use zshy (pnpm build:npm)
await build({
entryPoints: ["./entry.build.ts"],
bundle: true, bundle: true,
outfile: "./entry.cjs", format: "esm",
format: "cjs",
platform: "node", platform: "node",
target: "node20", target: "node20",
minify: true, minify: false,
sourcemap: false, sourcemap: false,
// @actions/core is provided by GitHub Actions runtime, but we still need it bundled // Bundle all dependencies - GitHub Actions doesn't have node_modules
// for local testing. However, we can mark it to reduce duplication if needed. // Only mark optional peer dependencies as external
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 // Enable tree-shaking to remove unused code
treeShaking: true, treeShaking: true,
// Drop console statements in production (but keep for debugging) // Drop console statements in production (but keep for debugging)
drop: [], 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!"); console.log("✅ Build completed successfully!");
+5 -1
View File
@@ -1,6 +1,7 @@
import { type } from "arktype"; import { type } from "arktype";
import { claude } from "./agents/claude.ts"; import { claude } from "./agents/claude.ts";
import { createMcpConfigs } from "./mcp/config.ts"; import { createMcpConfigs } from "./mcp/config.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts"; import { log } from "./utils/cli.ts";
import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts"; import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts"; import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
@@ -22,7 +23,7 @@ export type PromptJSON = {};
export async function main(inputs: Inputs): Promise<MainResult> { export async function main(inputs: Inputs): Promise<MainResult> {
try { try {
log.info("Starting agent run..."); log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
setupGitConfig(); setupGitConfig();
@@ -35,6 +36,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`); log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
// Install Claude CLI before running
await claude.install();
log.info("Running Claude Agent SDK..."); log.info("Running Claude Agent SDK...");
log.box(inputs.prompt, { title: "Prompt" }); log.box(inputs.prompt, { title: "Prompt" });
Executable
+101715
View File
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -6,8 +6,6 @@ import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs"; import { fromHere } from "@ark/fs";
import { parseRepoContext } from "../utils/github.ts"; import { parseRepoContext } from "../utils/github.ts";
const actionPath = fromHere("..");
export const ghPullfrogMcpName = "gh-pullfrog"; export const ghPullfrogMcpName = "gh-pullfrog";
export type McpName = typeof ghPullfrogMcpName; export type McpName = typeof ghPullfrogMcpName;
@@ -18,10 +16,14 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`; const githubRepository = `${repoContext.owner}/${repoContext.name}`;
const serverPath = process.env.GITHUB_ACTION_PATH
? `${process.env.GITHUB_ACTION_PATH}/mcp-server.js`
: fromHere("server.ts");
return { return {
[ghPullfrogMcpName]: { [ghPullfrogMcpName]: {
command: "node", command: "node",
args: [`${actionPath}/mcp/server.ts`], args: [serverPath],
env: { env: {
GITHUB_INSTALLATION_TOKEN: githubInstallationToken, GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository, GITHUB_REPOSITORY: githubRepository,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.74", "version": "0.0.85",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
-3
View File
@@ -5,7 +5,6 @@ import { fromHere } from "@ark/fs";
import arg from "arg"; import arg from "arg";
import { config } from "dotenv"; import { config } from "dotenv";
import { type Inputs, main } from "./main.ts"; import { type Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts"; import { log } from "./utils/cli.ts";
import { setupTestRepo } from "./utils/setup.ts"; import { setupTestRepo } from "./utils/setup.ts";
@@ -15,8 +14,6 @@ export async function run(
prompt: string prompt: string
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> { ): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
try { try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
const tempDir = join(process.cwd(), ".temp"); const tempDir = join(process.cwd(), ".temp");
setupTestRepo({ tempDir, forceClean: true }); setupTestRepo({ tempDir, forceClean: true });
+25 -2
View File
@@ -41,17 +41,40 @@ export function setupTestRepo(options: SetupOptions): void {
/** /**
* Setup git configuration to avoid identity errors * Setup git configuration to avoid identity errors
* Only runs in GitHub Actions environment to avoid overwriting local git config
*/ */
export function setupGitConfig(): void { 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..."); log.info("🔧 Setting up git configuration...");
execSync('git config user.email "action@pullfrog.ai"', { stdio: "inherit" }); try {
execSync('git config user.name "Pullfrog Action"', { stdio: "inherit" }); 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 * 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 { 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..."); log.info("🔐 Setting up git authentication...");
// Remove existing git auth headers that actions/checkout might have set // Remove existing git auth headers that actions/checkout might have set