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
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 { 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