diff --git a/agents/claude.ts b/agents/claude.ts index c85c14b..86e8952 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -1,9 +1,7 @@ -import { access, constants } from "node:fs/promises"; import * as core from "@actions/core"; -import { query } from "@anthropic-ai/claude-agent-sdk"; +import { query, type SDKMessage } 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"; import { boxString, tableString } from "../utils/table.ts"; import { instructions } from "./shared.ts"; import type { Agent, AgentConfig, AgentResult } from "./types.ts"; @@ -14,11 +12,6 @@ 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, - startTime: 0, - }; constructor(config: AgentConfig) { this.apiKey = config.apiKey; @@ -26,46 +19,10 @@ export class ClaudeAgent implements Agent { } /** - * Check if Claude Code CLI is already installed - */ - private async isClaudeInstalled(): Promise { - try { - const claudePath = `${process.env.HOME}/.local/bin/claude`; - await access(claudePath, constants.F_OK | constants.X_OK); - return true; - } catch { - return false; - } - } - - /** - * Install Claude Code CLI + * Install is a no-op since Claude CLI is bundled with the SDK */ async install(): Promise { - if (await this.isClaudeInstalled()) { - core.info("Claude Code is already installed, skipping installation"); - return; - } - - core.info("Installing Claude Code..."); - try { - const result = await spawn({ - cmd: "bash", - args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash -s 1.0.93"], - env: { ANTHROPIC_API_KEY: this.apiKey }, - timeout: 120000, // 2 minute timeout - onStdout: () => {}, - onStderr: (chunk) => process.stderr.write(chunk), - }); - - if (result.exitCode !== 0) { - throw new Error(`Installation failed with exit code ${result.exitCode}: ${result.stderr}`); - } - - core.info("Claude Code installed successfully"); - } catch (error) { - throw new Error(`Failed to install Claude Code: ${error}`); - } + // No installation needed - CLI is bundled with @anthropic-ai/claude-agent-sdk } /** @@ -84,14 +41,6 @@ export class ClaudeAgent implements Agent { core.startGroup("🔄 Run details"); - this.runStats = { - toolsUsed: 0, - turns: 0, - startTime: Date.now(), - }; - - let finalOutput = ""; - // Initialize session core.info(`🚀 Starting Claude Agent SDK session...`); @@ -109,92 +58,8 @@ export class ClaudeAgent implements Agent { // 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") { - this.runStats.toolsUsed++; - const toolName = content.name; - core.info(`→ ${toolName}`); - - if (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 - ? `${input.content.substring(0, 100)}...` - : 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) => { - if (edit.file_path) { - core.info(` ${index + 1}. ${edit.file_path}`); - } - }); - } - if (input.task) { - core.info(` └─ task: ${input.task}`); - } - if (input.bash_command) { - core.info(` └─ bash_command: ${input.bash_command}`); - } - } - } - } - } - } 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}`); - } - } - } - } 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", `$${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: ${JSON.stringify(message)}`); - } - } + const handler = messageHandlers[message.type]; + handler(message as never); } core.info("✅ Task complete."); @@ -202,7 +67,84 @@ export class ClaudeAgent implements Agent { return { success: true, - output: finalOutput, + output: "", }; } } + +type SDKMessageType = SDKMessage["type"]; + +type SDKMessageHandler = ( + data: Extract +) => void; + +type SDKMessageHandlers = { + [type in SDKMessageType]: SDKMessageHandler; +}; + +const messageHandlers: SDKMessageHandlers = { + assistant: (data) => { + if (data.message?.content) { + for (const content of data.message.content) { + if (content.type === "text" && content.text?.trim()) { + core.info(boxString(content.text.trim(), { title: "Claude" })); + } else if (content.type === "tool_use") { + core.info(`→ ${content.name}`); + + if (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 preview = + input.content.length > 100 + ? `${input.content.substring(0, 100)}...` + : input.content; + core.info(` └─ content: ${preview}`); + } + 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) => { + if (edit.file_path) core.info(` ${index + 1}. ${edit.file_path}`); + }); + } + if (input.task) core.info(` └─ task: ${input.task}`); + if (input.bash_command) core.info(` └─ bash_command: ${input.bash_command}`); + } + } + } + } + }, + user: (data) => { + if (data.message?.content) { + for (const content of data.message.content) { + if (content.type === "tool_result" && content.is_error) { + core.warning(`❌ Tool error: ${content.content}`); + } + } + } + }, + result: (data) => { + if (data.subtype === "success") { + core.info( + tableString([ + ["Cost", `$${data.total_cost_usd?.toFixed(4) || "0.0000"}`], + ["Input Tokens", data.usage?.input_tokens || 0], + ["Output Tokens", data.usage?.output_tokens || 0], + ]) + ); + } else if (data.subtype === "error_max_turns") { + core.error(`❌ Max turns reached: ${JSON.stringify(data)}`); + } else if (data.subtype === "error_during_execution") { + core.error(`❌ Execution error: ${JSON.stringify(data)}`); + } else { + core.error(`❌ Failed: ${JSON.stringify(data)}`); + } + }, + system: () => {}, + stream_event: () => {}, +}; diff --git a/package.json b/package.json index ed06d21..b210a6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/action", - "version": "0.0.61", + "version": "0.0.62", "type": "module", "files": [ "index.js",