tasq/node_modules/agentic-flow/dist/cli/claude-code-wrapper.js

351 lines
13 KiB
JavaScript

#!/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 <provider>', 'AI provider (anthropic, openrouter, gemini, onnx)', 'anthropic')
.option('--port <port>', 'Proxy server port', '3000')
.option('--model <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