From 1abbd7ff416831ec6e2ceab6d61a7dcebfd0f683 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Thu, 28 Aug 2025 13:37:20 -0700 Subject: [PATCH] Refactor action with agent interface system and make anthropic_api_key optional - Created extensible agent interface with install() and execute() methods - Moved Claude Code logic to agents/claude.ts implementing Agent interface - Added utilities directory for reusable functions (exec, files) - Refactored index.ts to be minimal (35 lines) using agent abstraction - Made anthropic_api_key optional in action.yml - Updated Node.js imports to use node: prefix convention - Bumped version to 0.0.5 - Architecture now supports multiple agents (OpenAI, Gemini, etc.) --- action.yml | 17 ++-- agents/claude.ts | 78 +++++++++++++++++ agents/factory.ts | 16 ++++ agents/index.ts | 3 + agents/types.ts | 34 ++++++++ index.cjs | 211 +++++++++++++++++++++++++++++++++++----------- index.ts | 46 +++++++--- package.json | 2 +- utils/exec.ts | 15 ++++ utils/files.ts | 13 +++ 10 files changed, 364 insertions(+), 71 deletions(-) create mode 100644 agents/claude.ts create mode 100644 agents/factory.ts create mode 100644 agents/index.ts create mode 100644 agents/types.ts create mode 100644 utils/exec.ts create mode 100644 utils/files.ts diff --git a/action.yml b/action.yml index 1ebc70b..f98ed35 100644 --- a/action.yml +++ b/action.yml @@ -1,17 +1,20 @@ -name: 'Simple Message Action' -description: 'A simple GitHub Action that prints a message to the console' +name: 'Pullfrog Claude Code Action' +description: 'Execute Claude Code with a prompt using Anthropic API' author: 'Pullfrog' inputs: - message: - description: 'Message to print to console' + prompt: + description: 'Prompt to send to Claude Code' required: true - default: 'Hello from Pullfrog Action!' + default: 'Hello from Claude Code!' + anthropic_api_key: + description: 'Anthropic API key for Claude Code authentication' + required: false runs: using: 'node20' main: 'index.cjs' branding: - icon: 'message-circle' - color: 'blue' + icon: 'code' + color: 'orange' diff --git a/agents/claude.ts b/agents/claude.ts new file mode 100644 index 0000000..3393640 --- /dev/null +++ b/agents/claude.ts @@ -0,0 +1,78 @@ +import * as core from "@actions/core"; +import { executeCommand } from "../utils/exec"; +import { createTempFile } from "../utils/files"; +import type { Agent, AgentConfig, AgentResult } from "./types"; + +/** + * Claude Code agent implementation + */ +export class ClaudeAgent implements Agent { + private apiKey: string; + + constructor(config: AgentConfig) { + if (!config.apiKey) { + throw new Error("Claude agent requires an API key"); + } + this.apiKey = config.apiKey; + } + + /** + * Install Claude Code CLI + */ + async install(): Promise { + core.info("Installing Claude Code..."); + try { + await executeCommand("curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"); + core.info("Claude Code installed successfully"); + } catch (error) { + throw new Error(`Failed to install Claude Code: ${error}`); + } + } + + /** + * Execute Claude Code with the given prompt + */ + async execute(prompt: string): Promise { + core.info("Executing Claude Code..."); + + try { + // Create a temporary file for the prompt + const promptFile = createTempFile(prompt, "prompt.txt"); + + // Execute Claude Code with the prompt + const command = `$HOME/.local/bin/claude --dangerously-skip-permissions "${promptFile}"`; + core.info(`Executing: ${command}`); + + const { stdout, stderr } = await executeCommand(command, { + ANTHROPIC_API_KEY: this.apiKey, + }); + + if (stderr) { + core.warning(`Claude Code stderr: ${stderr}`); + } + + if (stdout) { + core.info("Claude Code output:"); + console.log(stdout); + } + + core.info("Claude Code executed successfully"); + + return { + success: true, + output: stdout, + error: stderr || undefined, + metadata: { + promptFile, + command, + }, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return { + success: false, + error: `Failed to execute Claude Code: ${errorMessage}`, + }; + } + } +} diff --git a/agents/factory.ts b/agents/factory.ts new file mode 100644 index 0000000..0f76b18 --- /dev/null +++ b/agents/factory.ts @@ -0,0 +1,16 @@ +import { ClaudeAgent } from "./claude"; +import type { Agent, AgentConfig } from "./types"; + +export type AgentType = "claude"; + +/** + * Factory for creating agent instances + */ +export function createAgent(type: AgentType, config: AgentConfig): Agent { + switch (type) { + case "claude": + return new ClaudeAgent(config); + default: + throw new Error(`Unsupported agent type: ${type}`); + } +} diff --git a/agents/index.ts b/agents/index.ts new file mode 100644 index 0000000..b8180ed --- /dev/null +++ b/agents/index.ts @@ -0,0 +1,3 @@ +export * from "./claude"; +export * from "./factory"; +export * from "./types"; diff --git a/agents/types.ts b/agents/types.ts new file mode 100644 index 0000000..c33b33f --- /dev/null +++ b/agents/types.ts @@ -0,0 +1,34 @@ +/** + * Standard interface for all Pullfrog agents + */ +export interface Agent { + /** + * Install the agent and any required dependencies + */ + install(): Promise; + + /** + * Execute the agent with the given prompt + * @param prompt The prompt to send to the agent + * @param options Additional options specific to the agent + */ + execute(prompt: string, options?: Record): Promise; +} + +/** + * Result returned by agent execution + */ +export interface AgentResult { + success: boolean; + output?: string; + error?: string; + metadata?: Record; +} + +/** + * Configuration for agent creation + */ +export interface AgentConfig { + apiKey?: string; + [key: string]: any; +} diff --git a/index.cjs b/index.cjs index ecfeaff..8de35e7 100644 --- a/index.cjs +++ b/index.cjs @@ -10746,7 +10746,7 @@ var require_mock_interceptor = __commonJS({ var require_mock_client = __commonJS({ "node_modules/.pnpm/undici@5.29.0/node_modules/undici/lib/mock/mock-client.js"(exports2, module2) { "use strict"; - var { promisify } = require("util"); + var { promisify: promisify2 } = require("util"); var Client = require_client(); var { buildMockDispatch } = require_mock_utils(); var { @@ -10786,7 +10786,7 @@ var require_mock_client = __commonJS({ return new MockInterceptor(opts, this[kDispatches]); } async [kClose]() { - await promisify(this[kOriginalClose])(); + await promisify2(this[kOriginalClose])(); this[kConnected] = 0; this[kMockAgent][Symbols.kClients].delete(this[kOrigin]); } @@ -10799,7 +10799,7 @@ var require_mock_client = __commonJS({ var require_mock_pool = __commonJS({ "node_modules/.pnpm/undici@5.29.0/node_modules/undici/lib/mock/mock-pool.js"(exports2, module2) { "use strict"; - var { promisify } = require("util"); + var { promisify: promisify2 } = require("util"); var Pool = require_pool(); var { buildMockDispatch } = require_mock_utils(); var { @@ -10839,7 +10839,7 @@ var require_mock_pool = __commonJS({ return new MockInterceptor(opts, this[kDispatches]); } async [kClose]() { - await promisify(this[kOriginalClose])(); + await promisify2(this[kOriginalClose])(); this[kConnected] = 0; this[kMockAgent][Symbols.kClients].delete(this[kOrigin]); } @@ -17581,12 +17581,12 @@ var require_lib = __commonJS({ throw new Error("Client has already been disposed."); } const parsedUrl = new URL(requestUrl); - let info2 = this._prepareRequest(verb, parsedUrl, headers); + let info3 = this._prepareRequest(verb, parsedUrl, headers); const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) ? this._maxRetries + 1 : 1; let numTries = 0; let response; do { - response = yield this.requestRaw(info2, data); + response = yield this.requestRaw(info3, data); if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { let authenticationHandler; for (const handler of this.handlers) { @@ -17596,7 +17596,7 @@ var require_lib = __commonJS({ } } if (authenticationHandler) { - return authenticationHandler.handleAuthentication(this, info2, data); + return authenticationHandler.handleAuthentication(this, info3, data); } else { return response; } @@ -17619,8 +17619,8 @@ var require_lib = __commonJS({ } } } - info2 = this._prepareRequest(verb, parsedRedirectUrl, headers); - response = yield this.requestRaw(info2, data); + info3 = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = yield this.requestRaw(info3, data); redirectsRemaining--; } if (!response.message.statusCode || !HttpResponseRetryCodes.includes(response.message.statusCode)) { @@ -17649,7 +17649,7 @@ var require_lib = __commonJS({ * @param info * @param data */ - requestRaw(info2, data) { + requestRaw(info3, data) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { function callbackForResult(err, res) { @@ -17661,7 +17661,7 @@ var require_lib = __commonJS({ resolve(res); } } - this.requestRawWithCallback(info2, data, callbackForResult); + this.requestRawWithCallback(info3, data, callbackForResult); }); }); } @@ -17671,12 +17671,12 @@ var require_lib = __commonJS({ * @param data * @param onResult */ - requestRawWithCallback(info2, data, onResult) { + requestRawWithCallback(info3, data, onResult) { if (typeof data === "string") { - if (!info2.options.headers) { - info2.options.headers = {}; + if (!info3.options.headers) { + info3.options.headers = {}; } - info2.options.headers["Content-Length"] = Buffer.byteLength(data, "utf8"); + info3.options.headers["Content-Length"] = Buffer.byteLength(data, "utf8"); } let callbackCalled = false; function handleResult(err, res) { @@ -17685,7 +17685,7 @@ var require_lib = __commonJS({ onResult(err, res); } } - const req = info2.httpModule.request(info2.options, (msg) => { + const req = info3.httpModule.request(info3.options, (msg) => { const res = new HttpClientResponse(msg); handleResult(void 0, res); }); @@ -17697,7 +17697,7 @@ var require_lib = __commonJS({ if (socket) { socket.end(); } - handleResult(new Error(`Request timeout: ${info2.options.path}`)); + handleResult(new Error(`Request timeout: ${info3.options.path}`)); }); req.on("error", function(err) { handleResult(err); @@ -17733,27 +17733,27 @@ var require_lib = __commonJS({ return this._getProxyAgentDispatcher(parsedUrl, proxyUrl); } _prepareRequest(method, requestUrl, headers) { - const info2 = {}; - info2.parsedUrl = requestUrl; - const usingSsl = info2.parsedUrl.protocol === "https:"; - info2.httpModule = usingSsl ? https : http; + const info3 = {}; + info3.parsedUrl = requestUrl; + const usingSsl = info3.parsedUrl.protocol === "https:"; + info3.httpModule = usingSsl ? https : http; const defaultPort = usingSsl ? 443 : 80; - info2.options = {}; - info2.options.host = info2.parsedUrl.hostname; - info2.options.port = info2.parsedUrl.port ? parseInt(info2.parsedUrl.port) : defaultPort; - info2.options.path = (info2.parsedUrl.pathname || "") + (info2.parsedUrl.search || ""); - info2.options.method = method; - info2.options.headers = this._mergeHeaders(headers); + info3.options = {}; + info3.options.host = info3.parsedUrl.hostname; + info3.options.port = info3.parsedUrl.port ? parseInt(info3.parsedUrl.port) : defaultPort; + info3.options.path = (info3.parsedUrl.pathname || "") + (info3.parsedUrl.search || ""); + info3.options.method = method; + info3.options.headers = this._mergeHeaders(headers); if (this.userAgent != null) { - info2.options.headers["user-agent"] = this.userAgent; + info3.options.headers["user-agent"] = this.userAgent; } - info2.options.agent = this._getAgent(info2.parsedUrl); + info3.options.agent = this._getAgent(info3.parsedUrl); if (this.handlers) { for (const handler of this.handlers) { - handler.prepareRequest(info2.options); + handler.prepareRequest(info3.options); } } - return info2; + return info3; } _mergeHeaders(headers) { if (this.requestOptions && this.requestOptions.headers) { @@ -19411,7 +19411,7 @@ var require_exec = __commonJS({ exports2.getExecOutput = exports2.exec = void 0; var string_decoder_1 = require("string_decoder"); var tr = __importStar(require_toolrunner()); - function exec(commandLine, args, options) { + function exec2(commandLine, args, options) { return __awaiter(this, void 0, void 0, function* () { const commandArgs = tr.argStringToArray(commandLine); if (commandArgs.length === 0) { @@ -19423,7 +19423,7 @@ var require_exec = __commonJS({ return runner.exec(); }); } - exports2.exec = exec; + exports2.exec = exec2; function getExecOutput(commandLine, args, options) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { @@ -19446,7 +19446,7 @@ var require_exec = __commonJS({ } }; const listeners = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.listeners), { stdout: stdOutListener, stderr: stdErrListener }); - const exitCode = yield exec(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); + const exitCode = yield exec2(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); stdout += stdoutDecoder.end(); stderr += stderrDecoder.end(); return { @@ -19524,12 +19524,12 @@ var require_platform = __commonJS({ Object.defineProperty(exports2, "__esModule", { value: true }); exports2.getDetails = exports2.isLinux = exports2.isMacOS = exports2.isWindows = exports2.arch = exports2.platform = void 0; var os_1 = __importDefault(require("os")); - var exec = __importStar(require_exec()); + var exec2 = __importStar(require_exec()); var getWindowsInfo = () => __awaiter(void 0, void 0, void 0, function* () { - const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', void 0, { + const { stdout: version } = yield exec2.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', void 0, { silent: true }); - const { stdout: name } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', void 0, { + const { stdout: name } = yield exec2.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', void 0, { silent: true }); return { @@ -19539,7 +19539,7 @@ var require_platform = __commonJS({ }); var getMacOsInfo = () => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d; - const { stdout } = yield exec.getExecOutput("sw_vers", void 0, { + const { stdout } = yield exec2.getExecOutput("sw_vers", void 0, { silent: true }); const version = (_b = (_a = stdout.match(/ProductVersion:\s*(.+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ""; @@ -19550,7 +19550,7 @@ var require_platform = __commonJS({ }; }); var getLinuxInfo = () => __awaiter(void 0, void 0, void 0, function* () { - const { stdout } = yield exec.getExecOutput("lsb_release", ["-i", "-r", "-s"], { + const { stdout } = yield exec2.getExecOutput("lsb_release", ["-i", "-r", "-s"], { silent: true }); const [name, version] = stdout.trim().split("\n"); @@ -19735,18 +19735,18 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); (0, command_1.issueCommand)("error", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); } exports2.error = error; - function warning(message, properties = {}) { + function warning2(message, properties = {}) { (0, command_1.issueCommand)("warning", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); } - exports2.warning = warning; + exports2.warning = warning2; function notice(message, properties = {}) { (0, command_1.issueCommand)("notice", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); } exports2.notice = notice; - function info2(message) { + function info3(message) { process.stdout.write(message + os.EOL); } - exports2.info = info2; + exports2.info = info3; function startGroup(name) { (0, command_1.issue)("group", name); } @@ -19809,16 +19809,125 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); }); // index.ts +var core2 = __toESM(require_core(), 1); + +// agents/claude.ts var core = __toESM(require_core(), 1); -try { - const message = core.getInput("message") || "Hello from Pullfrog Action!"; - console.log(`\u{1F438} Pullfrog says: ${message}`); - core.info(`Action executed successfully with message: ${message}`); - core.setOutput("message", message); -} catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; - core.setFailed(`Action failed: ${errorMessage}`); + +// utils/exec.ts +var import_node_child_process = require("node:child_process"); +var import_node_util = require("node:util"); +var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec); +async function executeCommand(command, env) { + const execEnv = env ? { ...process.env, ...env } : process.env; + return execAsync(command, { env: execEnv }); } + +// utils/files.ts +var import_node_fs = require("node:fs"); +var import_node_os = require("node:os"); +var import_node_path = require("node:path"); +function createTempFile(content, filename = "temp.txt") { + const tempDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "pullfrog-")); + const filePath = (0, import_node_path.join)(tempDir, filename); + (0, import_node_fs.writeFileSync)(filePath, content); + return filePath; +} + +// agents/claude.ts +var ClaudeAgent = class { + constructor(config) { + if (!config.apiKey) { + throw new Error("Claude agent requires an API key"); + } + this.apiKey = config.apiKey; + } + /** + * Install Claude Code CLI + */ + async install() { + core.info("Installing Claude Code..."); + try { + await executeCommand("curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"); + core.info("Claude Code installed successfully"); + } catch (error) { + throw new Error(`Failed to install Claude Code: ${error}`); + } + } + /** + * Execute Claude Code with the given prompt + */ + async execute(prompt) { + core.info("Executing Claude Code..."); + try { + const promptFile = createTempFile(prompt, "prompt.txt"); + const command = `$HOME/.local/bin/claude --dangerously-skip-permissions "${promptFile}"`; + core.info(`Executing: ${command}`); + const { stdout, stderr } = await executeCommand(command, { + ANTHROPIC_API_KEY: this.apiKey + }); + if (stderr) { + core.warning(`Claude Code stderr: ${stderr}`); + } + if (stdout) { + core.info("Claude Code output:"); + console.log(stdout); + } + core.info("Claude Code executed successfully"); + return { + success: true, + output: stdout, + error: stderr || void 0, + metadata: { + promptFile, + command + } + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return { + success: false, + error: `Failed to execute Claude Code: ${errorMessage}` + }; + } + } +}; + +// agents/factory.ts +function createAgent(type, config) { + switch (type) { + case "claude": + return new ClaudeAgent(config); + default: + throw new Error(`Unsupported agent type: ${type}`); + } +} + +// index.ts +async function main() { + try { + const prompt = core2.getInput("prompt", { required: true }); + const anthropicApiKey = core2.getInput("anthropic_api_key", { required: true }); + if (!anthropicApiKey) { + throw new Error("anthropic_api_key is required"); + } + core2.info(`\u{1F438} Pullfrog Claude Code Action starting...`); + core2.info(`Prompt: ${prompt}`); + const agent = createAgent("claude", { apiKey: anthropicApiKey }); + await agent.install(); + const result = await agent.execute(prompt); + if (!result.success) { + throw new Error(result.error || "Agent execution failed"); + } + core2.setOutput("status", "success"); + core2.setOutput("prompt", prompt); + core2.setOutput("output", result.output || ""); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + core2.setFailed(`Action failed: ${errorMessage}`); + } +} +main(); /*! Bundled license information: undici/lib/fetch/body.js: diff --git a/index.ts b/index.ts index b935c5f..0c5df53 100644 --- a/index.ts +++ b/index.ts @@ -1,17 +1,39 @@ import * as core from "@actions/core"; +import { createAgent } from "./agents/factory"; -try { - // Get the message input parameter, with a default fallback - const message = core.getInput("message") || "Hello from Pullfrog Action!"; +async function main(): Promise { + try { + // Get inputs + const prompt = core.getInput("prompt", { required: true }); + const anthropicApiKey = core.getInput("anthropic_api_key", { required: true }); - // Print the message to console and GitHub Actions logs - console.log(`🐸 Pullfrog says: ${message}`); - core.info(`Action executed successfully with message: ${message}`); + if (!anthropicApiKey) { + throw new Error("anthropic_api_key is required"); + } - // Set an output for potential use by other actions - core.setOutput("message", message); -} catch (error) { - // Handle any errors and fail the action - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; - core.setFailed(`Action failed: ${errorMessage}`); + core.info(`🐸 Pullfrog Claude Code Action starting...`); + core.info(`Prompt: ${prompt}`); + + // Create and install the Claude agent + const agent = createAgent("claude", { apiKey: anthropicApiKey }); + await agent.install(); + + // Execute the agent with the prompt + const result = await agent.execute(prompt); + + if (!result.success) { + throw new Error(result.error || "Agent execution failed"); + } + + // Set outputs + core.setOutput("status", "success"); + core.setOutput("prompt", prompt); + core.setOutput("output", result.output || ""); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + core.setFailed(`Action failed: ${errorMessage}`); + } } + +// Execute main function +main(); diff --git a/package.json b/package.json index d5498c4..9b1d51e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "action", - "version": "0.0.4", + "version": "0.0.5", "main": "index.js", "directories": { "example": "examples" diff --git a/utils/exec.ts b/utils/exec.ts new file mode 100644 index 0000000..f2b63d2 --- /dev/null +++ b/utils/exec.ts @@ -0,0 +1,15 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; + +export const execAsync = promisify(exec); + +/** + * Execute a shell command with optional environment variables + */ +export async function executeCommand( + command: string, + env?: Record +): Promise<{ stdout: string; stderr: string }> { + const execEnv = env ? { ...process.env, ...env } : process.env; + return execAsync(command, { env: execEnv }); +} diff --git a/utils/files.ts b/utils/files.ts new file mode 100644 index 0000000..e984d46 --- /dev/null +++ b/utils/files.ts @@ -0,0 +1,13 @@ +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +/** + * Create a temporary file with the given content + */ +export function createTempFile(content: string, filename = "temp.txt"): string { + const tempDir = mkdtempSync(join(tmpdir(), "pullfrog-")); + const filePath = join(tempDir, filename); + writeFileSync(filePath, content); + return filePath; +}