switch to anthropic typescript-sdk
This commit is contained in:
+65
-188
@@ -1,5 +1,6 @@
|
|||||||
import { access, constants } from "node:fs/promises";
|
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 { 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 { 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<AgentResult> {
|
async execute(prompt: string): Promise<AgentResult> {
|
||||||
core.info("Running Claude Code...");
|
core.info("Running Claude Agent SDK...");
|
||||||
|
|
||||||
try {
|
console.log(boxString(prompt, { title: "Prompt" }));
|
||||||
const claudePath = `${process.env.HOME}/.local/bin/claude`;
|
|
||||||
|
|
||||||
const env = {
|
const mcpConfig = JSON.parse(createMcpConfig(this.githubInstallationToken));
|
||||||
ANTHROPIC_API_KEY: this.apiKey,
|
|
||||||
...(isDebug() && { LOG_LEVEL: "debug" }),
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(boxString(prompt, { title: "Prompt" }));
|
if (isDebug()) {
|
||||||
|
debugLog(`📋 MCP Config: ${JSON.stringify(mcpConfig, null, 2)}`);
|
||||||
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 (trimmedChunk.startsWith("[ERROR]") || trimmedChunk.startsWith("[error]")) {
|
core.startGroup("🔄 Run details");
|
||||||
console.error(chunk);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog(trimmedChunk);
|
this.runStats = {
|
||||||
|
toolsUsed: 0,
|
||||||
|
turns: 0,
|
||||||
|
startTime: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
const parsedChunk = JSON.parse(trimmedChunk);
|
let finalOutput = "";
|
||||||
|
|
||||||
switch (parsedChunk.type) {
|
// Initialize session
|
||||||
case "system":
|
core.info(`🚀 Starting Claude Agent SDK session...`);
|
||||||
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;
|
|
||||||
|
|
||||||
case "assistant":
|
// Set API key environment variable for SDK
|
||||||
if (parsedChunk.message?.content) {
|
process.env.ANTHROPIC_API_KEY = this.apiKey;
|
||||||
if (agent) {
|
|
||||||
agent.runStats.turns++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const content of parsedChunk.message.content) {
|
// Create the query with SDK options
|
||||||
if (content.type === "text") {
|
const queryInstance = query({
|
||||||
if (content.text.trim()) {
|
prompt: `${instructions}\n\n${prompt}`,
|
||||||
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
|
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") {
|
} else if (content.type === "tool_use") {
|
||||||
if (agent) {
|
this.runStats.toolsUsed++;
|
||||||
agent.runStats.toolsUsed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolName = content.name;
|
const toolName = content.name;
|
||||||
|
|
||||||
core.info(`→ ${toolName}`);
|
core.info(`→ ${toolName}`);
|
||||||
|
|
||||||
if (content.input) {
|
if (content.input) {
|
||||||
const input = content.input;
|
const input = content.input as any;
|
||||||
|
|
||||||
if (input.description) {
|
if (input.description) {
|
||||||
core.info(` └─ ${input.description}`);
|
core.info(` └─ ${input.description}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.command) {
|
if (input.command) {
|
||||||
core.info(` └─ command: ${input.command}`);
|
core.info(` └─ command: ${input.command}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.file_path) {
|
if (input.file_path) {
|
||||||
core.info(` └─ file: ${input.file_path}`);
|
core.info(` └─ file: ${input.file_path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.content) {
|
if (input.content) {
|
||||||
const contentPreview =
|
const contentPreview =
|
||||||
input.content.length > 100
|
input.content.length > 100
|
||||||
@@ -256,19 +142,15 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
: input.content;
|
: input.content;
|
||||||
core.info(` └─ content: ${contentPreview}`);
|
core.info(` └─ content: ${contentPreview}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.query) {
|
if (input.query) {
|
||||||
core.info(` └─ query: ${input.query}`);
|
core.info(` └─ query: ${input.query}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.pattern) {
|
if (input.pattern) {
|
||||||
core.info(` └─ pattern: ${input.pattern}`);
|
core.info(` └─ pattern: ${input.pattern}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.url) {
|
if (input.url) {
|
||||||
core.info(` └─ url: ${input.url}`);
|
core.info(` └─ url: ${input.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.edits && Array.isArray(input.edits)) {
|
if (input.edits && Array.isArray(input.edits)) {
|
||||||
core.info(` └─ edits: ${input.edits.length} changes`);
|
core.info(` └─ edits: ${input.edits.length} changes`);
|
||||||
input.edits.forEach((edit: any, index: number) => {
|
input.edits.forEach((edit: any, index: number) => {
|
||||||
@@ -277,11 +159,9 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.task) {
|
if (input.task) {
|
||||||
core.info(` └─ task: ${input.task}`);
|
core.info(` └─ task: ${input.task}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.bash_command) {
|
if (input.bash_command) {
|
||||||
core.info(` └─ bash_command: ${input.bash_command}`);
|
core.info(` └─ bash_command: ${input.bash_command}`);
|
||||||
}
|
}
|
||||||
@@ -289,43 +169,40 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
} else if (message.type === "user") {
|
||||||
|
// Handle tool results
|
||||||
case "user":
|
if (message.message?.content) {
|
||||||
if (parsedChunk.message?.content) {
|
for (const content of message.message.content) {
|
||||||
for (const content of parsedChunk.message.content) {
|
if (content.type === "tool_result" && content.is_error) {
|
||||||
if (content.type === "tool_result") {
|
core.warning(`❌ Tool error: ${content.content}`);
|
||||||
if (content.is_error) {
|
|
||||||
core.warning(`❌ Tool error: ${content.content}`);
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
} else if (message.type === "result") {
|
||||||
|
// Handle final results with usage information
|
||||||
case "result":
|
if (message.subtype === "success") {
|
||||||
if (parsedChunk.subtype === "success") {
|
const duration = Date.now() - this.runStats.startTime;
|
||||||
core.info(
|
core.info(
|
||||||
tableString([
|
tableString([
|
||||||
["Cost", `$${parsedChunk.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
["Cost", `$${message.total_cost_usd?.toFixed(4) || "0.0000"}`],
|
||||||
["Input Tokens", parsedChunk.usage?.input_tokens || 0],
|
["Input Tokens", message.usage?.input_tokens || 0],
|
||||||
["Output Tokens", parsedChunk.usage?.output_tokens || 0],
|
["Output Tokens", message.usage?.output_tokens || 0],
|
||||||
["Duration", `${parsedChunk.duration_ms}ms`],
|
["Duration", `${duration}ms`],
|
||||||
["Turns", parsedChunk.num_turns || 1],
|
["Turns", this.runStats.turns],
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
} else {
|
} 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}`);
|
core.info("✅ Task complete.");
|
||||||
debugLog(`Raw chunk: ${chunk.substring(0, 200)}...`);
|
core.endGroup();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: finalOutput,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { ClaudeAgent } from "./agents/claude.ts";
|
import { ClaudeAgent } from "./agents/claude.ts";
|
||||||
import { setupGitHubInstallationToken, parseRepoContext } from "./utils/github.ts";
|
import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
|
||||||
import { setupGitConfig, setupGitAuth } from "./utils/setup.ts";
|
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
|
||||||
|
|
||||||
export const Inputs = type({
|
export const Inputs = type({
|
||||||
prompt: "string",
|
prompt: "string",
|
||||||
@@ -25,7 +25,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
|
|
||||||
const githubInstallationToken = await setupGitHubInstallationToken();
|
const githubInstallationToken = await setupGitHubInstallationToken();
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
setupGitAuth(githubInstallationToken, repoContext);
|
setupGitAuth(githubInstallationToken, repoContext);
|
||||||
|
|
||||||
const agent = new ClaudeAgent({
|
const agent = new ClaudeAgent({
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.60",
|
"version": "0.0.61",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.11.1",
|
"@actions/core": "^1.11.1",
|
||||||
|
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
|
||||||
"@ark/fs": "0.50.0",
|
"@ark/fs": "0.50.0",
|
||||||
"@ark/util": "0.50.0",
|
"@ark/util": "0.50.0",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ export async function run(
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log("✅ Action completed successfully");
|
console.log("✅ Action completed successfully");
|
||||||
if (result.output) {
|
|
||||||
console.log("Output:", result.output);
|
|
||||||
}
|
|
||||||
return { success: true, output: result.output || undefined, error: undefined };
|
return { success: true, output: result.output || undefined, error: undefined };
|
||||||
} else {
|
} else {
|
||||||
console.error("❌ Action failed:", result.error);
|
console.error("❌ Action failed:", result.error);
|
||||||
|
|||||||
Reference in New Issue
Block a user