@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
1,217 lines (1,192 loc) β’ 115 kB
JavaScript
/**
* 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