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
JavaScript
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';
}
}