diff --git a/agents/claude.ts b/agents/claude.ts index 5e00268..c85c14b 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -1,5 +1,6 @@ import { access, constants } from "node:fs/promises"; import * as core from "@actions/core"; +import { query } from "@anthropic-ai/claude-agent-sdk"; import { createMcpConfig } from "../mcp/config.ts"; import { debugLog, isDebug } from "../utils/logging.ts"; import { spawn } from "../utils/subprocess.ts"; @@ -68,187 +69,72 @@ export class ClaudeAgent implements Agent { } /** - * Execute Claude Code with the given prompt + * Execute Claude Code with the given prompt using the SDK */ async execute(prompt: string): Promise { - core.info("Running Claude Code..."); + core.info("Running Claude Agent SDK..."); - try { - const claudePath = `${process.env.HOME}/.local/bin/claude`; + console.log(boxString(prompt, { title: "Prompt" })); - const env = { - ANTHROPIC_API_KEY: this.apiKey, - ...(isDebug() && { LOG_LEVEL: "debug" }), - }; + const mcpConfig = JSON.parse(createMcpConfig(this.githubInstallationToken)); - console.log(boxString(prompt, { title: "Prompt" })); - - const mcpConfig = createMcpConfig(this.githubInstallationToken); - - if (isDebug()) { - debugLog(`📋 MCP Config: ${mcpConfig}`); - } - - const args = [ - "--print", - "--output-format", - "stream-json", - "--verbose", - "--permission-mode", - "bypassPermissions", - "--mcp-config", - mcpConfig, - ...(isDebug() ? ["--debug"] : []), - ]; - - core.startGroup("🔄 Run details"); - - this.runStats = { - toolsUsed: 0, - turns: 0, - startTime: Date.now(), - }; - - const finalResult = ""; - const totalCost = 0; - - const result = await spawn({ - cmd: claudePath, - args, - env, - input: `${instructions} ${prompt}`, - timeout: 10 * 60 * 1000, // 10 minutes - onStdout: (_chunk) => { - if (_chunk.trim()) { - processJSONChunk(_chunk, this); - } - }, - onStderr: (_chunk) => { - if (_chunk.trim()) { - processJSONChunk(_chunk, this); - } - }, - }); - - if (result.exitCode !== 0) { - throw new Error( - `Command failed with exit code ${result.exitCode}\n\nStdout: ${result.stdout}\n\nStderr: ${result.stderr}` - ); - } - - const duration = Date.now() - this.runStats.startTime; - core.info( - `📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration` - ); - - core.info("✅ Task complete."); - core.endGroup(); // End the collapsible log group - - return { - success: true, - output: finalResult, - metadata: { - promptLength: prompt.length, - exitCode: result.exitCode, - durationMs: result.durationMs, - totalCost, - }, - }; - } catch (error: any) { - try { - core.endGroup(); - } catch {} - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - return { - success: false, - error: `Failed to execute Claude Code: ${errorMessage}`, - }; - } - } -} - -/** - * Pretty print a JSON chunk based on its type - */ -function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { - try { - const trimmedChunk = chunk.trim(); - if (trimmedChunk.startsWith("[DEBUG]") || trimmedChunk.startsWith("[debug]")) { - console.log(chunk); - return; + if (isDebug()) { + debugLog(`📋 MCP Config: ${JSON.stringify(mcpConfig, null, 2)}`); } - if (trimmedChunk.startsWith("[ERROR]") || trimmedChunk.startsWith("[error]")) { - console.error(chunk); - return; - } + core.startGroup("🔄 Run details"); - debugLog(trimmedChunk); + this.runStats = { + toolsUsed: 0, + turns: 0, + startTime: Date.now(), + }; - const parsedChunk = JSON.parse(trimmedChunk); + let finalOutput = ""; - switch (parsedChunk.type) { - case "system": - if (parsedChunk.subtype === "init") { - core.info(`🚀 Starting Claude Code session...`); - core.info( - tableString([ - ["model", parsedChunk.model], - ["cwd", parsedChunk.cwd], - ["permission_mode", parsedChunk.permissionMode], - ["tools", parsedChunk.tools?.length ? `${parsedChunk.tools.length} tools` : "none"], - [ - "mcp_servers", - parsedChunk.mcp_servers?.length - ? `${parsedChunk.mcp_servers.length} servers` - : "none", - ], - [ - "slash_commands", - parsedChunk.slash_commands?.length - ? `${parsedChunk.slash_commands.length} commands` - : "none", - ], - ]) - ); - } - break; + // Initialize session + core.info(`🚀 Starting Claude Agent SDK session...`); - case "assistant": - if (parsedChunk.message?.content) { - if (agent) { - agent.runStats.turns++; - } + // Set API key environment variable for SDK + process.env.ANTHROPIC_API_KEY = this.apiKey; - for (const content of parsedChunk.message.content) { - if (content.type === "text") { - if (content.text.trim()) { - core.info(boxString(content.text.trim(), { title: "Claude Code" })); - } + // Create the query with SDK options + const queryInstance = query({ + prompt: `${instructions}\n\n${prompt}`, + options: { + permissionMode: "bypassPermissions", + mcpServers: mcpConfig.mcpServers, + }, + }); + + // Stream the results + for await (const message of queryInstance) { + if (message.type === "assistant") { + this.runStats.turns++; + + // Handle assistant messages with content + if (message.message?.content) { + for (const content of message.message.content) { + if (content.type === "text" && content.text?.trim()) { + core.info(boxString(content.text.trim(), { title: "Claude" })); + finalOutput += content.text + "\n"; } else if (content.type === "tool_use") { - if (agent) { - agent.runStats.toolsUsed++; - } - + this.runStats.toolsUsed++; const toolName = content.name; - core.info(`→ ${toolName}`); if (content.input) { - const input = content.input; + const input = content.input as any; if (input.description) { core.info(` └─ ${input.description}`); } - if (input.command) { core.info(` └─ command: ${input.command}`); } - if (input.file_path) { core.info(` └─ file: ${input.file_path}`); } - if (input.content) { const contentPreview = input.content.length > 100 @@ -256,19 +142,15 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { : input.content; core.info(` └─ content: ${contentPreview}`); } - if (input.query) { core.info(` └─ query: ${input.query}`); } - if (input.pattern) { core.info(` └─ pattern: ${input.pattern}`); } - if (input.url) { core.info(` └─ url: ${input.url}`); } - if (input.edits && Array.isArray(input.edits)) { core.info(` └─ edits: ${input.edits.length} changes`); input.edits.forEach((edit: any, index: number) => { @@ -277,11 +159,9 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { } }); } - if (input.task) { core.info(` └─ task: ${input.task}`); } - if (input.bash_command) { core.info(` └─ bash_command: ${input.bash_command}`); } @@ -289,43 +169,40 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void { } } } - break; - - case "user": - if (parsedChunk.message?.content) { - for (const content of parsedChunk.message.content) { - if (content.type === "tool_result") { - if (content.is_error) { - core.warning(`❌ Tool error: ${content.content}`); - } else { - } + } else if (message.type === "user") { + // Handle tool results + if (message.message?.content) { + for (const content of message.message.content) { + if (content.type === "tool_result" && content.is_error) { + core.warning(`❌ Tool error: ${content.content}`); } } } - break; - - case "result": - if (parsedChunk.subtype === "success") { + } else if (message.type === "result") { + // Handle final results with usage information + if (message.subtype === "success") { + const duration = Date.now() - this.runStats.startTime; core.info( tableString([ - ["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`], - ["Input Tokens", parsedChunk.usage?.input_tokens || 0], - ["Output Tokens", parsedChunk.usage?.output_tokens || 0], - ["Duration", `${parsedChunk.duration_ms}ms`], - ["Turns", parsedChunk.num_turns || 1], + ["Cost", `$${message.total_cost_usd?.toFixed(4) || "0.0000"}`], + ["Input Tokens", message.usage?.input_tokens || 0], + ["Output Tokens", message.usage?.output_tokens || 0], + ["Duration", `${duration}ms`], + ["Turns", this.runStats.turns], ]) ); } else { - core.error(`❌ Failed: ${parsedChunk.error || "Unknown error"}`); + core.error(`❌ Failed: ${JSON.stringify(message)}`); } - break; - - default: - debugLog(`📦 Unknown chunk type: ${parsedChunk.type}`); - break; + } } - } catch (error) { - debugLog(`Failed to parse chunk: ${error}`); - debugLog(`Raw chunk: ${chunk.substring(0, 200)}...`); + + core.info("✅ Task complete."); + core.endGroup(); + + return { + success: true, + output: finalOutput, + }; } } diff --git a/main.ts b/main.ts index 51cfb17..7bceb29 100644 --- a/main.ts +++ b/main.ts @@ -1,8 +1,8 @@ import * as core from "@actions/core"; import { type } from "arktype"; import { ClaudeAgent } from "./agents/claude.ts"; -import { setupGitHubInstallationToken, parseRepoContext } from "./utils/github.ts"; -import { setupGitConfig, setupGitAuth } from "./utils/setup.ts"; +import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts"; +import { setupGitAuth, setupGitConfig } from "./utils/setup.ts"; export const Inputs = type({ prompt: "string", @@ -25,7 +25,7 @@ export async function main(inputs: Inputs): Promise { const githubInstallationToken = await setupGitHubInstallationToken(); const repoContext = parseRepoContext(); - + setupGitAuth(githubInstallationToken, repoContext); const agent = new ClaudeAgent({ diff --git a/package.json b/package.json index d746bbc..ed06d21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/action", - "version": "0.0.60", + "version": "0.0.61", "type": "module", "files": [ "index.js", @@ -21,6 +21,7 @@ }, "dependencies": { "@actions/core": "^1.11.1", + "@anthropic-ai/claude-agent-sdk": "^0.1.26", "@ark/fs": "0.50.0", "@ark/util": "0.50.0", "@octokit/rest": "^22.0.0", diff --git a/play.ts b/play.ts index 3e0784b..ecde0cf 100644 --- a/play.ts +++ b/play.ts @@ -46,9 +46,6 @@ export async function run( if (result.success) { console.log("✅ Action completed successfully"); - if (result.output) { - console.log("Output:", result.output); - } return { success: true, output: result.output || undefined, error: undefined }; } else { console.error("❌ Action failed:", result.error);