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

980 lines (977 loc) β€’ 62.3 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 { createRequire } from 'module'; 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/promises'; import fsSync from 'fs'; import { CGMBError, DocumentAnalysisArgsSchema, EnhancedCGMBRequestSchema, MultimodalProcessArgsSchema, WorkflowDefinitionArgsSchema, } from './types.js'; import { ConfigSchema } from './types.js'; const require = createRequire(import.meta.url); const packageJson = require('../../package.json'); const VERSION = packageJson.version; export class CGMBServer { server; layerManager; config; initialized = false; constructor(config) { this.server = new Server({ name: 'claude-gemini-multimodal-bridge', version: VERSION, description: `claude-gemini-multimodal-bridge v${VERSION} - 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 analyze C:\\path\\to\\file.png"\n' + 'β€’ "CGMB analyze /path/to/file.pdf"\n' + 'β€’ "CGMB generate image of sunset"\n' + 'β€’ "CGMB create audio saying welcome"\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) { const urlTypes = normalizedRequest.urlsDetected.map(url => ({ url, type: this.detectUrlType(url) })); const fileUrls = urlTypes.filter(u => u.type !== 'web'); const webUrls = urlTypes.filter(u => u.type === 'web'); logger.info('URLs detected - classifying for routing', { totalUrls: normalizedRequest.urlsDetected.length, fileUrls: fileUrls.length, webUrls: webUrls.length, urlTypes: urlTypes.map(u => ({ url: u.url.substring(0, 50), type: u.type })) }); if (fileUrls.length > 0) { logger.info('File URLs detected - routing directly to AI Studio', { fileUrls: fileUrls.map(u => u.url) }); try { const filesForProcessing = fileUrls.map(f => ({ path: f.url, type: f.type === 'pdf' ? 'document' : f.type })); const aiStudioLayer = await this.layerManager.getAIStudioLayerAsync(); const result = await aiStudioLayer.execute({ type: 'document_analysis', files: filesForProcessing, instructions: normalizedRequest.prompt }); const urlResponse = { success: result.success, data: result.data, metadata: { ...result.metadata, layer: 'aistudio', routing_reason: 'file_url_direct_routing', urls_processed: fileUrls.length } }; return this.formatResponse(urlResponse, normalizedRequest.hasCGMB); } catch (error) { logger.error('AI Studio URL processing failed', { error: error.message }); const errorResponse = { success: false, data: `URL PDF processing failed: ${error.message}. Try downloading the PDF and using: CGMB analyze local-file.pdf`, metadata: { layer: 'error', routing_reason: 'url_pdf_processing_failed', error: error.message } }; return this.formatResponse(errorResponse, normalizedRequest.hasCGMB); } } logger.info('Web URLs - routing to Gemini CLI layer', { urls: webUrls.length > 0 ? webUrls.map(u => u.url) : normalizedRequest.urlsDetected }); 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 geminiLayer = await this.layerManager.getGeminiLayerAsync(); const result = await geminiLayer.execute({ type: 'text_processing', prompt: analysisPrompt, files: [], useSearch: true }); const urlResponse = { success: result.success, data: result.data, metadata: { ...result.metadata, layer: 'gemini', routing_reason: 'web_url_auto_routing', urls_processed: normalizedRequest.urlsDetected.length, processing_time: result.metadata?.duration || 0 } }; return this.formatResponse(urlResponse, normalizedRequest.hasCGMB); } const searchKeywords = ['search', 'find', 'look up', 'lookup', 'what is', 'latest', 'news', 'current', 'today', '怜紒', 'ζœ€ζ–°', 'ニγƒ₯γƒΌγ‚Ή']; const lowerPrompt = normalizedRequest.prompt.toLowerCase(); const isSearchTask = searchKeywords.some(keyword => lowerPrompt.includes(keyword.toLowerCase())) && normalizedRequest.files.length === 0; if (isSearchTask) { logger.info('Search keywords detected - auto-routing to Gemini CLI layer', { prompt: normalizedRequest.prompt, matchedKeywords: searchKeywords.filter(k => lowerPrompt.includes(k.toLowerCase())) }); const geminiLayerForSearch = await this.layerManager.getGeminiLayerAsync(); const result = await geminiLayerForSearch.execute({ type: 'search', prompt: normalizedRequest.prompt, files: [], useSearch: true }); const searchResponse = { success: result.success, data: result.data, metadata: { ...result.metadata, layer: 'gemini', routing_reason: 'search_auto_routing', processing_time: result.metadata?.duration || 0 } }; return this.formatResponse(searchResponse, normalizedRequest.hasCGMB); } const imageGenKeywords = ['generate image', 'create image', 'make image', 'generate picture', 'create picture', 'draw', 'η”»εƒη”Ÿζˆ', 'η”»εƒγ‚’δ½œζˆ']; const audioGenKeywords = ['generate audio', 'create audio', 'make audio', 'create speech', 'text to speech', 'ιŸ³ε£°η”Ÿζˆ', 'ιŸ³ε£°γ‚’δ½œζˆ']; const isImageGeneration = imageGenKeywords.some(keyword => lowerPrompt.includes(keyword.toLowerCase())); const isAudioGeneration = audioGenKeywords.some(keyword => lowerPrompt.includes(keyword.toLowerCase())); if ((isImageGeneration || isAudioGeneration) && normalizedRequest.files.length === 0) { logger.info('Generation task detected - auto-routing to AI Studio layer', { prompt: normalizedRequest.prompt, isImageGeneration, isAudioGeneration }); try { const aiStudioLayerForGen = await this.layerManager.getAIStudioLayerAsync(); let result; if (isImageGeneration) { result = await aiStudioLayerForGen.execute({ type: 'image_generation', prompt: normalizedRequest.prompt, files: [], action: 'generate_image' }); } else { result = await aiStudioLayerForGen.execute({ type: 'audio_generation', prompt: normalizedRequest.prompt, files: [], action: 'generate_audio' }); } const genResponse = { success: result.success, data: result.data, metadata: { ...result.metadata, layer: 'aistudio', routing_reason: isImageGeneration ? 'image_generation_routing' : 'audio_generation_routing', processing_time: result.metadata?.duration || 0 } }; return this.formatResponse(genResponse, normalizedRequest.hasCGMB); } catch (error) { logger.error('AI Studio generation failed - detailed diagnostics', { error: error.message, stack: error.stack, platform: process.platform, isWindows: process.platform === 'win32', isImageGeneration, isAudioGeneration, prompt: normalizedRequest.prompt.substring(0, 100), nodeVersion: process.version }); } } 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, workingDirectory: normalizedRequest.workingDirectory }); 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 geminiLayerPreformatted = await this.layerManager.getGeminiLayerAsync(); const result = await geminiLayerPreformatted.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 isWindows = process.platform === 'win32'; let preprocessedPath = filePath; const isWindowsAbsolutePath = /^[A-Za-z]:[/\\]/.test(filePath); if (isWindows && isWindowsAbsolutePath) { preprocessedPath = filePath.replace(/\//g, '\\'); } const normalizedPath = path.normalize(preprocessedPath); const baseDir = workingDirectory || process.cwd(); const isAbsolute = path.isAbsolute(normalizedPath) || isWindowsAbsolutePath; const resolvedPath = isAbsolute ? 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 (fsSync.existsSync(resolvedPath)) { const stats = fsSync.statSync(resolvedPath); if (stats.isFile()) { try { fsSync.accessSync(resolvedPath, fsSync.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 filePathRegex = /(?:[A-Za-z]:\\[^\s"'<>|]+\.[a-zA-Z0-9]+|\/(?!https?:)[^\s"'<>|]+\.[a-zA-Z0-9]+|\.\.?\/[^\s"'<>|]+\.[a-zA-Z0-9]+)/gi; const localPathsInPrompt = prompt.match(filePathRegex) || []; if (localPathsInPrompt.length > 0) { logger.info('Local file paths detected in prompt', { paths: localPathsInPrompt }); for (const detectedPath of localPathsInPrompt) { const isDuplicate = resolvedFiles.some(f => (typeof f === 'string' ? f : f.path) === detectedPath); if (!isDuplicate) { try { const isWindows = process.platform === 'win32'; let preprocessedPath = detectedPath; const isWindowsAbsolutePath = /^[A-Za-z]:[/\\]/.test(detectedPath); if (isWindows && isWindowsAbsolutePath) { preprocessedPath = detectedPath.replace(/\//g, '\\'); } const normalizedPath = path.normalize(preprocessedPath); const baseDir = workingDirectory || process.cwd(); const isAbsolute = path.isAbsolute(normalizedPath) || isWindowsAbsolutePath; const resolvedPath = isAbsolute ? normalizedPath : path.resolve(baseDir, normalizedPath); if (fsSync.existsSync(resolvedPath)) { const stats = fsSync.statSync(resolvedPath); if (stats.isFile()) { const detectedType = this.layerManager.detectFileType(resolvedPath); resolvedFiles.push({ path: resolvedPath, type: detectedType }); logger.info('File path extracted from prompt', { original: detectedPath, resolved: resolvedPath, type: detectedType }); } } else { logger.warn('File path in prompt does not exist', { original: detectedPath, resolved: resolvedPath }); } } catch (error) { logger.warn('Failed to process file path from prompt', { path: detectedPath, error: error.message }); } } } } 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, workingDirectory: workingDirectory || process.cwd() }; } 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**: ' : ''; let responseData = null; if (typeof result.data === 'string' && result.data.trim()) { responseData = result.data; } else if (Array.isArray(result.results) && result.results.length > 0) { const firstResult = result.results[0]; if (typeof firstResult?.data === 'string' && firstResult.data.trim()) { responseData = firstResult.data; } else if (firstResult?.data?.content && Array.isArray(firstResult.data.content) && firstResult.data.content[0]?.text) { responseData = firstResult.data.content[0].text; } else if (firstResult?.content) { if (Array.isArray(firstResult.content) && firstResult.content[0]?.text) { responseData = firstResult.content[0].text; } else if (typeof firstResult.content === 'string') { responseData = firstResult.content; } else { responseData = JSON.stringify(firstResult.content); } } } else if (result.results && typeof result.results === 'object' && !Array.isArray(result.results)) { const resultValues = Object.values(result.results); if (resultValues.length > 0) { const firstResult = resultValues[0]; if (typeof firstResult?.data === 'string' && firstResult.data.trim()) { responseData = firstResult.data; } else if (firstResult?.data?.content && Array.isArray(firstResult.data.content) && firstResult.data.content[0]?.text) { responseData = firstResult.data.content[0].text; } else if (firstResult?.content) { if (Array.isArray(firstResult.content) && firstResult.content[0]?.text) { responseData = firstResult.content[0].text; } else if (typeof firstResult.content === 'string') { responseData = f