#!/usr/bin/env node // universal-developer/src/cli.ts import { program } from 'commander'; import { UniversalLLM } from './index'; import * as fs from 'fs'; import * as path from 'path'; import chalk from 'chalk'; import ora from 'ora'; import * as dotenv from 'dotenv'; import * as os from 'os'; import * as readline from 'readline'; import { createSpinner } from 'nanospinner'; // Load environment variables dotenv.config(); // Load package.json for version info const packageJson = JSON.parse( fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8') ); // Check for config file in user's home directory const configDir = path.join(os.homedir(), '.universal-developer'); const configPath = path.join(configDir, 'config.json'); let config: any = { defaultProvider: 'anthropic', enableTelemetry: true, apiKeys: {} }; // Create config directory if it doesn't exist if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // Load config if it exists if (fs.existsSync(configPath)) { try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (error) { console.error('Error loading config file:', error); } } // Save config function function saveConfig() { try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); } catch (error) { console.error('Error saving config:', error); } } // Configure CLI program .name('ud') .description('Universal Developer CLI - Control LLMs with symbolic runtime commands') .version(packageJson.version); // Configure command program .command('config') .description('Configure Universal Developer CLI') .option('-p, --provider ', 'Set default provider (anthropic, openai, qwen, gemini, ollama)') .option('-k, --key ', 'Set API key for the default provider') .option('--anthropic-key ', 'Set API key for Anthropic/Claude') .option('--openai-key ', 'Set API key for OpenAI') .option('--qwen-key ', 'Set API key for Qwen') .option('--gemini-key ', 'Set API key for Google Gemini') .option('--telemetry ', 'Enable or disable anonymous telemetry') .option('-l, --list', 'List current configuration') .action((options) => { if (options.list) { console.log(chalk.bold('\nCurrent Configuration:')); console.log(`Default Provider: ${chalk.green(config.defaultProvider)}`); console.log(`Telemetry: ${config.enableTelemetry ? chalk.green('Enabled') : chalk.yellow('Disabled')}`); console.log('\nAPI Keys:'); for (const [provider, key] of Object.entries(config.apiKeys)) { console.log(`${provider}: ${key ? chalk.green('Configured') : chalk.red('Not configured')}`); } return; } let changed = false; if (options.provider) { const validProviders = ['anthropic', 'openai', 'qwen', 'gemini', 'ollama']; if (validProviders.includes(options.provider)) { config.defaultProvider = options.provider; changed = true; console.log(`Default provider set to ${chalk.green(options.provider)}`); } else { console.error(`Invalid provider: ${options.provider}. Valid options are: ${validProviders.join(', ')}`); } } if (options.key) { if (!config.apiKeys) config.apiKeys = {}; config.apiKeys[config.defaultProvider] = options.key; changed = true; console.log(`API key for ${chalk.green(config.defaultProvider)} has been set`); } // Provider-specific keys const providerKeys = { 'anthropic': options.anthropicKey, 'openai': options.openaiKey, 'qwen': options.qwenKey, 'gemini': options.geminiKey }; for (const [provider, key] of Object.entries(providerKeys)) { if (key) { if (!config.apiKeys) config.apiKeys = {}; config.apiKeys[provider] = key; changed = true; console.log(`API key for ${chalk.green(provider)} has been set`); } } if (options.telemetry !== undefined) { const enableTelemetry = options.telemetry === 'true'; config.enableTelemetry = enableTelemetry; changed = true; console.log(`Telemetry ${enableTelemetry ? chalk.green('enabled') : chalk.yellow('disabled')}`); } if (changed) { saveConfig(); console.log(chalk.bold('\nConfiguration saved!')); } else { console.log('No changes made. Use --help to see available options.'); } }); // Helper function to handle piped input async function getPipedInput(): Promise { if (process.stdin.isTTY) { return null; } return new Promise((resolve) => { let data = ''; process.stdin.on('readable', () => { const chunk = process.stdin.read(); if (chunk !== null) { data += chunk; } }); process.stdin.on('end', () => { resolve(data); }); }); } // Helper to get API key for a provider function getApiKey(provider: string): string { // First check config if (config.apiKeys && config.apiKeys[provider]) { return config.apiKeys[provider]; } // Then check environment variables const envVarName = `${provider.toUpperCase()}_API_KEY`; const apiKey = process.env[envVarName]; if (!apiKey) { console.error(chalk.red(`Error: No API key found for ${provider}.`)); console.log(`Please set your API key using: ud config --${provider}-key `); console.log(`Or set the ${envVarName} environment variable.`); process.exit(1); } return apiKey; } // Interactive mode program .command('interactive') .alias('i') .description('Start an interactive session') .option('-p, --provider ', 'LLM provider to use') .option('-m, --model ', 'Model to use') .action(async (options) => { const provider = options.provider || config.defaultProvider; const apiKey = getApiKey(provider); const llm = new UniversalLLM({ provider, apiKey, model: options.model, telemetryEnabled: config.enableTelemetry }); console.log(chalk.bold('\nUniversal Developer Interactive Mode')); console.log(chalk.dim(`Using provider: ${provider}`)); console.log(chalk.dim('Type /exit or Ctrl+C to quit')); console.log(chalk.dim('Available commands: /think, /fast, /loop, /reflect, /fork, /collapse\n')); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); let conversationHistory: { role: string, content: string }[] = []; const promptUser = () => { rl.question('> ', async (input) => { if (input.toLowerCase() === '/exit') { rl.close(); return; } // Store user message conversationHistory.push({ role: 'user', content: input }); const spinner = createSpinner('Generating response...').start(); try { const response = await llm.generate({ messages: conversationHistory }); spinner.success(); console.log(`\n${chalk.blue('Assistant:')} ${response}\n`); // Store assistant response conversationHistory.push({ role: 'assistant', content: response }); } catch (error) { spinner.error(); console.error(`Error: ${error.message}`); } promptUser(); }); }; console.log(chalk.blue('Assistant:') + ' Hello! How can I help you today?\n'); conversationHistory.push({ role: 'assistant', content: 'Hello! How can I help you today?' }); promptUser(); }); // Command for each symbolic operation const symbolicCommands = [ { name: 'think', description: 'Generate response using deep reasoning' }, { name: 'fast', description: 'Generate quick, concise response' }, { name: 'loop', description: 'Generate iteratively refined response' }, { name: 'reflect', description: 'Generate response with self-reflection' }, { name: 'fork', description: 'Generate multiple alternative responses' }, { name: 'collapse', description: 'Generate response using default behavior' } ]; symbolicCommands.forEach(cmd => { program .command(cmd.name) .description(cmd.description) .argument('[prompt]', 'The prompt to send to the LLM') .option('-p, --provider ', 'LLM provider to use') .option('-m, --model ', 'Model to use') .option('-s, --system ', 'System prompt to use') .option('-i, --iterations ', 'Number of iterations (for loop command)') .option('-c, --count ', 'Number of alternatives (for fork command)') .action(async (promptArg, options) => { // Get provider from options or config const provider = options.provider || config.defaultProvider; const apiKey = getApiKey(provider); // Initialize LLM const llm = new UniversalLLM({ provider, apiKey, model: options.model, telemetryEnabled: config.enableTelemetry }); // Check for piped input const pipedInput = await getPipedInput(); // Combine prompt argument and piped input let prompt = promptArg || ''; if (pipedInput) { prompt = prompt ? `${prompt}\n\n${pipedInput}` : pipedInput; } // If no prompt provided, show help if (!prompt) { console.error('Error: Prompt is required.'); console.log(`Usage: ud ${cmd.name} "Your prompt here"`); console.log('Or pipe content: cat file.txt | ud ${cmd.name}'); process.exit(1); } // Build command string let commandString = `/${cmd.name}`; // Add command-specific parameters if (cmd.name === 'loop' && options.iterations) { commandString += ` --iterations=${options.iterations}`; } else if (cmd.name === 'fork' && options.count) { commandString += ` --count=${options.count}`; } // Add the prompt const fullPrompt = `${commandString} ${prompt}`; // Show what's happening console.log(chalk.dim(`Using provider: ${provider}`)); const spinner = createSpinner('Generating response...').start(); try { const response = await llm.generate({ systemPrompt: options.system, prompt: fullPrompt }); spinner.success(); console.log('\n' + response + '\n'); } catch (error) { spinner.error(); console.error(`Error: ${error.message}`); process.exit(1); } }); }); // Default command (no subcommand specified) program .arguments('[prompt]') .option('-p, --provider ', 'LLM provider to use') .option('-m, --model ', 'Model to use') .option('-s, --system ', 'System prompt to use') .option('-c, --command ', 'Symbolic command to use') .action(async (promptArg, options) => { if (!promptArg && !process.stdin.isTTY) { // No prompt argument but has piped input const pipedInput = await getPipedInput(); if (pipedInput) { promptArg = pipedInput; } } if (!promptArg) { // No prompt provided, show interactive mode program.commands.find(cmd => cmd.name() === 'interactive').action(options); return; } // Get provider from options or config const provider = options.provider || config.defaultProvider; const apiKey = getApiKey(provider); // Initialize LLM const llm = new UniversalLLM({ provider, apiKey, model: options.model, telemetryEnabled: config.enableTelemetry }); // Default to think command if none specified const command = options.command || 'think'; // Format prompt with command const fullPrompt = `/${command} ${promptArg}`; // Show what's happening console.log(chalk.dim(`Using provider: ${provider}`)); const spinner = createSpinner('Generating response...').start(); try { const response = await llm.generate({ systemPrompt: options.system, prompt: fullPrompt }); spinner.success(); console.log('\n' + response + '\n'); } catch (error) { spinner.error(); console.error(`Error: ${error.message}`); process.exit(1); } }); // Parse arguments program.parse();