UNPKG

claude-gemini-multimodal-bridge

Version:

Enterprise-grade AI integration bridge connecting Claude Code, Gemini CLI, and Google AI Studio with intelligent routing and advanced multimodal processing capabilities

951 lines (947 loc) β€’ 45.1 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { LayerManager } from './LayerManager.js'; import { logger } from '../utils/logger.js'; import { safeExecute } from '../utils/errorHandler.js'; import path from 'path'; import fs from 'fs'; import { CGMBError, DocumentAnalysisArgsSchema, EnhancedCGMBRequestSchema, MultimodalProcessArgsSchema, WorkflowDefinitionArgsSchema, } from './types.js'; import { ConfigSchema } from './types.js'; export class CGMBServer { server; layerManager; config; initialized = false; constructor(config) { this.server = new Server({ name: 'claude-gemini-multimodal-bridge', version: '1.0.0', description: 'claude-gemini-multimodal-bridge v1.0.0 - Enterprise-grade multi-layer AI integration with OAuth authentication, automatic translation, intelligent routing, and advanced multimodal processing for Claude Code.' }, { capabilities: { tools: {}, resources: {}, prompts: {}, }, }); this.config = ConfigSchema.parse({ ...this.getDefaultConfig(), ...config, }); this.layerManager = new LayerManager(this.config); this.setupErrorHandling(); this.registerTools(); } async initialize() { try { logger.info('Initializing CGMB Server...'); await this.verifyDependencies(); await this.layerManager.initializeLayers(); this.initialized = true; logger.info('CGMB Server initialized successfully'); } catch (error) { logger.error('Failed to initialize CGMB Server', error); throw new CGMBError('Server initialization failed', 'INIT_ERROR', undefined, { originalError: error }); } } async start() { if (!this.initialized) { await this.initialize(); } const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('CGMB Server started and listening...'); } async stop() { try { logger.info('Stopping CGMB Server...'); if (this.server) { await this.server.close(); } logger.info('CGMB Server stopped successfully'); } catch (error) { logger.error('Error stopping CGMB Server', error); throw error; } } registerTools() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'cgmb_get_layer_requirements', description: 'πŸ“‹ **Layer Info Tool** - Get AI layer capabilities:\n' + 'β€’ Returns detailed JSON with each layer\'s:\n' + ' - Input formats and requirements\n' + ' - Capabilities and features\n' + ' - Limitations and quotas\n' + 'β€’ Layers: gemini (text/search), aistudio (multimodal), adaptive', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, }, { name: 'cgmb', description: '🎯 **CGMB Universal AI Handler** - Processes all CGMB requests:\n' + '\n' + 'πŸ“‹ **Supported Commands** (auto-detected from prompt):\n' + 'β€’ chat/ask/tell β†’ Interactive conversation\n' + 'β€’ search/find/look up β†’ Web search via Gemini CLI\n' + 'β€’ analyze/review/examine β†’ Document/file analysis\n' + 'β€’ generate/create image β†’ Image generation via AI Studio\n' + 'β€’ generate/create audio/speech β†’ Audio generation via AI Studio\n' + 'β€’ process/handle files β†’ Multimodal file processing\n' + 'β€’ compare/diff β†’ Document comparison\n' + 'β€’ extract/get β†’ Information extraction\n' + 'β€’ translate/convert β†’ Translation/conversion\n' + '\n' + 'πŸ”§ **Features**:\n' + 'β€’ URL Detection: https:// links processed directly by Gemini CLI\n' + 'β€’ Path Resolution: ./relative β†’ /absolute using workingDirectory\n' + 'β€’ File Validation: Checks existence and read permissions\n' + 'β€’ Smart Routing: Auto-selects optimal AI layer\n' + 'β€’ Error Context: Shows original + resolved paths\n' + '\n' + 'πŸ“ **File Support**:\n' + 'β€’ Documents: PDF (max 1000 pages), TXT, MD, HTML\n' + 'β€’ Images: PNG, JPG, GIF (analysis + generation)\n' + 'β€’ Audio: WAV, MP3 (analysis + generation)\n' + 'β€’ Code: JS, PY, TS, etc.\n' + '\n' + 'πŸ’‘ **Usage Examples**:\n' + 'β€’ "CGMB search for latest AI news"\n' + 'β€’ "CGMB analyze document.pdf"\n' + 'β€’ "CGMB generate image of sunset"\n' + 'β€’ "CGMB create audio saying welcome"\n' + 'β€’ "CGMB process image.png and doc.pdf"\n' + '\n' + '⚠️ **Usage Guidelines**:\n' + 'β€’ βœ… **Recommended Commands** (for normal use):\n' + ' - chat/search/analyze/generate-image/generate-audio\n' + ' - Example: "cgmb chat \'latest AI news\'"\n' + 'β€’ ⚠️ **Internal Commands** (usually not needed):\n' + ' - gemini/aistudio β†’ Debug/test specific layers\n' + ' - Use recommended commands instead\n' + '\n' + '⚠️ **Important**: Always include "CGMB" keyword to trigger this tool', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'User prompt for AI processing (web search, analysis, multimodal tasks)', }, workingDirectory: { type: 'string', description: 'Working directory context for relative path resolution (optional)', }, targetLayer: { type: 'string', enum: ['gemini', 'aistudio', 'adaptive'], description: 'Target layer for direct routing (optional)', }, preformatted: { type: 'boolean', description: 'Whether the data is already formatted for the target layer', default: false, }, formattedData: { type: 'object', description: 'Pre-formatted data for layers (when preformatted=true)', properties: { geminiFormat: { type: 'object', properties: { prompt: { type: 'string' }, args: { type: 'array', items: { type: 'string' } } } }, aistudioFormat: { type: 'object', properties: { apiData: { type: 'object' }, files: { type: 'array', items: { type: 'string' } } } } } }, files: { type: 'array', description: 'Optional files to process', items: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, type: { type: 'string', enum: ['image', 'audio', 'pdf', 'document', 'text', 'video'], description: 'File type', }, }, required: ['path', 'type'], }, default: [], }, options: { type: 'object', description: 'Processing options', properties: { priority: { type: 'string', enum: ['fast', 'balanced', 'quality'], description: 'Processing priority', default: 'balanced', }, }, default: {}, }, }, required: ['prompt'], }, }, { name: 'cgmb_multimodal_process', description: '🎨 **Multimodal File Processor** - Specialized file handling:\n' + 'β€’ File Types: image, audio, pdf, document, text, video\n' + 'β€’ Workflows: analysis, conversion, extraction, generation\n' + 'β€’ Batch Mode: Process multiple files together\n' + 'β€’ Path Support: Relative (./file) and absolute (/path/file)\n' + 'β€’ Options: layer_priority, execution_mode, quality_level\n' + 'Auto-selected when multiple files specified or @file mentions', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'Processing instructions for the multimodal content', }, workingDirectory: { type: 'string', description: 'Working directory context for relative path resolution (optional)', }, files: { type: 'array', description: 'Array of files to process', items: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, type: { type: 'string', enum: ['image', 'audio', 'pdf', 'document', 'text', 'video'], description: 'File type', }, }, required: ['path', 'type'], }, }, workflow: { type: 'string', enum: ['analysis', 'conversion', 'extraction', 'generation'], description: 'Type of workflow to execute', }, options: { type: 'object', description: 'Processing options', properties: { layer_priority: { type: 'string', enum: ['claude', 'gemini', 'aistudio', 'adaptive'], description: 'Preferred layer for processing', }, execution_mode: { type: 'string', enum: ['sequential', 'parallel', 'adaptive'], description: 'How to execute the workflow', }, quality_level: { type: 'string', enum: ['fast', 'balanced', 'quality'], description: 'Quality vs speed preference', }, }, }, }, required: ['prompt', 'files', 'workflow'], }, }, { name: 'cgmb_document_analysis', description: 'πŸ“„ **Document Analysis Expert** - Deep document processing:\n' + 'β€’ Supported: PDF (via Gemini File API, max 1000 pages), TXT, MD, DOCX\n' + 'β€’ Analysis Types: summary, comparison, extraction, translation\n' + 'β€’ Batch PDFs: Automatic detection for multiple PDF processing\n' + 'β€’ Path Handling: Relative paths resolved with workingDirectory\n' + 'Auto-selected for document-specific analysis requests', inputSchema: { type: 'object', properties: { documents: { type: 'array', items: { type: 'string' }, description: 'List of document paths to analyze', }, workingDirectory: { type: 'string', description: 'Working directory context for relative path resolution (optional)', }, analysis_type: { type: 'string', enum: ['summary', 'comparison', 'extraction', 'translation'], description: 'Type of analysis to perform', }, output_requirements: { type: 'string', description: 'Specific requirements for the output format', }, }, required: ['documents', 'analysis_type'], }, }, { name: 'cgmb_workflow_orchestration', description: 'πŸ”„ **Workflow Orchestrator** - Complex multi-step tasks:\n' + 'β€’ Modes: sequential, parallel, adaptive execution\n' + 'β€’ Multi-Layer: Combines all 3 AI layers as needed\n' + 'β€’ Dependencies: Define step relationships\n' + 'β€’ Use Cases: Research workflows, report generation, data pipelines\n' + 'Auto-selected for complex multi-step requests', inputSchema: { type: 'object', properties: { workflow_definition: { type: 'object', description: 'Complete workflow definition', properties: { steps: { type: 'array', description: 'Workflow steps', items: { type: 'object' }, }, }, }, input_data: { type: 'object', description: 'Input data for the workflow', }, execution_mode: { type: 'string', enum: ['sequential', 'parallel', 'adaptive'], description: 'Execution strategy', }, }, required: ['workflow_definition', 'input_data'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { logger.info(`Tool called: ${name}`, { args }); let result; switch (name) { case 'cgmb_get_layer_requirements': result = await this.handleGetLayerRequirements(); break; case 'cgmb': result = await this.handleCGMBUnified(args); break; case 'cgmb_multimodal_process': case 'multimodal_process': result = await this.handleMultimodalProcess(args); break; case 'cgmb_document_analysis': case 'document_analysis': result = await this.handleDocumentAnalysis(args); break; case 'cgmb_workflow_orchestration': case 'workflow_orchestration': result = await this.handleWorkflowOrchestration(args); break; default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } logger.info(`Tool completed: ${name}`, { success: !result.isError, contentLength: result.content.length, }); return result; } catch (error) { logger.error(`Tool failed: ${name}`, error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error executing ${name}: ${errorMessage}`, }, ], isError: true, }; } }); } async handleGetLayerRequirements() { const requirements = { gemini: { format: 'Text prompts via stdin with optional command-line arguments', requirements: [ 'Text-based prompts for search and analysis', 'Web search queries for current information', 'No file upload capability - text only' ], capabilities: [ 'Real-time web search', 'Current information retrieval', 'Fast text processing', 'Natural language understanding' ], example: { stdin: 'What are the latest AI trends in 2024?', args: [] }, limitations: [ 'No direct file processing', 'Text-only input/output', 'Search results depend on web availability' ] }, aistudio: { format: 'JSON API format with base64-encoded files', requirements: [ 'Multimodal files (images, PDFs, documents)', 'Generation tasks (images, audio, video)', 'Complex document analysis' ], capabilities: [ 'Image generation with Imagen 3', 'Video generation with Veo 2', 'Document processing (PDF, DOCX, etc.)', 'Multimodal analysis', 'File format conversion' ], example: { apiData: { prompt: 'Analyze this document and extract key points', model: 'gemini-2.0-flash-exp', generationConfig: { temperature: 0.7, maxOutputTokens: 16384 } }, files: ['base64_encoded_file_content...'] }, limitations: [ 'File size limits (100MB total)', 'API quota restrictions', 'Processing time for large files' ] }, adaptive: { format: 'Automatic selection based on task requirements', requirements: [ 'Any type of request', 'CGMB automatically determines best layer' ], capabilities: [ 'Intelligent routing', 'Optimal layer selection', 'Fallback strategies', 'Combined layer processing' ], example: { prompt: 'CGMB analyze this document and search for related information', files: [{ path: 'document.pdf', type: 'pdf' }] } } }; return { content: [ { type: 'text', text: JSON.stringify(requirements, null, 2) } ] }; } async handleCGMBUnified(args) { return safeExecute(async () => { logger.info('CGMB unified handler starting', { args: typeof args === 'object' ? JSON.stringify(args).substring(0, 200) : args }); const request = this.parseEnhancedRequest(args); if (request.preformatted && request.formattedData) { logger.info('Using preformatted data from Claude Code', { targetLayer: request.targetLayer, hasGeminiFormat: !!request.formattedData.geminiFormat, hasAistudioFormat: !!request.formattedData.aistudioFormat }); const result = await this.processPreformattedRequest(request); return this.formatResponse(result, true); } logger.info('Using standard processing (not preformatted)'); const normalizedRequest = this.validateAndNormalize(args); logger.info('Request normalized', { hasCGMB: normalizedRequest.hasCGMB, promptLength: normalizedRequest.prompt.length, filesCount: normalizedRequest.files.length, hasUrls: normalizedRequest.hasUrls, urlCount: normalizedRequest.urlsDetected.length }); if (normalizedRequest.hasUrls) { logger.info('URLs detected - auto-routing to Gemini CLI layer', { urls: normalizedRequest.urlsDetected, originalPrompt: normalizedRequest.prompt }); let analysisPrompt; if (normalizedRequest.urlsDetected.length === 1) { const basePrompt = normalizedRequest.prompt.toLowerCase().includes('analyze') ? normalizedRequest.prompt : `Analyze the content at ${normalizedRequest.urlsDetected[0]}`; analysisPrompt = basePrompt; } else { analysisPrompt = `Analyze the content at these URLs: ${normalizedRequest.urlsDetected.join(', ')}. ${normalizedRequest.prompt}`; } logger.info('Executing URL analysis via Gemini CLI', { analysisPrompt }); const result = await this.layerManager.getGeminiLayer().execute({ type: 'text_processing', prompt: analysisPrompt, files: [], useSearch: true }); const urlResponse = { success: result.success, data: result.data, metadata: { ...result.metadata, layer: 'gemini', routing_reason: 'url_auto_routing', urls_processed: normalizedRequest.urlsDetected.length, processing_time: result.metadata?.duration || 0 } }; return this.formatResponse(urlResponse, normalizedRequest.hasCGMB); } const convertedRequest = this.convertForLayers(normalizedRequest.prompt, normalizedRequest.files, normalizedRequest.options); logger.info('Request converted for layers', { workflow: convertedRequest.workflow, optionsKeys: Object.keys(convertedRequest.options) }); logger.info('Calling LayerManager.processMultimodal...'); const result = await this.layerManager.processMultimodal(convertedRequest.prompt, convertedRequest.files, convertedRequest.workflow, convertedRequest.options); logger.info('LayerManager.processMultimodal completed', { success: result.success, hasResults: !!result.results, hasSummary: !!result.summary }); const response = this.formatResponse(result, normalizedRequest.hasCGMB); logger.info('Response formatted', { contentLength: response.content?.[0]?.text?.length || 0 }); return response; }, { operationName: 'cgmb_unified_process', timeout: this.config.claude.timeout, }); } parseEnhancedRequest(args) { try { return EnhancedCGMBRequestSchema.parse(args); } catch (error) { logger.debug('Failed to parse as enhanced request, using basic format'); const basicArgs = args; return { prompt: basicArgs.prompt || '', targetLayer: undefined, preformatted: false, formattedData: undefined, files: basicArgs.files || [], options: basicArgs.options || {} }; } } async processPreformattedRequest(request) { const targetLayer = request.targetLayer || 'adaptive'; if (targetLayer === 'gemini' && request.formattedData?.geminiFormat) { logger.info('Executing preformatted request on Gemini layer'); const result = await this.layerManager.getGeminiLayer().execute({ type: 'text_processing', prompt: request.formattedData.geminiFormat.stdin, files: [], useSearch: true, args: request.formattedData.geminiFormat.args }); return { success: result.success, results: [result], metadata: { workflow: 'analysis', execution_mode: 'direct', total_duration: result.metadata?.duration || 0, steps_completed: 1, steps_failed: 0, layers_used: ['gemini'], optimization: 'preformatted-direct' } }; } if (targetLayer === 'aistudio' && request.formattedData?.aistudioFormat) { logger.info('Executing preformatted request on AI Studio layer'); } return this.layerManager.processMultimodal(request.prompt, request.files || [], 'analysis', request.options); } validateAndNormalize(args) { if (!args || typeof args !== 'object' || !('prompt' in args)) { throw new Error('Invalid arguments: prompt is required'); } const { prompt, files = [], options = {}, workingDirectory } = args; if (typeof prompt !== 'string' || !prompt.trim()) { throw new Error('Invalid prompt: must be a non-empty string'); } const hasCGMB = prompt.toLowerCase().includes('cgmb'); const urlRegex = /https?:\/\/[^\s]+/g; const urlsInPrompt = prompt.match(urlRegex) || []; const fileArray = Array.isArray(files) ? files : []; const urlsInFiles = []; const resolvedFiles = []; for (const file of fileArray) { const filePath = typeof file === 'string' ? file : (file?.path || ''); if (/^https?:\/\//.test(filePath)) { urlsInFiles.push(filePath); resolvedFiles.push(typeof file === 'string' ? { path: filePath, type: 'url' } : { ...file, type: 'url' }); } else if (filePath) { try { const normalizedPath = path.normalize(filePath); const baseDir = workingDirectory || process.cwd(); const resolvedPath = path.isAbsolute(normalizedPath) ? normalizedPath : path.resolve(baseDir, normalizedPath); if (workingDirectory && !path.isAbsolute(normalizedPath)) { logger.info(`Relative path resolution using provided working directory`, { originalPath: filePath, normalizedPath, workingDirectory, resolvedPath }); } if (fs.existsSync(resolvedPath)) { const stats = fs.statSync(resolvedPath); if (stats.isFile()) { try { fs.accessSync(resolvedPath, fs.constants.R_OK); resolvedFiles.push(typeof file === 'string' ? { path: resolvedPath, type: 'document' } : { ...file, path: resolvedPath, type: file.type || 'document' }); } catch (permError) { logger.warn(`File permission denied: ${filePath} -> ${resolvedPath}`); throw new Error(`Permission denied: ${filePath} Resolved to: ${resolvedPath} File exists but cannot be read. Fix: chmod +r "${resolvedPath}"`); } } else { logger.warn(`Path is not a file: ${filePath} -> ${resolvedPath}`); throw new Error(`Not a file: ${filePath} Resolved to: ${resolvedPath} This path points to a directory or special file. Use a specific file path instead.`); } } else { logger.warn(`File not found: ${filePath} -> ${resolvedPath}`); throw new Error(`File not found: ${filePath} Resolved path: ${resolvedPath} Working directory: ${workingDirectory || 'not provided'} Current directory: ${process.cwd()} Solutions: 1. Check file exists: ls -la "${resolvedPath}" 2. Use relative path: "./filename.pdf" (from current dir) 3. Use absolute path: "/full/path/to/file.pdf" 4. Verify working directory matches file location`); } } catch (error) { if (error instanceof Error && error.message.includes('not found')) { throw error; } logger.warn(`File path resolution error: ${filePath}`, error); throw new Error(`Invalid file path: ${filePath}`); } } } const urlsDetected = [...urlsInPrompt, ...urlsInFiles]; const hasUrls = urlsDetected.length > 0; if (hasUrls) { logger.info(`URLs detected in request`, { urlCount: urlsDetected.length, urls: urlsDetected.slice(0, 3), inPrompt: urlsInPrompt.length, inFiles: urlsInFiles.length }); } return { prompt: prompt.trim(), files: resolvedFiles, options: typeof options === 'object' ? options : {}, hasCGMB, hasUrls, urlsDetected }; } convertForLayers(prompt, files, options) { let workflow = 'analysis'; const lowerPrompt = prompt.toLowerCase(); if (lowerPrompt.includes('generat') || lowerPrompt.includes('creat')) { workflow = 'generation'; } else if (lowerPrompt.includes('convert') || lowerPrompt.includes('transform')) { workflow = 'conversion'; } else if (lowerPrompt.includes('extract') || lowerPrompt.includes('取得')) { workflow = 'extraction'; } const processedFiles = files.map(file => ({ path: file.path || '', type: file.type || 'document', ...file })); const processedOptions = { layer_priority: 'adaptive', execution_mode: 'adaptive', quality_level: 'balanced', ...options }; return { prompt, files: processedFiles, workflow, options: processedOptions }; } formatResponse(result, hasCGMB) { const prefix = hasCGMB ? '🎯 **CGMB**: ' : ''; if (result.metadata?.optimization === 'fast-path-bypass') { const mainResult = Array.isArray(result.results) ? result.results[0] : Object.values(result.results || {})[0]; let responseText = prefix + (mainResult?.data || 'Processing completed'); if (!hasCGMB) { responseText += '\n\nπŸ’‘ CGMB Commands:\n' + 'β€’ Chat: "CGMB tell me about..."\n' + 'β€’ Search: "CGMB search for latest..."\n' + 'β€’ Analyze: "CGMB analyze file.pdf"\n' + 'β€’ Generate: "CGMB create image/audio..."\n' + 'β€’ Process: "CGMB process files..."'; } return { content: [{ type: 'text', text: responseText }] }; } let responseText = prefix + (result.summary || result.content || 'Processing completed'); if (!hasCGMB) { responseText += '\n\nπŸ’‘ CGMB Commands:\n' + 'β€’ Chat: "CGMB tell me about..."\n' + 'β€’ Search: "CGMB search for latest..."\n' + 'β€’ Analyze: "CGMB analyze file.pdf"\n' + 'β€’ Generate: "CGMB create image/audio..."\n' + 'β€’ Process: "CGMB process files..."'; } return { content: [{ type: 'text', text: responseText }] }; } async handleMultimodalProcess(args) { return safeExecute(async () => { const validatedArgs = MultimodalProcessArgsSchema.parse(args); const result = await this.layerManager.processMultimodal(validatedArgs.prompt, validatedArgs.files, validatedArgs.workflow, validatedArgs.options); const isLight = result.metadata?.optimization === 'fast-path-bypass'; if (isLight) { const mainResult = Array.isArray(result.results) ? result.results[0] : Object.values(result.results || {})[0]; return { content: [ { type: 'text', text: mainResult?.data || 'Processing completed' } ] }; } return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, { operationName: 'multimodal_process', timeout: this.config.claude.timeout, }); } async handleDocumentAnalysis(args) { return safeExecute(async () => { const validatedArgs = DocumentAnalysisArgsSchema.parse(args); const result = await this.layerManager.analyzeDocuments(validatedArgs.documents, validatedArgs.analysis_type, validatedArgs.output_requirements, validatedArgs.options); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, { operationName: 'document_analysis', timeout: this.config.claude.timeout, }); } async handleWorkflowOrchestration(args) { return safeExecute(async () => { const validatedArgs = WorkflowDefinitionArgsSchema.parse(args); const result = await this.layerManager.executeWorkflow(validatedArgs.workflow_definition, validatedArgs.input_data, { executionMode: validatedArgs.execution_mode ?? 'adaptive', timeout: 600000, }); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, { operationName: 'workflow_orchestration', timeout: 600000, }); } async verifyDependencies() { const checks = [ { name: 'Claude Code', check: async () => { try { const layer = this.layerManager.getClaudeLayer(); return await layer.isAvailable(); } catch { return false; } }, }, { name: 'Gemini CLI', check: async () => { try { const layer = this.layerManager.getGeminiLayer(); return await layer.isAvailable(); } catch { return false; } }, }, { name: 'AI Studio MCP', check: async () => { try { const layer = this.layerManager.getAIStudioLayer(); return await layer.isAvailable(); } catch { return false; } }, }, ]; const results = await Promise.allSettled(checks.map(async ({ name, check }) => { try { const available = await check(); return { name, available, error: null }; } catch (error) { return { name, available: false, error }; } })); const failures = results .map((result, index) => ({ ...checks[index], result })) .filter(({ result }) => result.status === 'rejected' || (result.status === 'fulfilled' && !result.value.available)); if (failures.length > 0) { logger.warn('Some dependencies are not available:', failures); } logger.info('Dependency verification completed', { totalChecks: checks.length, failures: failures.length, available: checks.length - failures.length, }); } setupErrorHandling() { process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection at:', { promise, reason }); }); process.on('uncaughtException', (error) => { logger.error('Uncaught Exception:', error); process.exit(1); }); } getDefaultConfig() { return { gemini: { api_key: process.env.GEMINI_API_KEY ?? '', model: process.env.GEMINI_MODEL ?? 'gemini-2.5-pro', timeout: parseInt(process.env.GEMINI_TIMEOUT ?? '60000'), max_tokens: parseInt(process.env.GEMINI_MAX_OUTPUT_TOKENS ?? '16384'), temperature: parseFloat(process.env.GEMINI_TEMPERATURE ?? '0.2'), }, claude: { code_path: process.env.CLAUDE_CODE_PATH ?? '/usr/local/bin/claude', timeout: parseInt(process.env.REQUEST_TIMEOUT ?? '300000'), }, aistudio: { enabled: process.env.AISTUDIO_MCP_ENABLED !== 'false', max_files: parseInt(process.env.GEMINI_MAX_FILES ?? '10'), max_file_size: parseInt(process.env.GEMINI_MAX_TOTAL_FILE_SIZE ?? '100'), }, cache: { enabled: process.env.ENABLE_CACHING !== 'false', ttl: parseInt(process.env.CACHE_TTL ?? '3600'), }, logging: { level: this.validateLogLevel(process.env.LOG_LEVEL) ?? 'info', file: process.env.LOG_FILE, }, }; } validateLogLevel(level) { const validLevels = ['error', 'warn', 'info', 'debug']; if (!level) { return 'info'; } const lowerLevel = level.toLowerCase(); if (lowerLevel === 'verbose') { return 'debug'; } return validLevels.includes(lowerLevel) ? lowerLevel : 'info'; } }