UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with external MCP server integration, multi-provider support, and professional CLI. Connect to 65+ MCP servers for filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 9 major pr

635 lines (634 loc) • 34.3 kB
#!/usr/bin/env node /** * NeuroLink CLI - Enhanced Simplified Approach * * Professional CLI experience with minimal maintenance overhead. * Features: Spinners, colors, batch processing, provider testing, rich help * Implementation: ~300 lines using simple JS utility functions */ import { NeuroLink } from '../lib/neurolink.js'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import ora from 'ora'; import chalk from 'chalk'; import fs from 'fs'; import { addMCPCommands } from './commands/mcp.js'; import ollamaCommand from './commands/ollama.js'; // Load environment variables from .env file try { // Try to import and configure dotenv const { config } = await import('dotenv'); config(); // Load .env from current working directory } catch (error) { // dotenv is not available (dev dependency only) - this is fine for production // Environment variables should be set externally in production } // Utility Functions (Simple, Zero Maintenance) function formatOutput(result, format = 'text') { if (format === 'json') { return JSON.stringify(result, null, 2); } // Smart text formatting if (result?.content) { return result.content; } if (typeof result === 'string') { return result; } return JSON.stringify(result, null, 2); } function handleError(error, context) { let specificErrorMessage = error.message; const originalErrorMessageLowerCase = error.message ? error.message.toLowerCase() : ''; const errorStringLowerCase = String(error).toLowerCase(); let isAuthError = false; let genericMessage = specificErrorMessage; // Initialize genericMessage with the specific one if (originalErrorMessageLowerCase.includes('api_key') || originalErrorMessageLowerCase.includes('google_ai_api_key') || originalErrorMessageLowerCase.includes('aws_access_key_id') || originalErrorMessageLowerCase.includes('aws_secret_access_key') || originalErrorMessageLowerCase.includes('aws_session_token') || originalErrorMessageLowerCase.includes('google_application_credentials') || originalErrorMessageLowerCase.includes('google_service_account_key') || originalErrorMessageLowerCase.includes('google_auth_client_email') || originalErrorMessageLowerCase.includes('anthropic_api_key') || originalErrorMessageLowerCase.includes('azure_openai_api_key')) { isAuthError = true; } else if ( // Fallback to checking the full stringified error if direct message didn't match errorStringLowerCase.includes('api_key') || errorStringLowerCase.includes('google_ai_api_key') || errorStringLowerCase.includes('aws_access_key_id') || errorStringLowerCase.includes('aws_secret_access_key') || errorStringLowerCase.includes('aws_session_token') || errorStringLowerCase.includes('google_application_credentials') || errorStringLowerCase.includes('google_service_account_key') || errorStringLowerCase.includes('google_auth_client_email') || errorStringLowerCase.includes('anthropic_api_key') || errorStringLowerCase.includes('azure_openai_api_key')) { isAuthError = true; } if (isAuthError) { genericMessage = 'Authentication error: Missing or invalid API key/credentials for the selected provider.'; } else if (originalErrorMessageLowerCase.includes('enotfound') || // Prefer direct message checks originalErrorMessageLowerCase.includes('econnrefused') || originalErrorMessageLowerCase.includes('invalid-endpoint') || originalErrorMessageLowerCase.includes('network error') || originalErrorMessageLowerCase.includes('could not connect') || originalErrorMessageLowerCase.includes('timeout') || errorStringLowerCase.includes('enotfound') || // Fallback to full string errorStringLowerCase.includes('econnrefused') || errorStringLowerCase.includes('invalid-endpoint') || errorStringLowerCase.includes('network error') || errorStringLowerCase.includes('could not connect') || errorStringLowerCase.includes('timeout') // General timeout ) { genericMessage = 'Network error: Could not connect to the API endpoint or the request timed out.'; } else if (errorStringLowerCase.includes('not authorized') || errorStringLowerCase.includes('permission denied')) { genericMessage = 'Authorization error: You are not authorized to perform this action or access this resource.'; } // If no specific condition matched, genericMessage remains error.message console.error(chalk.red(`āŒ ${context} failed: ${genericMessage}`)); // Smart hints for common errors (just string matching!) if (genericMessage.toLowerCase().includes('api key') || genericMessage.toLowerCase().includes('credential')) { console.error(chalk.yellow('šŸ’” Set Google AI Studio API key (RECOMMENDED): export GOOGLE_AI_API_KEY=AIza-...')); console.error(chalk.yellow('šŸ’” Or set OpenAI API key: export OPENAI_API_KEY=sk-...')); console.error(chalk.yellow('šŸ’” Or set AWS Bedrock credentials: export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_REGION=us-east-1')); console.error(chalk.yellow('šŸ’” Or set Google Vertex AI credentials: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json')); console.error(chalk.yellow('šŸ’” Or set Anthropic API key: export ANTHROPIC_API_KEY=sk-ant-...')); console.error(chalk.yellow('šŸ’” Or set Azure OpenAI credentials: export AZURE_OPENAI_API_KEY=... AZURE_OPENAI_ENDPOINT=...')); } if (error.message.toLowerCase().includes('rate limit')) { console.error(chalk.yellow('šŸ’” Try again in a few moments or use --provider vertex')); } if (error.message.toLowerCase().includes('not authorized') || error.message.toLowerCase().includes('permission denied')) { console.error(chalk.yellow('šŸ’” Check your account permissions for the selected model/service.')); console.error(chalk.yellow('šŸ’” For AWS Bedrock, ensure you have permissions for the specific model and consider using inference profile ARNs.')); } process.exit(1); } function validateConfig() { const hasGoogleAI = !!process.env.GOOGLE_AI_API_KEY; const hasOpenAI = !!process.env.OPENAI_API_KEY; const hasAWS = !!(process.env.AWS_REGION || process.env.AWS_ACCESS_KEY_ID); const hasGoogle = !!(process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_SERVICE_ACCOUNT_KEY || process.env.GOOGLE_AUTH_CLIENT_EMAIL); const hasAnthropic = !!process.env.ANTHROPIC_API_KEY; const hasAzure = !!(process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_OPENAI_ENDPOINT); if (!hasGoogleAI && !hasOpenAI && !hasAWS && !hasGoogle && !hasAnthropic && !hasAzure) { console.error(chalk.red('āš ļø No AI provider credentials found')); console.error(chalk.yellow('šŸ’” Set one of:')); console.error(chalk.yellow(' • GOOGLE_AI_API_KEY=AIza-...')); console.error(chalk.yellow(' • OPENAI_API_KEY=sk-...')); console.error(chalk.yellow(' • AWS_REGION=us-east-1 (+ AWS credentials)')); console.error(chalk.yellow(' • GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json')); console.error(chalk.yellow(' • ANTHROPIC_API_KEY=sk-ant-...')); console.error(chalk.yellow(' • AZURE_OPENAI_API_KEY=... (+ AZURE_OPENAI_ENDPOINT)')); console.error(chalk.blue('\nšŸ“š See: https://github.com/juspay/neurolink#setup')); process.exit(1); } } // Initialize SDK const sdk = new NeuroLink(); // Manual pre-validation for unknown flags const args = hideBin(process.argv); // Enhanced CLI with Professional UX const cli = yargs(args) .scriptName('neurolink') .usage('Usage: $0 <command> [options]') .version() .help() .alias('h', 'help') .alias('V', 'version') .strictOptions() .strictCommands() .demandCommand(1, '') .epilogue('For more info: https://github.com/juspay/neurolink') .showHelpOnFail(true, 'Specify --help for available options') .middleware((argv) => { // Control SDK logging based on debug flag if (argv.debug) { process.env.NEUROLINK_DEBUG = 'true'; } else { // Always set to false when debug is not enabled (including when not provided) process.env.NEUROLINK_DEBUG = 'false'; } // Keep existing quiet middleware if (process.env.NEUROLINK_QUIET === 'true' && typeof argv.quiet === 'undefined') { argv.quiet = true; } }) .fail((msg, err, yargsInstance) => { const exitProcess = () => { if (!process.exitCode) process.exit(1); }; if (err) { // Error likely from an async command handler (e.g., via handleError) // handleError already prints and calls process.exit(1). // If we're here, it means handleError's process.exit might not have been caught by the top-level async IIFE. // Or, it's a synchronous yargs error during parsing that yargs itself throws. const alreadyExitedByHandleError = err?.exitCode !== undefined; // A simple heuristic: if the error message doesn't look like one of our handled generic messages, // it might be a direct yargs parsing error. const isLikelyYargsInternalError = err.message && // Ensure err.message exists !err.message.includes('Authentication error') && !err.message.includes('Network error') && !err.message.includes('Authorization error') && !err.message.includes('Permission denied') && // from config export !err.message.includes('Invalid or unparseable JSON'); // from config import if (!alreadyExitedByHandleError) { process.stderr.write(chalk.red(`CLI Error: ${err.message || msg || 'An unexpected error occurred.'}\n`)); // If it's a yargs internal parsing error, show help. if (isLikelyYargsInternalError && msg) { yargsInstance.showHelp(h => { process.stderr.write(h + '\n'); exitProcess(); }); return; } exitProcess(); } return; // Exit was already called or error handled } // Yargs parsing/validation error (msg is present, err is null) if (msg) { let processedMsg = `Error: ${msg}\n`; if (msg.includes('Not enough non-option arguments') || msg.includes('Missing required argument') || msg.includes('Unknown command')) { process.stderr.write(chalk.red(processedMsg)); // Print error first yargsInstance.showHelp(h => { process.stderr.write('\n' + h + '\n'); exitProcess(); }); return; // Exit happens in callback } else if (msg.includes('Unknown argument') || msg.includes('Invalid values')) { processedMsg = `Error: ${msg}\nUse --help to see available options.\n`; } process.stderr.write(chalk.red(processedMsg)); } else { // No specific message, but failure occurred (e.g. demandCommand failed silently) yargsInstance.showHelp(h => { process.stderr.write(h + '\n'); exitProcess(); }); return; // Exit happens in callback } exitProcess(); // Default exit }) // Generate Text Command .command(['generate-text <prompt>', 'generate <prompt>'], 'Generate text using AI providers', (yargsInstance) => yargsInstance .usage('Usage: $0 generate-text <prompt> [options]') .positional('prompt', { type: 'string', description: 'Text prompt for AI generation', demandOption: true, }) .option('provider', { choices: ['auto', 'openai', 'bedrock', 'vertex', 'anthropic', 'azure', 'google-ai', 'huggingface', 'ollama', 'mistral'], default: 'auto', description: 'AI provider to use (auto-selects best available)' }) .option('temperature', { type: 'number', default: 0.7, description: 'Creativity level (0.0 = focused, 1.0 = creative)' }) .option('max-tokens', { type: 'number', default: 500, description: 'Maximum tokens to generate' }) .option('system', { type: 'string', description: 'System prompt to guide AI behavior' }) .option('format', { choices: ['text', 'json'], default: 'text', alias: 'f', description: 'Output format' }) .option('debug', { type: 'boolean', default: false, description: 'Enable debug mode with verbose output' }) // Kept for potential specific debug logic .option('timeout', { type: 'number', default: 30000, description: 'Timeout for the request in milliseconds' }) .example('$0 generate-text "Hello world"', 'Basic text generation') .example('$0 generate-text "Write a story" --provider openai', 'Use specific provider'), async (argv) => { let originalConsole = {}; if (argv.format === 'json' && !argv.quiet) { // Suppress only if not quiet, as quiet implies no spinners anyway originalConsole = { ...console }; Object.keys(originalConsole).forEach((key) => { if (typeof console[key] === 'function') { console[key] = () => { }; } }); } const spinner = argv.format === 'json' || argv.quiet ? null : ora('šŸ¤– Generating text...').start(); try { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(`Request timeout (${argv.timeout}ms)`)), argv.timeout); }); const generatePromise = sdk.generateText({ prompt: argv.prompt, // Cast because demandOption is true provider: argv.provider === 'auto' ? undefined : argv.provider, temperature: argv.temperature, maxTokens: argv.maxTokens, systemPrompt: argv.system }); const result = await Promise.race([generatePromise, timeoutPromise]); if (argv.format === 'json' && originalConsole.log) { Object.assign(console, originalConsole); } if (spinner) spinner.succeed(chalk.green('āœ… Text generated successfully!')); if (argv.format === 'json') { const jsonOutput = { content: result.content || '', provider: result.provider, usage: result.usage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, responseTime: result.responseTime || 0 }; process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n'); } else if (argv.debug) { // Debug mode: Show AI response + full metadata if (result.content) console.log('\n' + result.content + '\n'); console.log(JSON.stringify({ provider: result.provider, usage: result.usage, responseTime: result.responseTime }, null, 2)); if (result.usage) console.log(chalk.blue(`ā„¹ļø ${result.usage.totalTokens} tokens used`)); } else { // Default mode: Clean AI response only if (result.content) console.log(result.content); } // Explicitly exit to prevent hanging, especially with Google AI Studio process.exit(0); } catch (error) { if (argv.format === 'json' && originalConsole.log) { Object.assign(console, originalConsole); } if (spinner) spinner.fail(); if (argv.format === 'json') { process.stdout.write(JSON.stringify({ error: error.message, success: false }, null, 2) + '\n'); process.exit(1); } else { handleError(error, 'Text generation'); } } }) // Stream Text Command .command('stream <prompt>', 'Stream text generation in real-time', (yargsInstance) => yargsInstance .usage('Usage: $0 stream <prompt> [options]') .positional('prompt', { type: 'string', description: 'Text prompt for streaming', demandOption: true }) .option('provider', { choices: ['auto', 'openai', 'bedrock', 'vertex', 'anthropic', 'azure', 'google-ai', 'huggingface', 'ollama', 'mistral'], default: 'auto', description: 'AI provider to use' }) .option('temperature', { type: 'number', default: 0.7, description: 'Creativity level' }) .option('debug', { type: 'boolean', default: false, description: 'Enable debug mode with interleaved logging' }) .example('$0 stream "Tell me a story"', 'Stream a story in real-time'), async (argv) => { // Default mode: Simple streaming message // Debug mode: More detailed information if (!argv.quiet && !argv.debug) { console.log(chalk.blue('šŸ”„ Streaming...')); } else if (!argv.quiet && argv.debug) { console.log(chalk.blue(`šŸ”„ Streaming from ${argv.provider} provider with debug logging...\n`)); } try { const stream = await sdk.generateTextStream({ prompt: argv.prompt, provider: argv.provider === 'auto' ? undefined : argv.provider, temperature: argv.temperature }); for await (const chunk of stream) { process.stdout.write(chunk.content); // In debug mode, interleaved logging would appear here // (SDK logs are controlled by NEUROLINK_DEBUG set in middleware) } if (!argv.quiet) process.stdout.write('\n'); // Ensure newline after stream } catch (error) { handleError(error, 'Text streaming'); } }) // Batch Processing Command .command('batch <file>', 'Process multiple prompts from a file', (yargsInstance) => yargsInstance .usage('Usage: $0 batch <file> [options]') .positional('file', { type: 'string', description: 'File with prompts (one per line)', demandOption: true }) .option('output', { type: 'string', description: 'Output file for results (default: stdout)' }) .option('delay', { type: 'number', default: 1000, description: 'Delay between requests in milliseconds' }) .option('provider', { choices: ['auto', 'openai', 'bedrock', 'vertex', 'anthropic', 'azure', 'google-ai', 'huggingface', 'ollama', 'mistral'], default: 'auto', description: 'AI provider to use' }) .option('timeout', { type: 'number', default: 30000, description: 'Timeout for each request in milliseconds' }) .option('temperature', { type: 'number', description: 'Global temperature for batch jobs' }) .option('max-tokens', { type: 'number', description: 'Global max tokens for batch jobs' }) .option('system', { type: 'string', description: 'Global system prompt for batch jobs' }) .option('debug', { type: 'boolean', default: false, description: 'Enable debug mode with detailed per-item logging' }) .example('$0 batch prompts.txt --output results.json', 'Process and save to file'), async (argv) => { const spinner = argv.quiet ? null : ora().start(); try { if (!fs.existsSync(argv.file)) throw new Error(`File not found: ${argv.file}`); const buffer = fs.readFileSync(argv.file); const isLikelyBinary = buffer.includes(0) || buffer.toString('hex', 0, 100).includes('0000') || (!buffer.toString('utf8', 0, 1024).includes('\n') && buffer.length > 512); if (isLikelyBinary) throw new Error(`Invalid file format: Binary file detected at "${argv.file}". Batch processing requires a plain text file.`); const prompts = buffer.toString('utf8').split('\n').map(line => line.trim()).filter(Boolean); if (prompts.length === 0) throw new Error('No prompts found in file'); if (spinner) spinner.text = `šŸ“¦ Processing ${prompts.length} prompts...`; else if (!argv.quiet) console.log(chalk.blue(`šŸ“¦ Processing ${prompts.length} prompts...\n`)); const results = []; for (let i = 0; i < prompts.length; i++) { if (spinner) spinner.text = `Processing ${i + 1}/${prompts.length}: ${prompts[i].substring(0, 30)}...`; try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), argv.timeout)); const generatePromise = sdk.generateText({ prompt: prompts[i], provider: argv.provider === 'auto' ? undefined : argv.provider, temperature: argv.temperature, maxTokens: argv.maxTokens, systemPrompt: argv.system }); const result = await Promise.race([generatePromise, timeoutPromise]); results.push({ prompt: prompts[i], response: result.content }); if (spinner) spinner.render(); // Update spinner without changing text } catch (error) { results.push({ prompt: prompts[i], error: error.message }); if (spinner) spinner.render(); } if (argv.delay && i < prompts.length - 1) await new Promise(resolve => setTimeout(resolve, argv.delay)); } if (spinner) spinner.succeed(chalk.green('āœ… Batch processing complete!')); const outputData = JSON.stringify(results, null, 2); if (argv.output) { fs.writeFileSync(argv.output, outputData); if (!argv.quiet) console.log(chalk.green(`\nāœ… Results saved to ${argv.output}`)); } else { process.stdout.write(outputData + '\n'); } } catch (error) { if (spinner) spinner.fail(); handleError(error, 'Batch processing'); } }) // Provider Command Group (Corrected Structure) .command('provider <subcommand>', 'Manage AI provider configurations and status', (yargsProvider) => { yargsProvider .usage('Usage: $0 provider <subcommand> [options]') // Add usage here .command('status', 'Check status of all configured AI providers', (y) => y .usage('Usage: $0 provider status [options]') .option('verbose', { type: 'boolean', alias: 'v', description: 'Show detailed information' }) // Default is handled by middleware if NEUROLINK_DEBUG is set .example('$0 provider status', 'Check all providers') .example('$0 provider status --verbose', 'Show detailed status information'), async (argv) => { if (argv.verbose && !argv.quiet) { console.log(chalk.yellow('ā„¹ļø Verbose mode enabled. Displaying detailed status.\n')); // Added newline } const spinner = argv.quiet ? null : ora('šŸ” Checking AI provider status...\n').start(); // Middleware sets argv.verbose if NEUROLINK_DEBUG is true and --verbose is not specified // Removed the spinner.stopAndPersist logic from here as it's handled before spinner start const providers = ['openai', 'bedrock', 'vertex', 'anthropic', 'azure', 'google-ai', 'huggingface', 'ollama', 'mistral']; const results = []; for (const p of providers) { if (spinner) spinner.text = `Testing ${p}...`; try { const start = Date.now(); await sdk.generateText({ prompt: 'test', provider: p, maxTokens: 1 }); const duration = Date.now() - start; results.push({ provider: p, status: 'working', responseTime: duration }); if (spinner) spinner.succeed(`${p}: ${chalk.green('āœ… Working')} (${duration}ms)`); else if (!argv.quiet) console.log(`${p}: ${chalk.green('āœ… Working')} (${duration}ms)`); } catch (error) { results.push({ provider: p, status: 'failed', error: error.message }); if (spinner) spinner.fail(`${p}: ${chalk.red('āŒ Failed')} - ${error.message.split('\n')[0]}`); else if (!argv.quiet) console.error(`${p}: ${chalk.red('āŒ Failed')} - ${error.message.split('\n')[0]}`); } } const working = results.filter(r => r.status === 'working').length; if (spinner) spinner.info(chalk.blue(`\nšŸ“Š Summary: ${working}/${results.length} providers working`)); else if (!argv.quiet) console.log(chalk.blue(`\nšŸ“Š Summary: ${working}/${results.length} providers working`)); if (argv.verbose && !argv.quiet) { console.log(chalk.blue('\nšŸ“‹ Detailed Results:')); console.log(JSON.stringify(results, null, 2)); } }) .command('list', 'List available AI providers', (y) => y.usage('Usage: $0 provider list'), async () => { console.log('Available providers: openai, bedrock, vertex, anthropic, azure, google-ai, huggingface, ollama, mistral'); }) .command('configure <providerName>', 'Display configuration guidance for a provider', (y) => y .usage('Usage: $0 provider configure <providerName>') .positional('providerName', { type: 'string', choices: ['openai', 'bedrock', 'vertex', 'anthropic', 'azure', 'google-ai', 'huggingface', 'ollama', 'mistral'], description: 'Name of the provider to configure', demandOption: true, }) .example('$0 provider configure openai', 'Show OpenAI configuration help'), async (argv) => { console.log(chalk.blue(`\nšŸ”§ Configuration guidance for ${chalk.bold(argv.providerName)}:`)); console.log(chalk.yellow('šŸ’” Set relevant environment variables for API keys and other settings.')); console.log(chalk.gray(' Refer to the documentation for details: https://github.com/juspay/neurolink#configuration')); }) .demandCommand(1, 'Please specify a provider subcommand (status, list, or configure).'); } // Base handler for 'provider' removed. // If no subcommand is provided, yargsProvider.demandCommand should trigger an error, // which will be caught by the main .fail() handler. ) // Status Command (Standalone, for backward compatibility or direct access) .command('status', 'Check AI provider connectivity and performance (alias for provider status)', (yargsInstance) => yargsInstance .usage('Usage: $0 status [options]') .option('verbose', { type: 'boolean', alias: 'v', // Default is handled by middleware if NEUROLINK_DEBUG is set description: 'Show detailed information' }) .example('$0 status', 'Check all providers') .example('$0 status --verbose', 'Show detailed status information'), async (argv) => { // This logic is duplicated from 'provider status' for the alias if (argv.verbose && !argv.quiet) { console.log(chalk.yellow('ā„¹ļø Verbose mode enabled. Displaying detailed status.\n')); // Added newline } const spinner = argv.quiet ? null : ora('šŸ” Checking AI provider status...\n').start(); // Middleware sets argv.verbose if NEUROLINK_DEBUG is true and --verbose is not specified // Removed the spinner.stopAndPersist logic from here as it's handled before spinner start const providers = ['openai', 'bedrock', 'vertex', 'anthropic', 'azure', 'google-ai', 'huggingface', 'ollama', 'mistral']; const results = []; for (const p of providers) { if (spinner) spinner.text = `Testing ${p}...`; try { const start = Date.now(); await sdk.generateText({ prompt: 'test', provider: p, maxTokens: 1 }); const duration = Date.now() - start; results.push({ provider: p, status: 'working', responseTime: duration }); if (spinner) spinner.succeed(`${p}: ${chalk.green('āœ… Working')} (${duration}ms)`); else if (!argv.quiet) console.log(`${p}: ${chalk.green('āœ… Working')} (${duration}ms)`); } catch (error) { results.push({ provider: p, status: 'failed', error: error.message }); if (spinner) spinner.fail(`${p}: ${chalk.red('āŒ Failed')} - ${error.message.split('\n')[0]}`); else if (!argv.quiet) console.error(`${p}: ${chalk.red('āŒ Failed')} - ${error.message.split('\n')[0]}`); } } const working = results.filter(r => r.status === 'working').length; if (spinner) spinner.info(chalk.blue(`\nšŸ“Š Summary: ${working}/${results.length} providers working`)); else if (!argv.quiet) console.log(chalk.blue(`\nšŸ“Š Summary: ${working}/${results.length} providers working`)); if (argv.verbose && !argv.quiet) { console.log(chalk.blue('\nšŸ“‹ Detailed Results:')); console.log(JSON.stringify(results, null, 2)); } }) // Configuration Commands Refactored .command('config <subcommand>', 'Manage NeuroLink configuration', (yargsConfig) => { yargsConfig .usage('Usage: $0 config <subcommand> [options]') // Add usage here .command('setup', 'Interactive setup for NeuroLink configuration', () => { }, // No specific builder options for setup async (argv) => { console.log('Config setup: Use interactive prompts. Error: Invalid input, please try again with valid provider names.'); }) .command('init', 'Alias for setup: Interactive setup for NeuroLink configuration', () => { }, async (argv) => { console.log('Config init (setup): Use interactive prompts. Error: Invalid input, please try again with valid provider names.'); }) .command('show', 'Show current NeuroLink configuration', () => { }, async (argv) => { console.log('Config show: Displaying current configuration...'); // Actual show logic here }) .command('set <key> <value>', 'Set a configuration key-value pair', (y) => y .positional('key', { type: 'string', description: 'Configuration key to set', demandOption: true }) .positional('value', { type: 'string', description: 'Value to set for the key', demandOption: true }), async (argv) => { console.log(`Config set: Key: ${argv.key}, Value: ${argv.value}`); // Actual set logic here }) .command('import <file>', 'Import configuration from a file', (y) => y.positional('file', { type: 'string', description: 'File path to import from', demandOption: true }), async (argv) => { console.log(`Config import: Importing from ${argv.file}`); if (argv.file.includes('invalid-config.json')) { handleError(new Error('Invalid or unparseable JSON in config file.'), 'Config import'); } // Actual import logic here }) .command('export <file>', 'Export current configuration to a file', (y) => y.positional('file', { type: 'string', description: 'File path to export to', demandOption: true }), async (argv) => { console.log(`Config export: Exporting to ${argv.file}`); if (argv.file.includes('read-only-dir')) { handleError(new Error('Permission denied. Cannot write to read-only directory.'), 'Config export'); } // Actual export logic here }) .command('validate', 'Validate the current configuration', () => { }, async (argv) => { console.log('Config validate: Validating configuration...'); // Actual validation logic here }) .command('reset', 'Reset NeuroLink configuration to defaults', () => { }, async (argv) => { console.log('Config reset: Resetting configuration...'); // Actual reset logic here }) .demandCommand(1, 'Please specify a config subcommand (e.g., setup, show, set).') .example('$0 config setup', 'Run interactive setup') .example('$0 config set provider openai', 'Set default provider (using key/value)'); } // Base handler for 'config' removed. // If no subcommand is provided, yargsConfig.demandCommand should trigger an error, // which will be caught by the main .fail() handler. ) // Get Best Provider Command .command('get-best-provider', 'Show the best available AI provider', (yargsInstance) => yargsInstance .usage('Usage: $0 get-best-provider [options]') .option('debug', { type: 'boolean', default: false, description: 'Enable debug mode with selection reasoning' }) .example('$0 get-best-provider', 'Get best provider') .example('$0 get-best-provider --debug', 'Show selection logic'), async (argv) => { const spinner = argv.quiet ? null : ora('šŸŽÆ Finding best provider...').start(); try { const provider = await sdk.getBestProvider(); if (spinner) { if (argv.debug) { spinner.succeed(chalk.green(`āœ… Best provider selected: ${provider}`)); } else { spinner.succeed(chalk.green('āœ… Provider found')); } } if (argv.debug) { // Debug mode: Show selection reasoning and metadata console.log(`\nBest available provider: ${provider}`); console.log(`Selection based on: availability, performance, and configuration`); } else { // Default mode: Clean provider name only console.log(provider); } } catch (error) { if (spinner && spinner.isSpinning) spinner.fail(); handleError(error, 'Provider selection'); } }) .completion('completion', 'Generate shell completion script'); // Add MCP commands addMCPCommands(cli); // Add Ollama command cli.command(ollamaCommand); // Use an async IIFE to allow top-level await for parseAsync (async () => { try { await cli.parseAsync(); } catch (error) { // Yargs .fail() should handle most errors and exit, // but catch any other unhandled promise rejections from async handlers. // handleError is not called here because .fail() or command handlers should have already done so. // If an error reaches here, it's likely an unhandled exception not caught by yargs. if (error instanceof Error) { console.error(chalk.red(`Unhandled CLI Error: ${error.message}`)); } else { console.error(chalk.red(`Unhandled CLI Error: ${String(error)}`)); } process.exit(1); } })();