UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

1,217 lines (1,192 loc) β€’ 115 kB
/** * CLI Command Handlers - Implementation of all CLI commands * @description Contains all the handler methods for the OptlyCLI commands */ import chalk from 'chalk'; import * as repl from 'repl'; import * as readline from 'readline'; import { promises as fs } from 'fs'; import path from 'path'; import { parse as parseCSV } from 'csv-parse/sync'; import { stringify as stringifyCSV } from 'csv-stringify/sync'; import * as yaml from 'js-yaml'; import ora from 'ora'; import { format } from 'date-fns'; import { Table } from 'console-table-printer'; import { OptlyCLI } from './OptlyCLI.js'; import { getLogger } from '../logging/Logger.js'; // Implementation of all handler methods const handlers = { /** * Handle query command */ async handleQuery(entity, options) { // Merge with global options from context const mergedOptions = { ...this.context?.options, ...options }; let results; if (options.sql) { // Raw SQL query results = await this.context.storage.query(options.sql); } else if (entity) { // Build SQL query for entity // The table name is already plural in most cases (flags, not flagss) let sql = `SELECT * FROM ${entity}`; const conditions = []; // Add project filter if specified if (options.project) { conditions.push(`project_id = '${options.project}'`); } // Add custom filter conditions if (options.filter) { conditions.push(options.filter); } // Add WHERE clause if conditions exist if (conditions.length > 0) { sql += ` WHERE ${conditions.join(' AND ')}`; } // Add ORDER BY if (options.sort) { sql += ` ORDER BY ${options.sort}`; if (options.desc) { sql += ' DESC'; } } // Add LIMIT and OFFSET sql += ` LIMIT ${options.limit || 100}`; if (options.offset) { sql += ` OFFSET ${options.offset}`; } // Execute query results = await this.context.storage.query(sql); } else { throw new Error('Either entity type or --sql must be specified'); } await this.outputResults(results, mergedOptions); }, /** * Handle search command * COMMENTED OUT: search_all tool is redundant with list_entities functionality */ // async handleSearch(this: OptlyCLI, keyword: string, options: any): Promise<void> { // const spinner = ora('Searching...').start(); // try { // const searchResults = await this.context!.tools.searchAll({ // query: keyword, // project_id: options.project, // limit: parseInt(options.limit) // }); // spinner.succeed(`Found ${searchResults.results.length} results`); // await this.outputResults(searchResults.results, options); // } catch (error: any) { // spinner.fail('Search failed'); // throw error; // } // }, /** * Handle entity list command */ async handleEntityList(type, options) { const filters = { include_archived: options.archived, }; // Handle pagination options if (options.page) { filters.page = parseInt(options.page); } // Handle page size / limit // Note: pageSize takes precedence over limit if both are provided let effectivePageSize = undefined; if (options.pageSize !== undefined) { effectivePageSize = parseInt(options.pageSize); } else if (options.limit !== undefined) { effectivePageSize = parseInt(options.limit); } if (effectivePageSize === -1) { // Special case: -1 means get all results (bypass pagination) filters.bypass_pagination = true; filters.user_consent_required = true; } else if (effectivePageSize !== undefined) { filters.page_size = effectivePageSize; } const results = await this.context.tools.listEntities(type, options.project, filters); // Handle different result formats (paginated vs simple array) if (results && results.entities) { // Paginated result const count = results.entities.length; const total = results.pagination?.total_count || count; console.log(chalk.cyan(`Found ${count} ${type}(s) (${total} total)`)); // Show pagination info if available if (results.pagination && results.pagination.has_more) { console.log(chalk.gray(`Page ${results.pagination.current_page} of ${results.pagination.total_pages}`)); console.log(chalk.gray(`Use --page ${results.pagination.next_page} to see more results`)); } } else if (Array.isArray(results)) { // Simple array result console.log(chalk.cyan(`Found ${results.length} ${type}(s)`)); } else { // Unknown format console.log(chalk.cyan(`Listed ${type}s`)); } await this.outputResults(results, options); }, /** * Handle entity get command */ async handleEntityGet(type, id, options) { const result = await this.context.tools.getEntityDetails(type, id, options.project); await this.outputResults(result, options); }, /** * Handle entity create command */ async handleEntityCreate(type, options) { let data; if (options.file) { // Read from file const content = await fs.readFile(options.file, 'utf-8'); data = this.parseFileContent(content, options.file); } else if (options.template) { // Use template const templates = await this.context.tools.getEntityTemplates(options.project, type); // Templates are now returned as a single object with template property const template = templates.template_usage?.example_call ? templates : null; if (!template) { throw new Error(`Template '${options.template}' not found`); } // Extract data from template structure data = {}; // Templates need to be properly parsed based on new structure } else { throw new Error('Either --file or --template must be specified'); } if (options.dryRun) { console.log(chalk.yellow('πŸ” DRY RUN - Would create:')); await this.outputResults(data, options); return; } const spinner = ora(`Creating ${type}...`).start(); try { const result = await this.context.tools.manageEntityLifecycle('create', type, data, undefined, // entityId options.project); spinner.succeed(`Created ${type} successfully`); await this.outputResults(result, options); } catch (error) { spinner.fail(`Failed to create ${type}`); throw error; } }, /** * Handle entity update command */ async handleEntityUpdate(type, id, options) { let updates = {}; if (options.file) { // Read updates from file const content = await fs.readFile(options.file, 'utf-8'); updates = this.parseFileContent(content, options.file); } else if (options.set) { // Parse field=value pairs for (const pair of options.set) { const [field, value] = pair.split('='); updates[field] = this.parseValue(value); } } else { throw new Error('Either --file or --set must be specified'); } if (options.dryRun) { console.log(chalk.yellow('πŸ” DRY RUN - Would update:')); await this.outputResults({ id, updates }, options); return; } const spinner = ora(`Updating ${type}...`).start(); try { const result = await this.context.tools.manageEntityLifecycle('update', type, updates, id, options.project); spinner.succeed(`Updated ${type} successfully`); await this.outputResults(result, options); } catch (error) { spinner.fail(`Failed to update ${type}`); throw error; } }, /** * Handle entity delete command */ async handleEntityDelete(type, id, options) { if (!options.force) { const confirmed = await this.confirm(`Delete ${type} ${id}?`); if (!confirmed) { console.log('Cancelled'); return; } } const spinner = ora(`Deleting ${type}...`).start(); try { const result = await this.context.tools.manageEntityLifecycle('delete', type, undefined, // entityData id, options.project); spinner.succeed(`Deleted ${type} successfully`); await this.outputResults(result, options); } catch (error) { spinner.fail(`Failed to delete ${type}`); throw error; } }, /** * Handle entity templates command */ async handleEntityTemplates(type, options) { const templates = await this.context.tools.getEntityTemplates(options.project, type, true, // useModelFriendly options.complexity); console.log(chalk.cyan(`Available ${type} templates:`)); // Templates are now returned as a single object, not an array const templateList = templates.template_usage?.example_call ? [templates] : []; for (const template of templateList) { console.log(chalk.bold(`\n${type} Template`)); console.log(chalk.gray(`Complexity: ${template.template_mode?.level || 'N/A'}`)); if (template.template_mode?.headline) { console.log(chalk.gray(`Description: ${template.template_mode.headline}`)); } if (this.context?.options.verbose && template.template_usage?.example_call) { console.log(chalk.gray('\nExample usage:')); console.log(template.template_usage.example_call); } } }, /** * Handle export command */ async handleExport(entity, options) { const spinner = ora('Exporting data...').start(); try { let data; if (entity === 'all') { // Export all data data = {}; const types = await this.context.tools.getSupportedEntityTypes(); for (const type of types) { spinner.text = `Exporting ${type}...`; data[type] = await this.context.tools.listEntities(type, options.project); } } else { // Export specific entity type const params = { projectId: options.project }; if (options.filter) { params.filters = this.parseFilterConditions(options.filter); } data = await this.context.tools.listEntities(entity, params); } // Apply transformation if specified if (options.transform) { const transformFn = await this.loadTransformScript(options.transform); data = await transformFn(data); } spinner.succeed('Export complete'); // Format and output const format = options.format || 'json'; const formatted = await this.formatData(data, format); if (options.output) { await fs.writeFile(options.output, formatted); console.log(chalk.green(`βœ“ Exported to ${options.output}`)); } else { console.log(formatted); } } catch (error) { spinner.fail('Export failed'); throw error; } }, /** * Handle import command */ async handleImport(file, options) { const spinner = ora('Reading import file...').start(); try { // Read and parse file const content = await fs.readFile(file, 'utf-8'); const data = this.parseFileContent(content, file); // Auto-detect entity type if not specified let entityType = options.type; if (!entityType) { entityType = this.detectEntityType(data); spinner.text = `Detected entity type: ${entityType}`; } // Apply field mappings if specified if (options.map) { spinner.text = 'Applying field mappings...'; // TODO: Implement field mapping } if (options.dryRun) { spinner.succeed('Dry run complete'); console.log(chalk.yellow('πŸ” DRY RUN - Would import:')); console.log(`Entity Type: ${entityType}`); console.log(`Records: ${Array.isArray(data) ? data.length : 1}`); return; } // Import data spinner.text = 'Importing data...'; const results = []; const items = Array.isArray(data) ? data : [data]; for (const item of items) { const result = await this.context.tools.manageEntityLifecycle(options.merge ? 'update' : 'create', entityType, { projectId: options.project || item.project_id, entityData: item }); results.push(result); } spinner.succeed(`Imported ${results.length} ${entityType}(s)`); } catch (error) { spinner.fail('Import failed'); throw error; } }, /** * Handle backup command */ async handleBackup(options) { const spinner = ora('Creating backup...').start(); try { const timestamp = format(new Date(), 'yyyy-MM-dd-HHmmss'); const filename = options.output || `backup-${timestamp}.db`; await this.context.storage.backup(filename); if (options.compress) { spinner.text = 'Compressing backup...'; // TODO: Implement compression } spinner.succeed(`Backup created: ${filename}`); } catch (error) { spinner.fail('Backup failed'); throw error; } }, /** * Handle restore command */ async handleRestore(file, options) { if (!options.force) { console.log(chalk.yellow('⚠️ WARNING: This will replace all current data!')); const confirmed = await this.confirm('Continue with restore?'); if (!confirmed) { console.log('Cancelled'); return; } } const spinner = ora('Restoring database...').start(); try { // Create backup of current data first spinner.text = 'Creating safety backup...'; const timestamp = format(new Date(), 'yyyy-MM-dd-HHmmss'); await this.context.storage.backup(`pre-restore-${timestamp}.db`); // Restore from file spinner.text = 'Restoring from backup...'; // TODO: Implement restore logic spinner.succeed('Database restored successfully'); } catch (error) { spinner.fail('Restore failed'); throw error; } }, /** * Handle database optimization */ async handleOptimize(options) { const spinner = ora('Optimizing database...').start(); try { const operations = []; if (options.vacuum) { operations.push('VACUUM'); } if (options.analyze) { operations.push('ANALYZE'); } if (options.reindex) { operations.push('REINDEX'); } if (operations.length === 0) { operations.push('VACUUM', 'ANALYZE'); // Default } for (const op of operations) { spinner.text = `Running ${op}...`; await this.context.storage.run(op); } spinner.succeed('Database optimized successfully'); } catch (error) { spinner.fail('Optimization failed'); throw error; } }, /** * Handle database stats command */ async handleDatabaseStats(options) { const stats = { general: {}, tables: [] }; // Get general stats const dbInfo = await this.context.storage.get('PRAGMA page_count'); const pageSize = await this.context.storage.get('PRAGMA page_size'); stats.general.size = (dbInfo.page_count * pageSize.page_size); stats.general.sizeFormatted = this.formatBytes(stats.general.size); if (options.tables || options.detailed) { // Get table stats const tables = await this.context.storage.query(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`); for (const table of tables) { const count = await this.context.storage.get(`SELECT COUNT(*) as count FROM ${table.name}`); const info = await this.context.storage.get(`SELECT SUM(pgsize) as size FROM dbstat WHERE name='${table.name}'`); stats.tables.push({ name: table.name, rows: count.count, size: info?.size || 0, sizeFormatted: this.formatBytes(info?.size || 0) }); } } await this.outputResults(stats, options); }, /** * Handle database status command */ async handleDatabaseStatus(options) { const spinner = ora('Checking database status...').start(); try { const status = { connection: 'Unknown', health: 'Unknown', details: {} }; // Check if storage context exists if (!this.context?.storage) { status.connection = 'Disconnected'; status.health = 'Error'; status.details.error = 'Storage context not initialized'; spinner.fail('Database not connected'); await this.outputResults(status, options); return; } // Test basic connection by running a simple query try { spinner.text = 'Testing database connection...'; const testQuery = await this.context.storage.get('SELECT 1 as test'); if (testQuery && testQuery.test === 1) { status.connection = 'Connected'; } else { status.connection = 'Connected (with issues)'; status.health = 'Warning'; } } catch (connError) { status.connection = 'Failed'; status.health = 'Error'; status.details.connectionError = connError.message; spinner.fail('Database connection failed'); await this.outputResults(status, options); return; } // Get database path from storage try { spinner.text = 'Getting database information...'; // Get database file info const dbPath = this.context.storage.dbPath || 'Unknown'; status.details.path = dbPath; // Check if file exists if (dbPath !== 'Unknown') { try { const { existsSync, statSync } = await import('fs'); if (existsSync(dbPath)) { const stats = statSync(dbPath); status.details.exists = true; status.details.size = this.formatBytes(stats.size); status.details.modified = stats.mtime.toISOString(); } else { status.details.exists = false; status.health = 'Warning'; } } catch (fsError) { status.details.fsError = fsError.message; } } } catch (error) { status.details.pathError = error.message; } // Run integrity check if detailed if (options.detailed) { try { spinner.text = 'Running integrity check...'; const integrityCheck = await this.context.storage.query('PRAGMA integrity_check'); if (integrityCheck && integrityCheck[0] && integrityCheck[0].integrity_check === 'ok') { status.details.integrity = 'OK'; } else { status.details.integrity = 'Failed'; status.health = 'Error'; status.details.integrityErrors = integrityCheck; } } catch (error) { status.details.integrityError = error.message; } // Get table count try { const tables = await this.context.storage.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`); status.details.tableCount = tables[0].count; } catch (error) { status.details.tableCountError = error.message; } // Check for locked tables try { const locked = await this.context.storage.query('PRAGMA database_list'); status.details.databases = locked; } catch (error) { status.details.lockError = error.message; } } // Set overall health status if (status.connection === 'Connected' && (!status.health || status.health === 'Unknown')) { status.health = 'Healthy'; } spinner.succeed('Database status check complete'); // Display results console.log('\n' + chalk.bold('Database Status:')); console.log(chalk.gray('─'.repeat(40))); // Connection status const connColor = status.connection === 'Connected' ? chalk.green : status.connection === 'Failed' ? chalk.red : chalk.yellow; console.log(`Connection: ${connColor(status.connection)}`); // Health status const healthColor = status.health === 'Healthy' ? chalk.green : status.health === 'Error' ? chalk.red : chalk.yellow; console.log(`Health: ${healthColor(status.health)}`); // Details if (status.details.path) { console.log(`Path: ${chalk.cyan(status.details.path)}`); } if (status.details.exists !== undefined) { console.log(`Exists: ${status.details.exists ? chalk.green('Yes') : chalk.red('No')}`); } if (status.details.size) { console.log(`Size: ${chalk.yellow(status.details.size)}`); } if (options.detailed) { console.log('\n' + chalk.bold('Detailed Information:')); console.log(chalk.gray('─'.repeat(40))); if (status.details.modified) { console.log(`Last Modified: ${chalk.gray(status.details.modified)}`); } if (status.details.integrity) { const integrityColor = status.details.integrity === 'OK' ? chalk.green : chalk.red; console.log(`Integrity: ${integrityColor(status.details.integrity)}`); } if (status.details.tableCount !== undefined) { console.log(`Tables: ${chalk.cyan(status.details.tableCount)}`); } if (status.details.databases) { console.log(`Databases: ${chalk.gray(JSON.stringify(status.details.databases))}`); } // Show any errors const errors = ['connectionError', 'pathError', 'integrityError', 'tableCountError', 'lockError']; const hasErrors = errors.some(e => status.details[e]); if (hasErrors) { console.log('\n' + chalk.red.bold('Errors:')); errors.forEach(errorKey => { if (status.details[errorKey]) { console.log(chalk.red(`- ${errorKey}: ${status.details[errorKey]}`)); } }); } } // Provide recommendations if issues detected if (status.health !== 'Healthy') { console.log('\n' + chalk.yellow.bold('Recommendations:')); if (status.connection === 'Failed' || status.connection === 'Disconnected') { console.log(chalk.yellow('- Check if the database file exists and is accessible')); console.log(chalk.yellow('- Try running "optly db reset" to recreate the database')); } if (status.details.integrity && status.details.integrity !== 'OK') { console.log(chalk.yellow('- Database integrity check failed')); console.log(chalk.yellow('- Consider restoring from a backup')); } } } catch (error) { spinner.fail('Status check failed'); console.error(chalk.red(`Error: ${error.message}`)); if (options.detailed) { console.error(error.stack); } } }, /** * Handle database reset */ async handleDatabaseReset(options) { if (!options.force) { console.log(chalk.red('⚠️ DANGER: This will DELETE ALL DATA!')); const confirmed = await this.confirm('Are you absolutely sure?'); if (!confirmed) { console.log('Cancelled'); return; } } const spinner = ora('Resetting database...').start(); try { if (options.backup) { spinner.text = 'Creating backup before reset...'; const timestamp = format(new Date(), 'yyyy-MM-dd-HHmmss'); await this.context.storage.backup(`pre-reset-${timestamp}.db`); } spinner.text = 'Resetting database...'; await this.context.cache.resetCache(); spinner.succeed('Database reset successfully'); } catch (error) { spinner.fail('Reset failed'); throw error; } }, /** * Handle watch start command */ async handleWatchStart(options) { const watchId = `watch-${Date.now()}`; const interval = parseInt(options.interval) * 1000; console.log(chalk.cyan(`Starting watcher ${watchId}...`)); console.log(chalk.gray(`Interval: ${options.interval}s`)); if (options.entities) { console.log(chalk.gray(`Entities: ${options.entities}`)); } const watchFn = async () => { try { console.log(chalk.gray(`[${new Date().toISOString()}] Checking for changes...`)); const result = await this.context.tools.refreshCache({ projectId: options.project, incremental: true }); if (result.changes && result.changes.length > 0) { console.log(chalk.yellow(`Found ${result.changes.length} changes`)); if (options.webhook) { // Send webhook notification await this.sendWebhook(options.webhook, { watchId, changes: result.changes, timestamp: new Date().toISOString() }); } } } catch (error) { console.error(chalk.red(`Watch error: ${error.message}`)); } }; // Run immediately await watchFn(); // Set interval const intervalId = setInterval(watchFn, interval); this.watchIntervals.set(watchId, intervalId); console.log(chalk.green(`βœ“ Watcher ${watchId} started`)); console.log(chalk.gray('Press Ctrl+C to stop')); }, /** * Handle watch stop command */ async handleWatchStop(options) { if (options.all) { for (const [id, interval] of this.watchIntervals) { clearInterval(interval); console.log(chalk.yellow(`Stopped watcher ${id}`)); } this.watchIntervals.clear(); } else if (options.id) { const interval = this.watchIntervals.get(options.id); if (interval) { clearInterval(interval); this.watchIntervals.delete(options.id); console.log(chalk.yellow(`Stopped watcher ${options.id}`)); } else { console.log(chalk.red(`Watcher ${options.id} not found`)); } } }, /** * Handle watch list command */ async handleWatchList() { if (this.watchIntervals.size === 0) { console.log(chalk.gray('No active watchers')); return; } console.log(chalk.cyan('Active watchers:')); for (const id of this.watchIntervals.keys()) { console.log(` ${id}`); } }, /** * Handle diff command */ async handleDiff(options) { console.log('Diff functionality not yet implemented'); // TODO: Implement diff logic }, /** * Handle compare environments command */ async handleCompareEnvironments(env1, env2, options) { const result = await this.context.tools.compareEnvironments({ project_id: options.project, environments: [env1, env2] // flag_key can be added if needed for specific flag comparison }); await this.outputResults(result, options); }, /** * Handle config show command */ async handleConfigShow(options) { const config = this.context.config.getConfig(); if (options.section) { const section = config[options.section]; await this.outputResults(section, options); } else { await this.outputResults(config, options); } }, /** * Handle config set command */ async handleConfigSet(key, value, options) { console.log(`Setting ${key} = ${value}`); // TODO: Implement config set }, /** * Handle defaults command */ async handleDefaults(action, entity, options) { switch (action) { case 'get': if (!entity) throw new Error('Entity type required for get'); const defaults = await this.context.tools.getDefaultConfiguration(entity, options.project); await this.outputResults(defaults, options); break; case 'set': if (!entity) throw new Error('Entity type required for set'); if (!options.data) throw new Error('--data required for set'); const data = JSON.parse(options.data); // Default configuration would need to be implemented console.log(chalk.yellow('Set defaults not yet implemented')); console.log(chalk.green('βœ“ Defaults updated')); break; case 'reset': // Reset defaults would need to be implemented console.log(chalk.yellow('Reset defaults not yet implemented')); console.log(chalk.green('βœ“ Defaults reset')); break; default: throw new Error(`Unknown action: ${action}`); } }, /** * Handle health check command */ async handleHealthCheck(options) { const spinner = ora('Checking system health...').start(); try { // Get API health const apiHealth = await this.context.tools.healthCheck(); // Check database health let dbHealth = { status: 'unknown', connection: 'Unknown' }; try { if (this.context?.storage) { // Add retry logic for binary initialization race condition let retries = 3; let lastError; while (retries > 0) { try { const testQuery = await this.context.storage.get('SELECT 1 as test'); if (testQuery && testQuery.test === 1) { dbHealth.status = 'healthy'; dbHealth.connection = 'Connected'; // Get basic database info const dbInfo = await this.context.storage.get('PRAGMA page_count'); const pageSize = await this.context.storage.get('PRAGMA page_size'); const dbSize = dbInfo.page_count * pageSize.page_size; dbHealth.size = this.formatBytes(dbSize); dbHealth.path = this.context.storage.dbPath; break; // Success, exit retry loop } } catch (error) { lastError = error; retries--; // If it's a binary loading issue and we have retries left, wait and retry if (retries > 0 && (error.message.includes('better_sqlite3.node') || error.message.includes('Cannot find module') || error.message.includes('was compiled against a different Node.js version'))) { // Wait 500ms for binary to be ready await new Promise(resolve => setTimeout(resolve, 500)); } else { // Other errors or no retries left throw error; } } } // If we exhausted retries, throw the last error if (retries === 0 && lastError) { throw lastError; } } else { dbHealth.status = 'error'; dbHealth.connection = 'Disconnected'; dbHealth.error = 'Storage context not initialized'; } } catch (dbError) { // Handle errors with more context if (dbError.message.includes('better_sqlite3.node') || dbError.message.includes('Cannot find module') || dbError.message.includes('was compiled against a different Node.js version')) { dbHealth.status = 'initializing'; dbHealth.connection = 'Binary not ready'; dbHealth.error = 'SQLite binary is still being prepared. Please try again in a moment.'; } else { dbHealth.status = 'error'; dbHealth.connection = 'Failed'; dbHealth.error = dbError.message; } } // Combine health results const overallHealth = { status: apiHealth.status === 'healthy' && dbHealth.status === 'healthy' ? 'healthy' : 'unhealthy', api: apiHealth, database: dbHealth, timestamp: new Date().toISOString() }; spinner.succeed('Health check complete'); if (options.detailed) { await this.outputResults(overallHealth, options); } else { console.log('\n' + chalk.bold('System Health Summary:')); console.log(chalk.gray('─'.repeat(40))); // Overall status const overallStatus = overallHealth.status === 'healthy' ? chalk.green('βœ“ All Systems Healthy') : chalk.red('βœ— Issues Detected'); console.log(`Overall: ${overallStatus}`); // API status const apiStatus = apiHealth.status === 'healthy' ? chalk.green('βœ“ Connected') : chalk.red('βœ— ' + (apiHealth.error || 'Disconnected')); console.log(`API: ${apiStatus}`); // Database status const dbStatus = dbHealth.connection === 'Connected' ? chalk.green('βœ“ Connected') : chalk.red('βœ— ' + dbHealth.connection); console.log(`Database: ${dbStatus}`); if (dbHealth.size) { console.log(`DB Size: ${chalk.yellow(dbHealth.size)}`); } // Show errors if any if (apiHealth.error || dbHealth.error) { console.log('\n' + chalk.red.bold('Errors:')); if (apiHealth.error) { console.log(chalk.red(`- API: ${apiHealth.error}`)); } if (dbHealth.error) { console.log(chalk.red(`- Database: ${dbHealth.error}`)); } } } } catch (error) { spinner.fail('Health check failed'); console.error(chalk.red(`Error: ${error.message}`)); } }, /* * handleSetupDocs is now implemented directly in OptlyCLI.ts */ /* handleSetupDocs: async function(this: OptlyCLI): Promise<void> { const fs = await import('fs/promises'); const path = await import('path'); try { console.log(chalk.blue('πŸ“š Setting up Optimizely MCP documentation...\n')); // Find package root by looking for package.json let packagePath = __dirname; while (packagePath !== path.dirname(packagePath)) { try { await fs.access(path.join(packagePath, 'package.json')); break; } catch { packagePath = path.dirname(packagePath); } } const targetDir = path.join(process.cwd(), 'optimizely-mcp-docs'); // Create target directory await fs.mkdir(targetDir, { recursive: true }); // Helper function to copy directory const copyDirectory = async (src: string, dest: string): Promise<void> => { await fs.mkdir(dest, { recursive: true }); const entries = await fs.readdir(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { await copyDirectory(srcPath, destPath); } else { await fs.copyFile(srcPath, destPath); } } }; // Files to copy const filesToCopy = [ { src: 'README.md', dest: 'README.md' }, { src: 'docs', dest: 'docs' }, { src: 'templates', dest: 'templates' } ]; let copiedCount = 0; for (const { src, dest } of filesToCopy) { const sourcePath = path.join(packagePath, src); const destPath = path.join(targetDir, dest); try { const stats = await fs.stat(sourcePath); if (stats.isDirectory()) { await copyDirectory(sourcePath, destPath); console.log(chalk.green(`βœ“ Copied ${dest}/`)); } else { await fs.copyFile(sourcePath, destPath); console.log(chalk.green(`βœ“ Copied ${dest}`)); } copiedCount++; } catch (error: any) { console.warn(chalk.yellow(`⚠ Could not copy ${src}: ${error.message}`)); } } // Create quick start guide const quickStartContent = `# Optimizely MCP Server - Quick Start Welcome to Optimizely MCP Server! This folder contains all the documentation you need to get started. ## πŸš€ For Claude Desktop Add to your Claude Desktop config (\`%APPDATA%\\\\Claude\\\\claude_desktop_config.json\`): \`\`\`json { "mcpServers": { "optimizely": { "command": "npx", "args": ["@simonecoelhosfo/optimizely-mcp-server"], "env": { "OPTIMIZELY_API_TOKEN": "your-api-token-here" } } } } \`\`\` ## πŸš€ For Cursor 1. Open Settings (Ctrl+,) 2. Search for "MCP" 3. Add the Optimizely server configuration ## πŸ”‘ Get Your API Token 1. Log in to [app.optimizely.com](https://app.optimizely.com) 2. Go to Account Settings β†’ Personal Settings β†’ API Access 3. Generate a new Personal Access Token ## πŸ“– Next Steps - Check the \`docs/\` folder for detailed guides - Run \`optly --help\` to see all commands - Run \`optly health\` to test your connection `; await fs.writeFile(path.join(targetDir, 'QUICK-START.md'), quickStartContent); console.log(chalk.green('βœ“ Created QUICK-START.md')); console.log(chalk.green(`\nβœ… Documentation setup complete!`)); console.log(chalk.blue(`πŸ“ Files copied to: ${targetDir}`)); console.log(chalk.yellow(`πŸ’‘ Start with: QUICK-START.md\n`)); } catch (error: any) { console.error(chalk.red(`❌ Failed to setup documentation: ${error.message}`)); } }, */ /** * Handle results command */ async handleResults(id, options) { const spinner = ora('Fetching results...').start(); try { let results; if (options.type === 'campaign') { results = await this.context.tools.getCampaignResults({ campaign_id: id, start_date: options.start, end_date: options.end }); } else { results = await this.context.tools.getExperimentResults({ experiment_id: id, start_date: options.start, end_date: options.end, stats_config: options.metrics }); } spinner.succeed('Results fetched'); await this.outputResults(results, options); } catch (error) { spinner.fail('Failed to fetch results'); throw error; } }, /** * Handle performance analysis command */ async handlePerformanceAnalysis(options) { console.log('Performance analysis not yet implemented'); // TODO: Implement performance analysis }, /** * Handle structured analytics command with enhanced features */ async handleAnalyze(query, options) { if (!query && !options.template && !options.query) { console.log(chalk.yellow('πŸ” Structured Analytics Engine\n')); console.log('Examples:'); console.log(chalk.cyan(' optly analytics analyze --template variable_analysis')); console.log(chalk.cyan(' optly analytics analyze --query \'{"from":"flags_unified_view"}\'')); console.log(chalk.cyan(' optly analytics analyze --interactive')); console.log(chalk.cyan(' optly analytics analyze --template list_flags --optimize --benchmark')); console.log('\nAvailable templates: variable_analysis, complexity_analysis, performance_trends, audience_usage, recent_changes, unused_entities'); console.log('\nOptions:'); console.log(' --no-cache Bypass query cache for fresh results'); console.log(' --optimize Show query optimization details'); console.log(' --benchmark Show performance benchmarks'); console.log(' --explain Explain query interpretation'); return; } // Initialize ora spinner for progress reporting const spinner = ora('Initializing analytics engine...').start(); try { // Lazy load the analytics engine for direct access const module = await import(new URL('../analytics/AnalyticsEngine.js', import.meta.url).href); const { AnalyticsEngine } = module; const db = this.context.storage.getDatabase(); const analyticsEngine = new AnalyticsEngine(db, { allowedProjects: options.project ? [options.project] : undefined }); // Prepare analytics options const analyzeOptions = { format: options.format || 'table', limit: parseInt(options.limit) || 100, interactive: options.interactive, simplified: true, projectId: options.project }; // Add cache control if (options.noCache) { analyzeOptions.bypassCache = true; spinner.text = 'Bypassing cache, fetching fresh data...'; } if (options.cacheTtl) { analyzeOptions.cacheTTL = parseInt(options.cacheTtl); } // Determine input let input; if (options.query) { // Direct structured query input = JSON.parse(options.query); } else if (options.template) { // Template-based query input = { template: options.template, template_params: options.templateParams || {} }; } else if (query) { // Legacy query argument (for backward compatibility) input = query; } // Set up progress callback let lastProgress = 0; analyzeOptions.progressCallback = (progress, message) => { lastProgress = progress; const progressText = message || `Processing query... ${Math.round(progress * 100)}%`; spinner.text = progressText; }; // Execute analysis spinner.text = 'Analyzing query...'; const startTime = Date.now(); const result = await analyticsEngine.analyze(input, analyzeOptions); const endTime = Date.now(); spinner.succeed('Analysis complete'); // Display results await this.outputResults(result, options); // Show optimization details if requested if (options.optimize && result.metadata?.optimizations) { console.log(chalk.bold.blue('\nπŸ”§ Query Optimization Details:\n')); const opt = result.metadata.optimizations; if (opt.originalQuery) { console.log(chalk.yellow('Original Query:')); console.log(chalk.gray(' ' + opt.originalQuery)); } if (opt.optimizedQuery) { console.log(chalk.yellow('\nOptimized Query:')); console.log(chalk.gray(' ' + opt.optimizedQuery)); } if (opt.appliedOptimizations) { console.log(chalk.yellow('\nApplied Optimizations:')); opt.appliedOptimizations.forEach((o) => { console.log(chalk.green(` βœ“ ${o.type}: ${o.description}`)); if (o.impact) { console.log(chalk.gray(` Impact: ${o.impact}`)); } }); } if (opt.suggestedIndexes) { console.log(chalk.yellow('\nSuggested Indexes:')); opt.suggestedIndexes.forEach((idx) => { console.log(chalk.cyan(` - ${idx.table}: ${idx.columns.join(', ')}`)); }); } } // Show performance benchmarks if requested if (options.benchmark) { console.log(chalk.bold.magenta('\nπŸ“Š Performance Benchmarks:\n')); console.log(`Total execution time: ${chalk.green((endTime - startTime) + 'ms')}`); if (result.metadata?.performanc