add handler map for sdk parsing
This commit is contained in:
+83
-141
@@ -1,9 +1,7 @@
|
|||||||
import { access, constants } from "node:fs/promises";
|
|
||||||
import * as core from "@actions/core";
|
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 { createMcpConfig } from "../mcp/config.ts";
|
||||||
import { debugLog, isDebug } from "../utils/logging.ts";
|
import { debugLog, isDebug } from "../utils/logging.ts";
|
||||||
import { spawn } from "../utils/subprocess.ts";
|
|
||||||
import { boxString, tableString } from "../utils/table.ts";
|
import { boxString, tableString } from "../utils/table.ts";
|
||||||
import { instructions } from "./shared.ts";
|
import { instructions } from "./shared.ts";
|
||||||
import type { Agent, AgentConfig, AgentResult } from "./types.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 {
|
export class ClaudeAgent implements Agent {
|
||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
private githubInstallationToken: string;
|
private githubInstallationToken: string;
|
||||||
public runStats = {
|
|
||||||
toolsUsed: 0,
|
|
||||||
turns: 0,
|
|
||||||
startTime: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
this.apiKey = config.apiKey;
|
this.apiKey = config.apiKey;
|
||||||
@@ -26,46 +19,10 @@ export class ClaudeAgent implements Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Claude Code CLI is already installed
|
* Install is a no-op since Claude CLI is bundled with the SDK
|
||||||
*/
|
|
||||||
private async isClaudeInstalled(): Promise<boolean> {
|
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
async install(): Promise<void> {
|
async install(): Promise<void> {
|
||||||
if (await this.isClaudeInstalled()) {
|
// No installation needed - CLI is bundled with @anthropic-ai/claude-agent-sdk
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,14 +41,6 @@ export class ClaudeAgent implements Agent {
|
|||||||
|
|
||||||
core.startGroup("🔄 Run details");
|
core.startGroup("🔄 Run details");
|
||||||
|
|
||||||
this.runStats = {
|
|
||||||
toolsUsed: 0,
|
|
||||||
turns: 0,
|
|
||||||
startTime: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let finalOutput = "";
|
|
||||||
|
|
||||||
// Initialize session
|
// Initialize session
|
||||||
core.info(`🚀 Starting Claude Agent SDK session...`);
|
core.info(`🚀 Starting Claude Agent SDK session...`);
|
||||||
|
|
||||||
@@ -109,92 +58,8 @@ export class ClaudeAgent implements Agent {
|
|||||||
|
|
||||||
// Stream the results
|
// Stream the results
|
||||||
for await (const message of queryInstance) {
|
for await (const message of queryInstance) {
|
||||||
if (message.type === "assistant") {
|
const handler = messageHandlers[message.type];
|
||||||
this.runStats.turns++;
|
handler(message as never);
|
||||||
|
|
||||||
// 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)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info("✅ Task complete.");
|
core.info("✅ Task complete.");
|
||||||
@@ -202,7 +67,84 @@ export class ClaudeAgent implements Agent {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: finalOutput,
|
output: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SDKMessageType = SDKMessage["type"];
|
||||||
|
|
||||||
|
type SDKMessageHandler<type extends SDKMessageType = SDKMessageType> = (
|
||||||
|
data: Extract<SDKMessage, { type: type }>
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
type SDKMessageHandlers = {
|
||||||
|
[type in SDKMessageType]: SDKMessageHandler<type>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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: () => {},
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.61",
|
"version": "0.0.62",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user