UNPKG

@lanonasis/cli

Version:

Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security

737 lines (736 loc) 33.9 kB
import chalk from 'chalk'; import inquirer from 'inquirer'; import * as path from 'path'; import { CLIConfig } from '../utils/config.js'; import { apiClient } from '../utils/api.js'; export function configCommands(program) { // Generic config set command program .command('set <key> <value>') .description('Set configuration value') .action(async (key, value) => { const config = new CLIConfig(); await config.init(); let shouldSave = true; // Handle special cases switch (key) { case 'api-url': await config.setApiUrl(value); console.log(chalk.green('✓ API URL updated:'), value); shouldSave = false; // setApiUrl already persists break; case 'ai-integration': if (value === 'claude-mcp') { config.set('mcpEnabled', true); config.set('aiIntegration', 'claude-mcp'); console.log(chalk.green('✓ AI integration set to Claude MCP')); console.log(chalk.cyan(' MCP will be automatically initialized for memory operations')); console.log(chalk.cyan(' Run "lanonasis mcp-server init" to test the connection')); } else { console.log(chalk.yellow('⚠️ Unknown AI integration:'), value); console.log(chalk.gray(' Currently supported: claude-mcp')); } break; case 'mcp-use-remote': config.set('mcpUseRemote', value === 'true'); console.log(chalk.green('✓ MCP remote mode:'), value === 'true' ? 'enabled' : 'disabled'); break; case 'mcp-server-path': config.set('mcpServerPath', value); console.log(chalk.green('✓ MCP server path updated:'), value); break; case 'mcp-server-url': config.set('mcpServerUrl', value); console.log(chalk.green('✓ MCP server URL updated:'), value); break; case 'force-api': config.set('forceApi', value === 'true'); config.set('connectionTransport', value === 'true' ? 'api' : 'auto'); console.log(chalk.green('✓ Force direct API mode:'), value === 'true' ? 'enabled' : 'disabled'); break; default: // Generic config set config.set(key, value); console.log(chalk.green(`✓ ${key} set to:`), value); } if (shouldSave) { await config.save(); } }); // Generic config get command program .command('get <key>') .description('Get configuration value') .action(async (key) => { const config = new CLIConfig(); await config.init(); const value = config.get(key); if (value !== undefined) { console.log(chalk.green(`${key}:`), value); } else { console.log(chalk.yellow(`⚠️ ${key} is not set`)); } }); // Show current configuration program .command('show') .description('Show current configuration') .action(async () => { const config = new CLIConfig(); await config.init(); console.log(chalk.blue.bold('⚙️ Current Configuration')); console.log(); console.log(chalk.green('API URL:'), config.getApiUrl()); console.log(chalk.green('Config Path:'), config.getConfigPath()); const isAuth = await config.isAuthenticated(); console.log(chalk.green('Authenticated:'), isAuth ? chalk.green('Yes') : chalk.red('No')); if (isAuth) { const user = await config.getCurrentUser(); if (user) { console.log(chalk.green('User:'), user.email); console.log(chalk.green('Organization:'), user.organization_id); console.log(chalk.green('Role:'), user.role); console.log(chalk.green('Plan:'), user.plan); } } }); // List all configuration options program .command('list') .description('List all configuration options') .action(async () => { const config = new CLIConfig(); await config.init(); console.log(chalk.blue.bold('📋 Configuration Options')); console.log(); const configOptions = [ { key: 'api-url', description: 'API endpoint URL', current: config.getApiUrl() }, { key: 'ai-integration', description: 'AI integration mode', current: config.get('aiIntegration') || 'none' }, { key: 'mcp-use-remote', description: 'Use remote MCP server', current: config.get('mcpUseRemote') || false }, { key: 'mcp-server-path', description: 'Local MCP server path', current: config.get('mcpServerPath') || 'default' }, { key: 'mcp-server-url', description: 'Remote MCP server URL', current: config.get('mcpServerUrl') || 'https://mcp.lanonasis.com' }, { key: 'force-api', description: 'Force direct API transport', current: config.get('forceApi') || false }, { key: 'mcpEnabled', description: 'MCP integration enabled', current: config.get('mcpEnabled') || false } ]; configOptions.forEach(opt => { console.log(chalk.green(opt.key.padEnd(20)), chalk.gray(opt.description.padEnd(30)), chalk.yellow(String(opt.current))); }); console.log(); console.log(chalk.gray('Use "lanonasis config set <key> <value>" to update any option')); }); // Set API URL program .command('set-url') .description('Set API URL') .argument('[url]', 'API URL') .action(async (url) => { const config = new CLIConfig(); await config.init(); if (!url) { const answer = await inquirer.prompt([ { type: 'input', name: 'url', message: 'API URL:', default: config.getApiUrl(), validate: (input) => { try { new URL(input); return true; } catch { return 'Please enter a valid URL'; } } } ]); url = answer.url; } await config.setApiUrl(url); console.log(chalk.green('✓ API URL updated:'), url); }); // Test connection program .command('test') .description('Test connection to API') .action(async () => { const config = new CLIConfig(); await config.init(); console.log(chalk.blue('🔌 Testing connection...')); console.log(chalk.gray(`API URL: ${config.getApiUrl()}`)); console.log(); try { const health = await apiClient.getHealth(); console.log(chalk.green('✓ Connection successful')); console.log(`Status: ${health.status}`); console.log(`Version: ${health.version}`); if (health.dependencies) { console.log(); console.log(chalk.yellow('Dependencies:')); Object.entries(health.dependencies).forEach(([name, info]) => { const status = info.status === 'healthy' ? chalk.green('✓') : chalk.red('✖'); const responseTime = info.response_time || info.latency_ms || 0; console.log(` ${status} ${name}: ${info.status} (${responseTime}ms)`); }); } } catch (error) { console.log(chalk.red('✖ Connection failed')); const errorCode = error && typeof error === 'object' && 'code' in error ? error.code : null; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (errorCode === 'ECONNREFUSED') { console.error(chalk.red('Cannot connect to API server')); console.log(chalk.yellow('Make sure the API server is running')); } else { console.error(chalk.red('Error:'), errorMessage); } process.exit(1); } }); // Service discovery and endpoint management program .command('discover') .description('Discover service endpoints') .option('-v, --verbose', 'show detailed discovery information') .action(async (options) => { const config = new CLIConfig(); await config.init(); console.log(chalk.blue('🔍 Discovering service endpoints...')); console.log(); try { await config.discoverServices(options.verbose); const services = config.get('discoveredServices'); if (services) { console.log(chalk.green('✓ Service discovery completed')); console.log(); console.log(chalk.yellow('Discovered endpoints:')); console.log(` Auth: ${services.auth_base}`); console.log(` Memory: ${services.memory_base}`); console.log(` MCP HTTP: ${services.mcp_base}`); console.log(` MCP WebSocket: ${services.mcp_ws_base}`); console.log(` MCP SSE: ${services.mcp_sse_base}`); console.log(` Project: ${services.project_scope}`); } } catch { console.log(chalk.red('✖ Service discovery failed')); console.log(chalk.gray('Using fallback endpoints')); } }); program .command('endpoints') .description('Show current service endpoints') .action(async () => { const config = new CLIConfig(); await config.init(); const services = config.get('discoveredServices'); const hasManualOverrides = config.hasManualEndpointOverrides(); const lastDiscovery = config.get('lastServiceDiscovery'); const lastManualUpdate = config.get('lastManualEndpointUpdate'); console.log(chalk.blue.bold('🌐 Service Endpoints')); console.log(); if (services) { console.log(chalk.yellow('Current endpoints:')); console.log(` Auth Base: ${chalk.white(services.auth_base)}`); console.log(` Memory Base: ${chalk.white(services.memory_base)}`); console.log(` MCP HTTP: ${chalk.white(services.mcp_base)}`); console.log(` MCP WebSocket: ${chalk.white(services.mcp_ws_base)}`); console.log(` MCP SSE: ${chalk.white(services.mcp_sse_base)}`); console.log(` Project Scope: ${chalk.white(services.project_scope)}`); console.log(); if (hasManualOverrides) { console.log(chalk.cyan('📝 Manual overrides are active')); if (lastManualUpdate) { const updateDate = new Date(lastManualUpdate); console.log(` Last updated: ${updateDate.toLocaleString()}`); } } else if (lastDiscovery) { const discoveryDate = new Date(lastDiscovery); console.log(chalk.green('🔍 Endpoints from service discovery')); console.log(` Last discovered: ${discoveryDate.toLocaleString()}`); } else { console.log(chalk.gray('📋 Using fallback endpoints')); } } else { console.log(chalk.yellow('⚠️ No endpoints configured')); console.log(chalk.gray('Run: lanonasis config discover')); } }); program .command('set-endpoint <type> <url>') .description('Set manual endpoint override (auth|memory|mcp-http|mcp-ws|mcp-sse)') .action(async (type, url) => { const config = new CLIConfig(); await config.init(); // Validate URL format try { new URL(url); } catch { console.log(chalk.red('✖ Invalid URL format')); process.exit(1); } // Map type to config key const endpointMap = { 'auth': 'auth_base', 'memory': 'memory_base', 'mcp-http': 'mcp_base', 'mcp-ws': 'mcp_ws_base', 'mcp-sse': 'mcp_sse_base' }; const configKey = endpointMap[type]; if (!configKey) { console.log(chalk.red('✖ Invalid endpoint type')); console.log(chalk.gray('Valid types: auth, memory, mcp-http, mcp-ws, mcp-sse')); process.exit(1); } // Set the manual override const overrides = { [configKey]: url }; await config.setManualEndpoints(overrides); console.log(chalk.green(`✓ ${type} endpoint set to:`), url); console.log(chalk.cyan('💡 Use "lanonasis config clear-overrides" to remove manual overrides')); }); program .command('clear-overrides') .description('Clear manual endpoint overrides and rediscover services') .action(async () => { const config = new CLIConfig(); await config.init(); if (!config.hasManualEndpointOverrides()) { console.log(chalk.yellow('⚠️ No manual overrides to clear')); return; } await config.clearManualEndpointOverrides(); console.log(chalk.green('✓ Manual endpoint overrides cleared')); console.log(chalk.cyan('✓ Service endpoints rediscovered')); }); // Validate configuration program .command('validate') .description('Validate configuration and check for issues') .option('-v, --verbose', 'show detailed validation information') .option('--repair', 'automatically repair common issues') .action(async (options) => { const config = new CLIConfig(); await config.init(); console.log(chalk.blue.bold('🔍 Configuration Validation')); console.log(chalk.cyan('━'.repeat(50))); console.log(); const validation = { configExists: false, configReadable: false, configVersion: null, configFormat: false, authenticationValid: false, endpointsValid: false, mcpConfigValid: false, backupExists: false, issues: [], repairs: [] }; // Step 1: Check config file existence and readability console.log(chalk.cyan('1. Configuration File')); try { validation.configExists = await config.exists(); if (validation.configExists) { console.log(chalk.green(' ✓ Config file exists at'), config.getConfigPath()); // Try to read the config await config.load(); validation.configReadable = true; console.log(chalk.green(' ✓ Config file is readable')); // Check config version validation.configVersion = config.get('version'); if (validation.configVersion) { console.log(chalk.green(' ✓ Config version:'), validation.configVersion); } else { console.log(chalk.yellow(' ⚠ Config version missing (legacy config)')); validation.issues.push('Config version missing'); if (options.repair) { await config.save(); // This will add the version validation.repairs.push('Added config version'); console.log(chalk.cyan(' → Repaired: Added config version')); } } validation.configFormat = true; } else { console.log(chalk.red(' ✖ Config file not found')); validation.issues.push('Config file does not exist'); if (options.repair) { await config.save(); // Create empty config validation.repairs.push('Created config file'); console.log(chalk.cyan(' → Repaired: Created config file')); } } } catch (error) { console.log(chalk.red(' ✖ Config file is corrupted or unreadable')); console.log(chalk.gray(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); validation.issues.push('Config file is corrupted'); if (options.repair) { try { // Try to backup the corrupted config const backupPath = await config.backupConfig(); console.log(chalk.cyan(` → Backed up corrupted config to: ${backupPath}`)); // Create new config await config.clear(); await config.save(); validation.repairs.push('Recreated corrupted config file'); console.log(chalk.cyan(' → Repaired: Recreated config file')); } catch { console.log(chalk.red(' ✖ Could not repair corrupted config')); } } } // Step 2: Validate authentication configuration console.log(chalk.cyan('\n2. Authentication Configuration')); const token = config.getToken(); const vendorKey = await config.getVendorKeyAsync(); const authMethod = config.get('authMethod'); if (!token && !vendorKey) { console.log(chalk.yellow(' ⚠ No authentication credentials configured')); validation.issues.push('No authentication credentials'); } else { console.log(chalk.green(' ✓ Authentication credentials found')); // Validate auth method consistency // Note: OAuth users can have vendorKey stored (for MCP access) while authMethod is 'oauth' if (vendorKey && authMethod !== 'vendor_key' && authMethod !== 'oauth') { console.log(chalk.yellow(' ⚠ Auth method mismatch (has vendor key but method is not vendor_key or oauth)')); validation.issues.push('Auth method mismatch'); if (options.repair) { config.set('authMethod', 'vendor_key'); await config.save(); validation.repairs.push('Fixed auth method for vendor key'); console.log(chalk.cyan(' → Repaired: Set auth method to vendor_key')); } } else if (token && !vendorKey && authMethod !== 'jwt' && authMethod !== 'oauth') { console.log(chalk.yellow(' ⚠ Auth method mismatch (has token but method is not jwt/oauth)')); validation.issues.push('Auth method mismatch'); if (options.repair) { config.set('authMethod', 'jwt'); await config.save(); validation.repairs.push('Fixed auth method for token'); console.log(chalk.cyan(' → Repaired: Set auth method to jwt')); } } // Validate vendor key presence if present if (vendorKey) { const formatValidation = config.validateVendorKeyFormat(vendorKey); if (formatValidation === true) { console.log(chalk.green(' ✓ Vendor key is set')); } else { console.log(chalk.red(` ✖ Vendor key issue: ${formatValidation}`)); validation.issues.push('Vendor key missing or invalid'); } } // Test authentication validity try { const isValid = await config.validateStoredCredentials(); validation.authenticationValid = isValid; if (isValid) { console.log(chalk.green(' ✓ Authentication credentials are valid')); } else { console.log(chalk.red(' ✖ Authentication credentials are invalid')); validation.issues.push('Invalid authentication credentials'); } } catch (error) { console.log(chalk.yellow(' ⚠ Could not validate authentication credentials')); if (options.verbose) { console.log(chalk.gray(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } } // Step 3: Validate service endpoints console.log(chalk.cyan('\n3. Service Endpoints')); try { await config.discoverServices(options.verbose); const services = config.get('discoveredServices'); if (services) { validation.endpointsValid = true; console.log(chalk.green(' ✓ Service endpoints are configured')); // Validate endpoint URLs const endpoints = [ { name: 'Auth', url: services.auth_base }, { name: 'Memory', url: services.memory_base }, { name: 'MCP HTTP', url: services.mcp_base }, { name: 'MCP WebSocket', url: services.mcp_ws_base }, { name: 'MCP SSE', url: services.mcp_sse_base } ]; for (const endpoint of endpoints) { if (endpoint.url) { try { new URL(endpoint.url); if (options.verbose) { console.log(chalk.green(` ✓ ${endpoint.name}: ${endpoint.url}`)); } } catch { console.log(chalk.red(` ✖ Invalid ${endpoint.name} URL: ${endpoint.url}`)); validation.issues.push(`Invalid ${endpoint.name} URL`); } } } // Check for manual overrides if (config.hasManualEndpointOverrides()) { console.log(chalk.cyan(' ℹ Manual endpoint overrides are active')); const lastUpdate = config.get('lastManualEndpointUpdate'); if (lastUpdate && options.verbose) { const updateDate = new Date(lastUpdate); console.log(chalk.gray(` Last updated: ${updateDate.toLocaleString()}`)); } } } else { console.log(chalk.red(' ✖ Service endpoints are not configured')); validation.issues.push('Service endpoints not configured'); } } catch { console.log(chalk.red(' ✖ Service endpoint discovery failed')); validation.issues.push('Service endpoint discovery failed'); if (options.repair) { // Set fallback endpoints await config.setManualEndpoints({ auth_base: 'https://api.lanonasis.com', memory_base: 'https://api.lanonasis.com/api/v1', mcp_base: 'https://mcp.lanonasis.com/api/v1', mcp_ws_base: 'wss://mcp.lanonasis.com/ws', mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events' }); validation.repairs.push('Set fallback service endpoints'); console.log(chalk.cyan(' → Repaired: Set fallback service endpoints')); } } // Step 4: Validate MCP configuration console.log(chalk.cyan('\n4. MCP Configuration')); const mcpPreference = config.get('mcpPreference'); // const mcpServerPath = config.get<string>('mcpServerPath'); const mcpServerUrl = config.get('mcpServerUrl'); if (mcpPreference) { if (['local', 'remote', 'websocket', 'auto'].includes(mcpPreference)) { console.log(chalk.green(` ✓ MCP preference: ${mcpPreference}`)); validation.mcpConfigValid = true; } else { console.log(chalk.red(` ✖ Invalid MCP preference: ${mcpPreference}`)); validation.issues.push('Invalid MCP preference'); if (options.repair) { config.set('mcpPreference', 'auto'); await config.save(); validation.repairs.push('Reset MCP preference to auto'); console.log(chalk.cyan(' → Repaired: Reset MCP preference to auto')); } } } else { console.log(chalk.yellow(' ⚠ MCP preference not set (using default: auto)')); if (options.repair) { config.set('mcpPreference', 'auto'); await config.save(); validation.repairs.push('Set default MCP preference'); console.log(chalk.cyan(' → Repaired: Set MCP preference to auto')); } } // Validate MCP URLs if present if (mcpServerUrl) { try { new URL(mcpServerUrl); console.log(chalk.green(' ✓ MCP server URL is valid')); } catch { console.log(chalk.red(` ✖ Invalid MCP server URL: ${mcpServerUrl}`)); validation.issues.push('Invalid MCP server URL'); } } // Step 5: Check for configuration backup console.log(chalk.cyan('\n5. Configuration Backup')); try { const configDir = path.dirname(config.getConfigPath()); const fs = await import('fs/promises'); const files = await fs.readdir(configDir); const backupFiles = files.filter(f => f.startsWith('config.backup.')); if (backupFiles.length > 0) { validation.backupExists = true; console.log(chalk.green(` ✓ Found ${backupFiles.length} configuration backup(s)`)); if (options.verbose) { const latestBackup = backupFiles.sort().reverse()[0]; console.log(chalk.gray(` Latest backup: ${latestBackup}`)); } } else { console.log(chalk.yellow(' ⚠ No configuration backups found')); if (options.repair) { const backupPath = await config.backupConfig(); validation.repairs.push('Created configuration backup'); console.log(chalk.cyan(` → Repaired: Created backup at ${path.basename(backupPath)}`)); } } } catch (error) { console.log(chalk.yellow(' ⚠ Could not check for backups')); if (options.verbose) { console.log(chalk.gray(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } // Summary and recommendations console.log(chalk.blue.bold('\n📋 Configuration Validation Summary')); console.log(chalk.cyan('━'.repeat(50))); if (validation.issues.length === 0) { console.log(chalk.green('✅ Configuration validation passed!')); console.log(chalk.cyan(' Your configuration is valid and healthy.')); } else { console.log(chalk.red(`❌ Found ${validation.issues.length} issue(s):`)); validation.issues.forEach(issue => { console.log(chalk.red(` • ${issue}`)); }); } if (validation.repairs.length > 0) { console.log(chalk.yellow(`\n🔧 Applied ${validation.repairs.length} repair(s):`)); validation.repairs.forEach(repair => { console.log(chalk.cyan(` • ${repair}`)); }); } // Recommendations const recommendations = []; if (!validation.configExists && !options.repair) { recommendations.push('Run: lanonasis config validate --repair'); } if (!validation.authenticationValid) { recommendations.push('Run: lanonasis auth login'); } if (!validation.endpointsValid && !options.repair) { recommendations.push('Run: lanonasis config discover'); } if (!validation.backupExists && !options.repair) { recommendations.push('Run: lanonasis config validate --repair (to create backup)'); } if (recommendations.length > 0) { console.log(chalk.yellow('\n💡 Recommended actions:')); recommendations.forEach(rec => { console.log(chalk.cyan(` • ${rec}`)); }); } }); // Backup configuration program .command('backup') .description('Create a backup of current configuration') .action(async () => { const config = new CLIConfig(); await config.init(); try { const backupPath = await config.backupConfig(); console.log(chalk.green('✓ Configuration backed up to:')); console.log(chalk.cyan(` ${backupPath}`)); } catch (error) { console.log(chalk.red('✖ Failed to create backup:')); console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } }); // Restore configuration from backup program .command('restore') .description('Restore configuration from backup') .argument('[backup-file]', 'specific backup file to restore from') .action(async (backupFile) => { const config = new CLIConfig(); await config.init(); try { const configDir = path.dirname(config.getConfigPath()); const fs = await import('fs/promises'); let backupPath; if (backupFile) { // Use specified backup file backupPath = path.isAbsolute(backupFile) ? backupFile : path.join(configDir, backupFile); } else { // Find latest backup const files = await fs.readdir(configDir); const backupFiles = files.filter(f => f.startsWith('config.backup.')).sort().reverse(); if (backupFiles.length === 0) { console.log(chalk.red('✖ No backup files found')); console.log(chalk.gray(' Run: lanonasis config backup')); process.exit(1); } backupPath = path.join(configDir, backupFiles[0]); console.log(chalk.cyan(`Using latest backup: ${backupFiles[0]}`)); } // Verify backup file exists await fs.access(backupPath); // Confirm restoration const answer = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: `Restore configuration from backup? This will overwrite current config.`, default: false } ]); if (!answer.confirm) { console.log(chalk.yellow('Restore cancelled')); return; } // Create backup of current config before restoring const currentBackupPath = await config.backupConfig(); console.log(chalk.cyan(`Current config backed up to: ${path.basename(currentBackupPath)}`)); // Restore from backup await fs.copyFile(backupPath, config.getConfigPath()); console.log(chalk.green('✓ Configuration restored from backup')); console.log(chalk.cyan(' You may need to re-authenticate if credentials were changed')); } catch (error) { console.log(chalk.red('✖ Failed to restore from backup:')); console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } }); // Reset configuration program .command('reset') .description('Reset all configuration') .option('-f, --force', 'skip confirmation') .action(async (options) => { if (!options.force) { const answer = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: 'Are you sure you want to reset all configuration? This will log you out.', default: false } ]); if (!answer.confirm) { console.log(chalk.yellow('Reset cancelled')); return; } } const config = new CLIConfig(); // Create backup before reset try { const backupPath = await config.backupConfig(); console.log(chalk.cyan(`Configuration backed up to: ${path.basename(backupPath)}`)); } catch { // Ignore backup errors during reset } await config.clear(); console.log(chalk.green('✓ Configuration reset')); console.log(chalk.yellow('Run'), chalk.white('lanonasis auth login'), chalk.yellow('to reconfigure')); }); }