UNPKG

claude-code-templates

Version:

CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects

505 lines (418 loc) 15.5 kB
#!/usr/bin/env node /** * Cloudflare Sandbox Launcher * Executes Claude Code prompts using Cloudflare Workers and Sandbox SDK */ import { query, ClaudeAgentOptions } from '@anthropic-ai/claude-agent-sdk'; import fetch from 'node-fetch'; import * as fs from 'fs'; import * as path from 'path'; interface ExecutionResult { success: boolean; question: string; code: string; output: string; error: string; sandboxId?: string; files?: { path: string; content: string }[]; } interface LauncherConfig { prompt: string; componentsToInstall: string; anthropicApiKey: string; workerUrl?: string; useLocalWorker?: boolean; targetDir?: string; } // ANSI color codes for terminal output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', }; function log(message: string, level: 'info' | 'success' | 'error' | 'warning' = 'info') { const timestamp = new Date().toLocaleTimeString(); const prefix = { info: `${colors.blue}${colors.reset}`, success: `${colors.green}${colors.reset}`, error: `${colors.red}${colors.reset}`, warning: `${colors.yellow}${colors.reset}`, }[level]; console.log(`[${timestamp}] ${prefix} ${message}`); } function printSeparator(char: string = '=', length: number = 60) { console.log(char.repeat(length)); } /** * Extract files from generated code (handles multiple code blocks) */ function extractFilesFromCode(code: string): { path: string; content: string }[] { const files: { path: string; content: string }[] = []; // Pattern to match code blocks with file names // Matches: ```html:filename.html or ```javascript:script.js or ```css:styles.css const fileBlockPattern = /```(\w+):([^\n]+)\n([\s\S]*?)```/g; let match; while ((match = fileBlockPattern.exec(code)) !== null) { const [, _language, filename, content] = match; files.push({ path: filename.trim(), content: content.trim() }); } // If no explicit filenames, try to detect from code blocks if (files.length === 0) { const codeBlockPattern = /```(\w+)\n([\s\S]*?)```/g; const languageExtensions: Record<string, string> = { html: 'index.html', css: 'styles.css', javascript: 'script.js', js: 'script.js', typescript: 'index.ts', ts: 'index.ts', python: 'main.py', py: 'main.py' }; const detectedFiles = new Map<string, string>(); while ((match = codeBlockPattern.exec(code)) !== null) { const [, language, content] = match; const ext = language.toLowerCase(); const filename = languageExtensions[ext] || `code.${ext}`; // If we already have this type of file, append a number let finalFilename = filename; let counter = 1; while (detectedFiles.has(finalFilename)) { const parts = filename.split('.'); const extension = parts.pop(); const base = parts.join('.'); finalFilename = `${base}${counter}.${extension}`; counter++; } detectedFiles.set(finalFilename, content.trim()); } detectedFiles.forEach((content, filename) => { files.push({ path: filename, content }); }); } return files; } /** * Save files to local directory */ function saveFilesToDirectory(files: { path: string; content: string }[], baseDir: string): void { if (files.length === 0) { return; } // Create output directory if (!fs.existsSync(baseDir)) { fs.mkdirSync(baseDir, { recursive: true }); } files.forEach(file => { const fullPath = path.join(baseDir, file.path); const dir = path.dirname(fullPath); // Create directory if needed if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Write file fs.writeFileSync(fullPath, file.content, 'utf-8'); log(`✓ ${file.path}`, 'success'); }); console.log(''); console.log(`${colors.green}${colors.reset} All files saved to: ${colors.cyan}${path.resolve(baseDir)}${colors.reset}`); printSeparator(); } async function executeViaWorker( config: LauncherConfig ): Promise<ExecutionResult> { const workerUrl = config.workerUrl || 'http://localhost:8787'; const endpoint = `${workerUrl}/execute`; log(`Sending request to Cloudflare Worker: ${endpoint}`); try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ question: config.prompt, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Worker returned ${response.status}: ${errorText}`); } const result = (await response.json()) as ExecutionResult; return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to execute via worker: ${errorMessage}`); } } async function executeDirectly(config: LauncherConfig): Promise<ExecutionResult> { log('Executing with Claude Agent SDK...'); // Extract agent names for context const agents = config.componentsToInstall ? extractAgents(config.componentsToInstall) : []; try { log('Generating code with Claude Agent SDK...'); log(`Working directory: ${process.cwd()}`); // Detect if this is a web development request const isWebRequest = /html|css|javascript|webpage|website|form|ui|interface|frontend/i.test(config.prompt); const promptContent = isWebRequest ? `Create a complete web application for: "${config.prompt}" IMPORTANT FORMAT REQUIREMENTS: - Provide complete, working code for ALL files needed - Use this EXACT format for each file: \`\`\`html:index.html [your HTML code here] \`\`\` \`\`\`css:styles.css [your CSS code here] \`\`\` \`\`\`javascript:script.js [your JavaScript code here] \`\`\` Requirements: - Create a complete, functional web application - Include all necessary HTML, CSS, and JavaScript - Use modern, responsive design - Add proper comments - Ensure code is ready to run - Do NOT include any explanations, ONLY code blocks with filenames` : config.prompt; // Configure Claude Agent SDK options // Using settingSources: ['project'] to automatically load agents from .claude/ directory // This is supported in SDK version ^0.1.23 and later const options: ClaudeAgentOptions = { model: 'claude-sonnet-4-5', apiKey: config.anthropicApiKey, systemPrompt: { type: 'preset', preset: 'claude_code' }, // Automatically load agents, settings, and configurations from .claude/ directory settingSources: ['project'], }; if (agents.length > 0) { log(`Using agents from .claude/agents/ directory via settingSources`, 'success'); } // Collect the full response let generatedCode = ''; try { for await (const message of query({ prompt: promptContent, options })) { // The Agent SDK returns different message types: // - 'system': Initialization info // - 'assistant': Individual API responses // - 'result': Final aggregated result (THIS is what we need!) if (message.type === 'result' && message.result) { generatedCode = message.result; log(`Received result (${message.result.length} chars)`, 'success'); } else if (message.type === 'text' && message.text) { // Fallback for older SDK versions generatedCode += message.text; } } } catch (queryError) { const err = queryError as Error; log(`Query error: ${err.message}`, 'error'); log(`Stack: ${err.stack}`, 'error'); throw new Error(`Claude Agent SDK query failed: ${err.message}`); } if (!generatedCode) { throw new Error('Failed to generate code from Claude Agent SDK (empty response)'); } log('Code generated successfully', 'success'); // Note: Direct execution would require local Python runtime // For now, we return the code for manual execution or deployment return { success: true, question: config.prompt, code: generatedCode, output: 'Code generated. Deploy to Cloudflare Worker to execute.', error: '', }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Direct execution failed: ${errorMessage}`); } } function extractAgents(componentsString: string): string[] { const agents: string[] = []; const parts = componentsString.split('--'); for (const part of parts) { const trimmed = part.trim(); if (trimmed.startsWith('agent ')) { const agentNames = trimmed.substring(6).trim(); if (agentNames) { agents.push(...agentNames.split(',').map((a) => a.trim())); } } } return agents; } async function installAgents(agents: string[]): Promise<void> { if (agents.length === 0) { return; } log(`Installing ${agents.length} agent(s)...`); // Create .claude/agents directory const claudeDir = path.join(process.cwd(), '.claude'); const agentsDir = path.join(claudeDir, 'agents'); if (!fs.existsSync(agentsDir)) { fs.mkdirSync(agentsDir, { recursive: true }); } // Download each agent from GitHub const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents'; for (const agent of agents) { try { log(`Downloading agent: ${agent}...`); // Construct the GitHub URL for the agent const agentUrl = `${GITHUB_RAW_BASE}/${agent}.md`; // Download the agent file const response = await fetch(agentUrl); if (!response.ok) { log(`Failed to download agent ${agent}: ${response.statusText}`, 'warning'); continue; } const agentContent = await response.text(); // Save to .claude/agents/ const agentFileName = agent.replace(/\//g, '-') + '.md'; const agentPath = path.join(agentsDir, agentFileName); fs.writeFileSync(agentPath, agentContent, 'utf-8'); log(`Installed agent: ${agent}`, 'success'); } catch (error) { log(`Error installing agent ${agent}: ${error instanceof Error ? error.message : String(error)}`, 'warning'); } } // Create settings.json to reference the agents const settingsPath = path.join(claudeDir, 'settings.json'); const settings = { agents: agents.map(agent => ({ name: agent.split('/').pop() || agent, path: `.claude/agents/${agent.replace(/\//g, '-')}.md` })) }; fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8'); log('Created .claude/settings.json', 'success'); } function displayResults(result: ExecutionResult, targetDir?: string) { console.log(''); if (result.error) { console.log(`${colors.red}❌ Error:${colors.reset} ${result.error}`); console.log(''); return; } if (result.sandboxId) { log(`Sandbox ID: ${result.sandboxId}`); } // Extract and save files const files = extractFilesFromCode(result.code); if (files.length > 0) { // Generate unique directory name with timestamp (similar to E2B's sandbox-xxxxxxxx) const timestamp = Date.now().toString(36); const baseDir = targetDir || process.cwd(); const outputDir = path.join(baseDir, `cloudflare-${timestamp}`); console.log(''); printSeparator(); log(`Downloading ${files.length} file(s)...`); console.log(''); saveFilesToDirectory(files, outputDir); } } async function checkWorkerAvailability(url: string): Promise<boolean> { try { const response = await fetch(url, { method: 'GET' }); return response.ok || response.status === 405; // 405 is fine, means worker is up } catch { return false; } } async function main() { // Parse command line arguments const args = process.argv.slice(2); if (args.length < 1) { console.log('Cloudflare Sandbox Launcher'); console.log(''); console.log('Usage:'); console.log(' node launcher.ts <prompt> [components] [anthropic_api_key] [worker_url]'); console.log(''); console.log('Examples:'); console.log(' node launcher.ts "Calculate factorial of 5"'); console.log(' node launcher.ts "Create a React app" "--agent frontend-developer" YOUR_KEY'); console.log(' node launcher.ts "Fibonacci" "" YOUR_KEY https://your-worker.workers.dev'); console.log(''); console.log('Environment Variables:'); console.log(' ANTHROPIC_API_KEY - Anthropic API key'); console.log(' CLOUDFLARE_WORKER_URL - Cloudflare Worker endpoint'); process.exit(1); } const config: LauncherConfig = { prompt: args[0], componentsToInstall: args[1] || '', anthropicApiKey: args[2] || process.env.ANTHROPIC_API_KEY || '', workerUrl: args[3] || process.env.CLOUDFLARE_WORKER_URL || 'http://localhost:8787', targetDir: args[4] || process.cwd(), useLocalWorker: true, }; if (!config.anthropicApiKey) { log('Error: Anthropic API key is required', 'error'); console.log('Provide via command line argument or ANTHROPIC_API_KEY environment variable'); process.exit(1); } console.log(''); printSeparator(); console.log(`${colors.bright}☁️ CLOUDFLARE SANDBOX LAUNCHER${colors.reset}`); printSeparator(); console.log(''); log(`Prompt: "${config.prompt.substring(0, 100)}${config.prompt.length > 100 ? '...' : ''}"`); if (config.componentsToInstall) { const agents = extractAgents(config.componentsToInstall); if (agents.length > 0) { log(`Agents: ${agents.join(', ')}`); // Install agents before execution console.log(''); await installAgents(agents); } } console.log(''); try { // Check if worker is available log('Checking Cloudflare Worker availability...'); const workerAvailable = await checkWorkerAvailability(config.workerUrl || 'http://localhost:8787'); let result: ExecutionResult; if (workerAvailable) { log('Cloudflare Worker is available', 'success'); log('Executing via Cloudflare Sandbox...'); result = await executeViaWorker(config); } else { log('Cloudflare Worker not available, using direct execution', 'warning'); log('For full sandbox execution, deploy worker with: npx wrangler deploy', 'warning'); result = await executeDirectly(config); } displayResults(result, config.targetDir); if (!result.success) { process.exit(1); } } catch (error) { console.log(''); log(`Execution failed: ${error instanceof Error ? error.message : String(error)}`, 'error'); console.log(''); log('Troubleshooting:', 'info'); console.log('1. Ensure Cloudflare Worker is deployed: npx wrangler deploy'); console.log('2. Check API key is set: npx wrangler secret put ANTHROPIC_API_KEY'); console.log('3. Wait 2-3 minutes after first deployment for container provisioning'); console.log('4. Check container status: npx wrangler containers list'); console.log('5. For local testing: npm run dev'); process.exit(1); } } export { executeViaWorker, executeDirectly, type LauncherConfig, type ExecutionResult }; // Run if executed directly (ES modules compatible) if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error('Fatal error:', error); process.exit(1); }); }