#!/usr/bin/env node /** * Claude Code Wrapper for Agentic Flow * * Automatically spawns Claude Code with the correct ANTHROPIC_BASE_URL * and environment variables based on the provider/args used. * * Usage: * npx agentic-flow claude-code --provider openrouter "Write a function" * npx agentic-flow claude-code --provider gemini "Create a REST API" * npx agentic-flow claude-code --provider anthropic "Help me debug" * * Features: * - Auto-starts proxy server in background if not running * - Sets ANTHROPIC_BASE_URL to proxy endpoint * - Configures provider-specific API keys * - Supports all Claude Code native arguments * - Cleans up proxy on exit (optional) */ import { spawn } from 'child_process'; import { Command } from 'commander'; import * as dotenv from 'dotenv'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { logger } from '../utils/logger.js'; // Load environment variables from root .env const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); dotenv.config({ path: resolve(__dirname, '../../../.env') }); /** * Get proxy configuration based on provider */ function getProxyConfig(provider, customPort) { const port = customPort || 3000; const baseUrl = `http://localhost:${port}`; switch (provider.toLowerCase()) { case 'openrouter': return { provider: 'openrouter', port, baseUrl, model: process.env.COMPLETION_MODEL || 'deepseek/deepseek-chat', apiKey: process.env.OPENROUTER_API_KEY || '', requiresProxy: true }; case 'requesty': return { provider: 'requesty', port, baseUrl, model: process.env.COMPLETION_MODEL || 'deepseek/deepseek-chat', apiKey: process.env.REQUESTY_API_KEY || '', requiresProxy: true }; case 'gemini': return { provider: 'gemini', port, baseUrl, model: process.env.GEMINI_MODEL || 'gemini-2.0-flash-exp', apiKey: process.env.GOOGLE_GEMINI_API_KEY || '', requiresProxy: true }; case 'onnx': return { provider: 'onnx', port, baseUrl, model: 'onnx-local', apiKey: 'dummy', requiresProxy: true }; case 'anthropic': default: return { provider: 'anthropic', port: 0, baseUrl: 'https://api.anthropic.com', apiKey: process.env.ANTHROPIC_API_KEY || '', requiresProxy: false }; } } /** * Check if proxy server is already running */ async function isProxyRunning(port) { try { const response = await fetch(`http://localhost:${port}/health`); return response.ok; } catch { return false; } } /** * Start the proxy server in background using the same approach as the agent */ async function startProxyServer(config) { if (!config.requiresProxy) { return null; } // Check if already running const running = await isProxyRunning(config.port); if (running) { logger.info(`Proxy already running on port ${config.port}`); return null; } logger.info(`Starting ${config.provider} proxy on port ${config.port}...`); let proxy; if (config.provider === 'gemini') { const { AnthropicToGeminiProxy } = await import('../proxy/anthropic-to-gemini.js'); proxy = new AnthropicToGeminiProxy({ geminiApiKey: config.apiKey, defaultModel: config.model || 'gemini-2.0-flash-exp' }); } else if (config.provider === 'onnx') { const { AnthropicToONNXProxy } = await import('../proxy/anthropic-to-onnx.js'); proxy = new AnthropicToONNXProxy({ port: config.port, modelPath: process.env.ONNX_MODEL_PATH, executionProviders: process.env.ONNX_EXECUTION_PROVIDERS?.split(',') || ['cpu'] }); } else { // OpenRouter - DeepSeek Chat: cheap ($0.14/M), fast, supports tools, good quality const { AnthropicToOpenRouterProxy } = await import('../proxy/anthropic-to-openrouter.js'); proxy = new AnthropicToOpenRouterProxy({ openrouterApiKey: config.apiKey, openrouterBaseUrl: process.env.ANTHROPIC_PROXY_BASE_URL, defaultModel: config.model || 'deepseek/deepseek-chat' }); } // Start proxy proxy.start(config.port); console.log(`🔗 Proxy Mode: ${config.provider}`); console.log(`🔧 Proxy URL: ${config.baseUrl}`); console.log(`🤖 Default Model: ${config.model}\n`); // Wait for proxy to be ready await new Promise(resolve => setTimeout(resolve, 1500)); return proxy; } /** * Spawn Claude Code with configured environment */ function spawnClaudeCode(config, claudeArgs) { logger.info('Starting Claude Code...'); logger.info(`Provider: ${config.provider}`); logger.info(`Base URL: ${config.baseUrl}`); if (config.model) { logger.info(`Model: ${config.model}`); } // Build environment variables const env = { ...process.env }; if (config.requiresProxy) { // Using proxy - set base URL and realistic dummy key // Use a properly formatted key that won't trigger Claude's validation warnings env.ANTHROPIC_BASE_URL = config.baseUrl; env.ANTHROPIC_API_KEY = 'sk-ant-api03-proxy-forwarded-to-' + config.provider + '-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // Set provider-specific keys if (config.provider === 'openrouter') { env.OPENROUTER_API_KEY = config.apiKey; } else if (config.provider === 'gemini') { env.GOOGLE_GEMINI_API_KEY = config.apiKey; } } else { // Direct Anthropic env.ANTHROPIC_API_KEY = config.apiKey; if (env.ANTHROPIC_BASE_URL) { delete env.ANTHROPIC_BASE_URL; } } logger.debug('Environment variables:', { ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL || '(default)', ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY?.substring(0, 10) + '...', OPENROUTER_API_KEY: env.OPENROUTER_API_KEY ? '(set)' : '(not set)', GOOGLE_GEMINI_API_KEY: env.GOOGLE_GEMINI_API_KEY ? '(set)' : '(not set)' }); // Spawn Claude Code const claudeProcess = spawn('claude', claudeArgs, { env: env, stdio: 'inherit' }); return claudeProcess; } /** * Main CLI function */ async function main() { const program = new Command(); program .name('agentic-flow claude-code') .description('Spawn Claude Code with automatic proxy configuration for alternative AI providers') .usage('[options] [task]') .addHelpText('after', ` Examples: # Interactive mode - Opens Claude Code UI with proxy $ agentic-flow claude-code --provider openrouter $ agentic-flow claude-code --provider gemini # Non-interactive mode - Execute task and exit $ agentic-flow claude-code --provider openrouter "Write a Python hello world function" $ agentic-flow claude-code --provider openrouter --model "mistralai/mistral-small-3.1-24b-instruct" "Create REST API" # Using different providers $ agentic-flow claude-code --provider openrouter # Uses Mistral Small (default, $0.02/M tokens) $ agentic-flow claude-code --provider gemini # Uses Gemini 2.0 Flash $ agentic-flow claude-code --provider onnx # Uses local ONNX models (free) # With Agent Booster for 57x faster code edits $ agentic-flow claude-code --provider openrouter --agent-booster Recommended Models: OpenRouter: mistralai/mistral-small-3.1-24b-instruct (default, $0.02/M, 128k context, optimized for tools) anthropic/claude-3.5-sonnet ($3/M, highest quality, large context) google/gemini-2.0-flash-exp:free (FREE tier, rate limited) Note: Models with <128k context will fail with tool definitions (Claude sends 35k+ tokens) Environment Variables: OPENROUTER_API_KEY Required for --provider openrouter GOOGLE_GEMINI_API_KEY Required for --provider gemini ANTHROPIC_API_KEY Required for --provider anthropic (default) ONNX_MODEL_PATH Optional for --provider onnx Documentation: https://github.com/ruvnet/agentic-flow#claude-code-mode https://ruv.io `) .option('--provider ', 'AI provider (anthropic, openrouter, gemini, onnx)', 'anthropic') .option('--port ', 'Proxy server port', '3000') .option('--model ', 'Specific model to use (e.g., mistralai/mistral-small-3.1-24b-instruct)') .option('--agent-booster', 'Enable Agent Booster MCP tools for 57x faster code edits (default: true)', true) .option('--keep-proxy', 'Keep proxy running after Claude Code exits') .option('--no-auto-start', 'Skip proxy startup (use existing proxy)') .allowUnknownOption(true) .allowExcessArguments(true); program.parse(process.argv); const options = program.opts(); // Get provider configuration const config = getProxyConfig(options.provider, parseInt(options.port)); // Override model if specified if (options.model) { config.model = options.model; } // Validate API keys if (config.requiresProxy && !config.apiKey) { console.error(`❌ Error: Missing API key for ${config.provider}`); console.error(` Please set ${config.provider === 'openrouter' ? 'OPENROUTER_API_KEY' : 'GOOGLE_GEMINI_API_KEY'}`); process.exit(1); } if (!config.requiresProxy && !config.apiKey) { console.error('❌ Error: Missing ANTHROPIC_API_KEY'); process.exit(1); } // Get Claude Code arguments (filter out wrapper-specific flags only) const wrapperFlags = new Set(['--provider', '--port', '--model', '--agent-booster', '--keep-proxy', '--no-auto-start']); const wrapperValues = new Set([options.provider, options.port, options.model]); const claudeArgs = []; let skipNext = false; for (let i = 2; i < process.argv.length; i++) { const arg = process.argv[i]; if (skipNext) { skipNext = false; continue; } // Check if this is a wrapper flag const isWrapperFlag = Array.from(wrapperFlags).some(flag => arg.startsWith(flag)); if (isWrapperFlag) { // Skip this flag and its value if it has one if (!arg.includes('=') && i + 1 < process.argv.length && !process.argv[i + 1].startsWith('-')) { skipNext = true; } continue; } // Keep all other arguments claudeArgs.push(arg); } // Auto-detect non-interactive mode: if there's a task string and no -p flag, add it // Claude expects: claude [prompt] [flags], not claude [flags] [prompt] const hasTaskString = claudeArgs.some(arg => !arg.startsWith('-')); const hasPrintFlag = claudeArgs.includes('-p') || claudeArgs.includes('--print'); if (hasTaskString && !hasPrintFlag) { // Find the prompt (first non-flag argument) const promptIndex = claudeArgs.findIndex(arg => !arg.startsWith('-')); if (promptIndex !== -1) { // Insert -p after the prompt claudeArgs.splice(promptIndex + 1, 0, '-p'); } } let proxyServer = null; try { // Info about Agent Booster MCP tools if enabled if (options.agentBooster) { logger.info(''); logger.info('⚡ Agent Booster enabled (57x faster code edits)'); logger.info(' Available tools: agent_booster_edit_file, agent_booster_batch_edit, agent_booster_parse_markdown'); logger.info(' Configure MCP: Add "agentic-flow" to Claude Desktop config'); logger.info(' Learn more: examples/mcp-integration.md'); logger.info(''); } // Start proxy if needed and auto-start is enabled if (options.autoStart) { proxyServer = await startProxyServer(config); } // Spawn Claude Code const claudeProcess = spawnClaudeCode(config, claudeArgs); // Handle cleanup on exit const cleanup = () => { if (proxyServer && !options.keepProxy) { logger.info('Stopping proxy server...'); if (proxyServer.stop) { proxyServer.stop(); } } }; claudeProcess.on('exit', (code) => { cleanup(); process.exit(code || 0); }); process.on('SIGINT', () => { claudeProcess.kill('SIGINT'); cleanup(); }); process.on('SIGTERM', () => { claudeProcess.kill('SIGTERM'); cleanup(); }); } catch (error) { console.error('❌ Error:', error.message); if (proxyServer && proxyServer.stop) { proxyServer.stop(); } process.exit(1); } } // Run if executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error('❌ Fatal error:', error); process.exit(1); }); } //# sourceMappingURL=claude-code-wrapper.js.map