cleanup comments

This commit is contained in:
ssalbdivad
2025-10-09 16:33:11 -04:00
parent f74a75cfac
commit 9459803aaa
11 changed files with 7 additions and 185 deletions
+1 -85
View File
@@ -16,14 +16,12 @@ export class ClaudeAgent implements Agent {
startTime: 0,
};
// $: ExecaMethod;
constructor(config: AgentConfig) {
if (!config.apiKey) {
throw new Error("Claude agent requires an API key");
}
this.apiKey = config.apiKey;
// Removed execa dependency - using spawn utility instead
}
/**
@@ -43,7 +41,6 @@ export class ClaudeAgent implements Agent {
* Install Claude Code CLI
*/
async install(): Promise<void> {
// Check if Claude Code is already installed
if (await this.isClaudeInstalled()) {
core.info("Claude Code is already installed, skipping installation");
return;
@@ -51,16 +48,12 @@ export class ClaudeAgent implements Agent {
core.info("Installing Claude Code...");
try {
// Use shell execution to properly handle the pipe
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: () => {
// no logs
// process.stdout.write(chunk)
},
onStdout: () => {},
onStderr: (chunk) => process.stderr.write(chunk),
});
@@ -79,14 +72,10 @@ export class ClaudeAgent implements Agent {
*/
async execute(prompt: string): Promise<AgentResult> {
core.info("Running Claude Code...");
// printTable([[prompt]]);
try {
// Execute Claude Code with the prompt directly using proper headless mode
// core.info(`Executing Claude Code with prompt: ${prompt.substring(0, 100)}...`);
const claudePath = `${process.env.HOME}/.local/bin/claude`;
// console.log("Using Claude Code from:", claudePath);
console.log(boxString(prompt, { title: "Prompt" }));
const args = [
"--print",
@@ -97,7 +86,6 @@ export class ClaudeAgent implements Agent {
"bypassPermissions",
];
// Add MCP configuration if GitHub credentials are available
if (
process.env.GITHUB_INSTALLATION_TOKEN &&
process.env.REPO_OWNER &&
@@ -116,10 +104,8 @@ export class ClaudeAgent implements Agent {
ANTHROPIC_API_KEY: this.apiKey,
};
// Start a collapsible log group for streaming output
core.startGroup("🔄 Run details");
// Initialize run statistics
this.runStats = {
toolsUsed: 0,
turns: 0,
@@ -129,7 +115,6 @@ export class ClaudeAgent implements Agent {
const finalResult = "";
const totalCost = 0;
// run Claude Code with the prompt
const result = await spawn({
cmd: claudePath,
args,
@@ -137,37 +122,21 @@ export class ClaudeAgent implements Agent {
input: prompt,
timeout: 10 * 60 * 1000, // 10 minutes
onStdout: (_chunk) => {
// console.log(chunk);
processJSONChunk(_chunk, this);
},
onStderr: (_chunk) => {
if (_chunk.trim()) {
// core.warning(`[warn] ${chunk}`);
processJSONChunk(_chunk, this);
}
},
});
// throw on non-zero exit code
if (result.exitCode !== 0) {
throw new Error(
`Command failed with exit code ${result.exitCode}\n\nStdout: ${result.stdout}\n\nStderr: ${result.stderr}`
);
}
// Process the complete buffered stdout to extract final results
// if (result.stdout.trim()) {
// const lines = result.stdout.trim().split("\n");
// for (const line of lines) {
// if (line.trim()) {
// const chunkResult = processJsonChunk(line);
// if (chunkResult.finalResult) finalResult = chunkResult.finalResult;
// if (chunkResult.totalCost) totalCost = chunkResult.totalCost;
// }
// }
// }
// Log run summary
const duration = Date.now() - this.runStats.startTime;
core.info(
`📊 Run Summary: ${this.runStats.toolsUsed} tools used, ${this.runStats.turns} turns, ${duration}ms duration`
@@ -187,11 +156,9 @@ export class ClaudeAgent implements Agent {
},
};
} catch (error: any) {
// Ensure group is closed even if error occurs before group is started
try {
core.endGroup();
} catch {
// Group might not have been started, ignore
}
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
@@ -202,34 +169,11 @@ export class ClaudeAgent implements Agent {
}
}
/**
* Process a JSON chunk line and extract result data
*/
// function processJsonChunk(line: string): { finalResult?: string; totalCost?: number } {
// try {
// const chunk = JSON.parse(line.trim());
// processJSONChunk(chunk);
// // Collect final result and cost data
// if (chunk.type === "result" && chunk.result) {
// return {
// finalResult: chunk.result,
// totalCost: chunk.total_cost_usd || 0,
// };
// }
// return {};
// } catch {
// core.debug(`Failed to parse JSON line: ${line}`);
// return {};
// }
// }
/**
* Pretty print a JSON chunk based on its type
*/
function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
try {
// Parse the JSON string first
console.log(chunk);
const parsedChunk = JSON.parse(chunk.trim());
@@ -237,8 +181,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
case "system":
if (parsedChunk.subtype === "init") {
core.info(`🚀 Starting Claude Code session...`);
// core.info(`📁 Working directory: ${parsedChunk.cwd}`);
// core.info(`🔑 Permission mode: ${parsedChunk.permissionMode}`);
core.info(
tableString([
["model", parsedChunk.model],
@@ -264,39 +206,31 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
case "assistant":
if (parsedChunk.message?.content) {
// Track turns
if (agent) {
agent.runStats.turns++;
}
for (const content of parsedChunk.message.content) {
if (content.type === "text") {
// Skip empty text content
if (content.text.trim()) {
core.info(boxString(content.text.trim(), { title: "Claude Code" }));
}
} else if (content.type === "tool_use") {
// Track tools used
if (agent) {
agent.runStats.toolsUsed++;
}
// Enhanced tool usage logging
const toolName = content.name;
// const toolId = content.id;
core.info(`${toolName}`);
// Log tool-specific details based on tool type
if (content.input) {
const input = content.input;
// Common tool input fields
if (input.description) {
core.info(` └─ ${input.description}`);
}
// Tool-specific input fields
if (input.command) {
core.info(` └─ command: ${input.command}`);
}
@@ -325,7 +259,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
core.info(` └─ url: ${input.url}`);
}
// For multi-edit or complex operations
if (input.edits && Array.isArray(input.edits)) {
core.info(` └─ edits: ${input.edits.length} changes`);
input.edits.forEach((edit: any, index: number) => {
@@ -335,19 +268,15 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
});
}
// For task operations
if (input.task) {
core.info(` └─ task: ${input.task}`);
}
// For bash operations with specific details
if (input.bash_command) {
core.info(` └─ bash_command: ${input.bash_command}`);
}
}
// Log tool ID for debugging
// core.debug(` 🔗 Tool ID: ${toolId}`);
}
}
}
@@ -360,9 +289,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
if (content.is_error) {
core.warning(`❌ Tool error: ${content.content}`);
} else {
// Enhanced tool result logging
const _resultContent = content.content.trim();
// do nothing for now. usually useless in headless more.
}
}
}
@@ -371,15 +297,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
case "result":
if (parsedChunk.subtype === "success") {
// Claude already prints something almost identical to this, so skip for now
// if (parsedChunk.result) {
// core.info(
// boxString(parsedChunk.result.trim(), {
// title: "🤖 Claude Code",
// maxWidth: 70,
// }),
// );
// }
core.info(
tableString([
@@ -396,7 +313,6 @@ function processJSONChunk(chunk: string, agent?: ClaudeAgent): void {
break;
default:
// Log unknown chunk types for debugging
core.debug(`📦 Unknown chunk type: ${parsedChunk.type}`);
break;
}
+3 -5
View File
@@ -25580,7 +25580,7 @@ async function spawn(options) {
durationMs
});
});
child.on("error", (error2) => {
child.on("error", (_error) => {
const durationMs = Date.now() - startTime;
if (timeoutId) {
clearTimeout(timeoutId);
@@ -25609,8 +25609,8 @@ function tableString(rows, options) {
return lineIndex === 0 || options?.title && lineIndex === 1 || lineIndex === rowCount;
}
} = options || {};
if (options?.title) {
rows.unshift([options.title]);
if (title) {
rows.unshift([title]);
}
const tableOutput = (0, import_table.table)(rows, {
drawHorizontalLine,
@@ -25698,7 +25698,6 @@ var ClaudeAgent = class {
turns: 0,
startTime: 0
};
// $: ExecaMethod;
constructor(config) {
if (!config.apiKey) {
throw new Error("Claude agent requires an API key");
@@ -25927,7 +25926,6 @@ function processJSONChunk(chunk, agent) {
if (content.is_error) {
core.warning(`\u274C Tool error: ${content.content}`);
} else {
const _resultContent = content.content.trim();
}
}
}
-5
View File
@@ -11,7 +11,6 @@ import { setupGitHubInstallationToken } from "./utils/github.ts";
async function run(): Promise<void> {
try {
// Get inputs from GitHub Actions
const prompt = core.getInput("prompt", { required: true });
const anthropic_api_key = core.getInput("anthropic_api_key");
@@ -19,13 +18,11 @@ async function run(): Promise<void> {
throw new Error("prompt is required");
}
// Create params object with new structure
const inputs: ExecutionInputs = {
prompt,
anthropic_api_key,
};
// Add optional properties only if they exist
const githubToken = core.getInput("github_token") || process.env.GITHUB_TOKEN;
if (githubToken) {
inputs.github_token = githubToken;
@@ -47,7 +44,6 @@ async function run(): Promise<void> {
const result = await main(params);
// TODO: Set outputs
if (!result.success) {
throw new Error(result.error || "Agent execution failed");
@@ -58,7 +54,6 @@ async function run(): Promise<void> {
}
}
// Run the action
run().catch((error) => {
console.error("Action failed:", error);
process.exit(1);
-6
View File
@@ -1,7 +1,6 @@
import * as core from "@actions/core";
import { ClaudeAgent } from "./agents/claude.ts";
// Expected environment variables that should be passed as inputs
export const EXPECTED_INPUTS: string[] = [
"ANTHROPIC_API_KEY",
"GITHUB_TOKEN",
@@ -29,24 +28,19 @@ export interface MainResult {
export async function main(params: MainParams): Promise<MainResult> {
try {
// Extract inputs from params
const { inputs, env, cwd } = params;
// Set working directory if different from current
if (cwd !== process.cwd()) {
process.chdir(cwd);
}
// Set environment variables
Object.assign(process.env, env);
core.info(`→ Starting agent run with Claude Code`);
// Create and install the Claude agent
const agent = new ClaudeAgent({ apiKey: inputs.anthropic_api_key });
await agent.install();
// Execute the agent with the prompt
const result = await agent.execute(inputs.prompt);
if (!result.success) {
-19
View File
@@ -8,7 +8,6 @@ import { runAct } from "./utils/act.ts";
import { generateInstallationToken } from "./utils/generate-installation-token.ts";
import { setupTestRepo } from "./utils/setup.ts";
// Load environment variables from .env file
config();
const __filename = fileURLToPath(import.meta.url);
@@ -20,17 +19,14 @@ export async function run(
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
try {
if (options.act) {
// Use Docker/act to run the action
console.log("🐳 Running with Docker/act...");
runAct(prompt);
return { success: true };
}
// Setup test repository and run directly
const tempDir = join(process.cwd(), ".temp");
setupTestRepo({ tempDir, forceClean: true });
// Change to the temp directory
const originalCwd = process.cwd();
process.chdir(tempDir);
@@ -40,7 +36,6 @@ export async function run(
console.log(prompt);
console.log("─".repeat(50));
// Set environment variables from our .env for the action to use
const { EXPECTED_INPUTS } = await import("./main.ts");
EXPECTED_INPUTS.forEach((inputName) => {
const value = process.env[inputName];
@@ -49,13 +44,11 @@ export async function run(
}
});
// Run main with the new params structure
const inputs: any = {
prompt,
anthropic_api_key: process.env.ANTHROPIC_API_KEY || "",
};
// Add optional properties only if they exist
if (process.env.GITHUB_TOKEN) {
inputs.github_token = process.env.GITHUB_TOKEN;
}
@@ -65,7 +58,6 @@ export async function run(
inputs.github_installation_token = installationToken;
console.log("✅ GitHub installation token generated successfully");
// Set installation token in environment for Claude agent MCP configuration
const envWithToken = {
...process.env,
GITHUB_INSTALLATION_TOKEN: installationToken,
@@ -77,7 +69,6 @@ export async function run(
cwd: process.cwd(),
});
// Change back to original directory
process.chdir(originalCwd);
if (result.success) {
@@ -97,7 +88,6 @@ export async function run(
}
}
// CLI execution when run directly
if (import.meta.url === `file://${process.argv[1]}`) {
const args = arg({
"--help": Boolean,
@@ -133,15 +123,12 @@ Examples:
let prompt: string;
if (args["--raw"]) {
// Use raw prompt string
prompt = args["--raw"];
} else {
// Load prompt from file
const filePath = args._[0] || "fixtures/basic.txt";
const ext = extname(filePath).toLowerCase();
let resolvedPath: string;
// First try as fixtures path
const fixturesPath = join(__dirname, "fixtures", filePath);
if (existsSync(fixturesPath)) {
resolvedPath = fixturesPath;
@@ -153,13 +140,11 @@ Examples:
switch (ext) {
case ".txt": {
// Plain text - pass directly as prompt
prompt = readFileSync(resolvedPath, "utf8").trim();
break;
}
case ".json": {
// JSON - stringify and pass as prompt
const content = readFileSync(resolvedPath, "utf8");
const parsed = JSON.parse(content);
prompt = JSON.stringify(parsed, null, 2);
@@ -167,7 +152,6 @@ Examples:
}
case ".ts": {
// TypeScript - dynamic import and stringify default export
const fileUrl = pathToFileURL(resolvedPath).href;
const module = await import(fileUrl);
@@ -175,14 +159,11 @@ Examples:
throw new Error(`TypeScript file ${filePath} must have a default export`);
}
// If it's a string, use it directly
if (typeof module.default === "string") {
prompt = module.default;
} else if (typeof module.default === "object" && module.default.prompt) {
// If it's a MainParams object with a prompt field, extract the prompt
prompt = module.default.prompt;
} else {
// Otherwise stringify it
prompt = JSON.stringify(module.default, null, 2);
}
break;
-13
View File
@@ -12,27 +12,21 @@ const tempDir = join(__dirname, "..", ".temp");
const actionPath = join(__dirname, "..");
const envPath = join(__dirname, "..", "..", ".env");
// Environment variables that should be passed as secrets to the workflow
const ENV_VARS = ["ANTHROPIC_API_KEY", "GITHUB_INSTALLATION_TOKEN"];
export function runAct(prompt: string): void {
// Setup test repository
setupTestRepo({ tempDir });
// Load environment variables
config({ path: envPath });
// Build action bundles
buildAction(actionPath);
const workflowPath = join(tempDir, ".github", "workflows", "pullfrog.yml");
// Create minimal dist for act (avoids pnpm symlink issues)
const distPath = join(actionPath, ".act-dist");
console.log("📦 Creating minimal distribution for act...");
execSync(`rm -rf "${distPath}" && mkdir -p "${distPath}"`, { shell: "/bin/bash" });
// Copy only necessary files (bundled, no node_modules needed)
["action.yml", "entry.cjs", "index.cjs", "package.json"].forEach((file) => {
const src = join(actionPath, file);
if (existsSync(src)) {
@@ -41,8 +35,6 @@ export function runAct(prompt: string): void {
});
try {
// Build the act command with input directly
// Properly escape the prompt for shell
const escapedPrompt = prompt.replace(/'/g, "'\\''");
const actCommandParts = [
@@ -56,14 +48,12 @@ export function runAct(prompt: string): void {
`pullfrog/action@v0=${distPath}`, // Use minimal dist without symlinks
];
// Add environment variables as secrets that will be available to the workflow
ENV_VARS.forEach((key) => {
if (process.env[key]) {
actCommandParts.push("-s", key);
}
});
// We only need the specific ENV_VARS, no need to add other variables
const actCommand = actCommandParts.join(" ");
@@ -73,15 +63,12 @@ export function runAct(prompt: string): void {
console.log("─".repeat(50));
console.log("");
// Execute act
execSync(actCommand, {
stdio: "inherit",
cwd: join(__dirname, "..", ".."),
});
// Clean up
execSync(`rm -rf "${distPath}"`);
} catch (error) {
// Clean up on error
execSync(`rm -rf "${distPath}"`);
console.error("❌ Act execution failed:", (error as Error).message);
process.exit(1);
-1
View File
@@ -165,7 +165,6 @@ const findInstallationId = async (jwt: string, repoOwner: string, repoName: stri
return installation.id;
}
} catch {
// Installation doesn't have access to repository
}
}
-7
View File
@@ -14,13 +14,11 @@ export interface InstallationToken {
* Setup GitHub installation token for the action
*/
export async function setupGitHubInstallationToken(): Promise<string> {
// Check if we have an installation token from inputs or environment
const inputToken = core.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
const existingToken = inputToken || envToken;
if (existingToken) {
// Mask the existing token in logs for security
core.setSecret(existingToken);
core.info("Using provided GitHub installation token");
return existingToken;
@@ -29,11 +27,9 @@ export async function setupGitHubInstallationToken(): Promise<string> {
core.info("Generating OIDC token...");
try {
// Generate OIDC token for our API
const oidcToken = await core.getIDToken("pullfrog-api");
core.info("OIDC token generated successfully");
// Exchange OIDC token for installation token
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
core.info("Exchanging OIDC token for installation token...");
@@ -52,14 +48,11 @@ export async function setupGitHubInstallationToken(): Promise<string> {
);
}
// This type is enforced by us when the response is created
const tokenData = (await tokenResponse.json()) as InstallationToken;
core.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
// Mask the token in logs for security
core.setSecret(tokenData.token);
// Set the token as an environment variable for this run
process.env.GITHUB_INSTALLATION_TOKEN = tokenData.token;
return tokenData.token;
-2
View File
@@ -17,13 +17,11 @@ export function setupTestRepo(options: SetupOptions): void {
forceClean = false,
} = options;
// Handle existing temp directory
if (existsSync(tempDir)) {
if (forceClean) {
console.log("🗑️ Removing existing .temp directory...");
rmSync(tempDir, { recursive: true, force: true });
// Clone the repository
console.log("📦 Cloning pullfrogai/scratch into .temp...");
execSync(`git clone ${repoUrl} ${tempDir}`, { stdio: "inherit" });
} else {
+1 -10
View File
@@ -28,13 +28,11 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
let stderrBuffer = "";
return new Promise((resolve, reject) => {
// Spawn the child process
const child = nodeSpawn(cmd, args, {
env: env ? { ...process.env, ...env } : process.env,
stdio: ["pipe", "pipe", "pipe"],
});
// Set up timeout if specified
let timeoutId: NodeJS.Timeout | undefined;
let isTimedOut = false;
@@ -43,7 +41,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
isTimedOut = true;
child.kill("SIGTERM");
// If SIGTERM doesn't work, use SIGKILL after 5 seconds
setTimeout(() => {
if (!child.killed) {
child.kill("SIGKILL");
@@ -52,7 +49,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
}, timeout);
}
// Handle stdout streaming
if (child.stdout) {
child.stdout.on("data", (data: Buffer) => {
const chunk = data.toString();
@@ -61,7 +57,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
});
}
// Handle stderr streaming
if (child.stderr) {
child.stderr.on("data", (data: Buffer) => {
const chunk = data.toString();
@@ -70,7 +65,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
});
}
// Handle process completion
child.on("close", (exitCode) => {
const durationMs = Date.now() - startTime;
@@ -91,15 +85,13 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
});
});
// Handle process errors
child.on("error", (error) => {
child.on("error", (_error) => {
const durationMs = Date.now() - startTime;
if (timeoutId) {
clearTimeout(timeoutId);
}
// Still return buffered output even on error
resolve({
stdout: stdoutBuffer,
stderr: stderrBuffer,
@@ -108,7 +100,6 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
});
});
// Send input if provided
if (input && child.stdin) {
child.stdin.write(input);
child.stdin.end();
+2 -32
View File
@@ -22,8 +22,8 @@ export function tableString(
},
} = options || {};
if (options?.title) {
rows.unshift([options.title]);
if (title) {
rows.unshift([title]);
}
const tableOutput = table(rows, {
@@ -140,33 +140,3 @@ export function boxString(
return result;
}
// /**
// * Create a simple two-column table for displaying key-value pairs
// * @param data - Array of [key, value] pairs
// * @param title - Optional table title
// * @param indent - Optional indentation string
// */
// export function printKeyValueTable(
// data: [string, string][],
// title?: string,
// indent?: string
// ): void {
// const rows: string[][] = [["Key", "Value"], ...data];
// const options: Parameters<typeof printTable>[1] = {};
// if (title !== undefined) options.title = title;
// if (indent !== undefined) options.indent = indent;
// printTable(rows, options);
// }
// /**
// * Create a path resolution table (specific use case)
// * @param pathData - Array of [location, resolvedPath] pairs
// * @param indent - Optional indentation string
// */
// export function printPathTable(pathData: [string, string][], indent?: string): void {
// const rows: string[][] = [["Location", "Resolved path"], ...pathData];
// const options: Parameters<typeof printTable>[1] = {};
// if (indent !== undefined) options.indent = indent;
// printTable(rows, options);
// }