/** * Remote Server Client for Chrome Extension * Connects to the remote MCP server via WebSocket */ import { TIMEOUTS } from '@/common/constants'; import { handleCallTool } from '@/entrypoints/background/tools'; import { DEFAULT_CONNECTION_CONFIG } from '@/common/env-config'; export interface RemoteServerConfig { serverUrl: string; reconnectInterval: number; maxReconnectAttempts: number; } export interface SessionInfo { userId: string; sessionId: string; connectionId: string; } export interface RemoteServerStatus { connected: boolean; connecting: boolean; connectionTime?: number; reconnectAttempts: number; error?: string; serverUrl: string; sessionInfo?: SessionInfo; } export class RemoteServerClient { private ws: WebSocket | null = null; private config: RemoteServerConfig; private reconnectAttempts = 0; private isConnecting = false; private messageHandlers = new Map void>(); private connectionTime: number | null = null; private statusUpdateCallback: ((status: any) => void) | null = null; private persistentConnectionEnabled = true; // Enable persistent connections by default private connectionStateKey = 'remoteServerConnectionState'; private keepAliveInterval: NodeJS.Timeout | null = null; private sessionInfo: SessionInfo | null = null; constructor(config: Partial = {}) { this.config = { serverUrl: config.serverUrl || DEFAULT_CONNECTION_CONFIG.serverUrl, reconnectInterval: config.reconnectInterval || DEFAULT_CONNECTION_CONFIG.reconnectInterval, maxReconnectAttempts: config.maxReconnectAttempts || 999999, // Effectively unlimited for persistent connections }; // Do not auto-connect on initialization - only connect when user explicitly requests it console.log( 'RemoteServerClient: Initialized without auto-connection. Use connect() method to establish connection.', ); } /** * Set up keep-alive mechanism to prevent connection timeouts */ private setupKeepAlive(): void { // Clear any existing keep-alive interval if (this.keepAliveInterval) { clearInterval(this.keepAliveInterval); } // Send a ping every 25 seconds to keep connection alive this.keepAliveInterval = setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { // Send a simple ping message with proper ID to avoid "unknown request ID" warnings try { const pingId = `ping_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; this.ws.send( JSON.stringify({ id: pingId, type: 'ping', timestamp: Date.now(), }), ); } catch (error) { console.warn('RemoteServerClient: Failed to send keep-alive ping:', error); } } }, 25000); // 25 seconds - slightly less than server's 30-second ping interval } /** * Clear keep-alive mechanism */ private clearKeepAlive(): void { if (this.keepAliveInterval) { clearInterval(this.keepAliveInterval); this.keepAliveInterval = null; } } setStatusUpdateCallback(callback: (status: any) => void) { this.statusUpdateCallback = callback; } /** * Save connection state to chrome storage for persistence */ private async saveConnectionState(): Promise { try { const state = { wasConnected: this.isConnected(), connectionTime: this.connectionTime, serverUrl: this.config.serverUrl, lastSaveTime: Date.now(), }; await chrome.storage.local.set({ [this.connectionStateKey]: state }); console.log('RemoteServerClient: Connection state saved', state); } catch (error) { console.warn('RemoteServerClient: Failed to save connection state:', error); } } /** * Load connection state from chrome storage (for manual restoration only) */ private async loadConnectionState(): Promise { try { const result = await chrome.storage.local.get(this.connectionStateKey); const state = result[this.connectionStateKey]; if (state && state.wasConnected) { console.log( 'RemoteServerClient: Found previous connection state (manual restoration available)', state, ); return state; } return null; } catch (error) { console.warn('RemoteServerClient: Failed to load connection state:', error); return null; } } /** * Manually restore connection from saved state (only when user requests it) */ async restoreConnectionFromState(): Promise { const state = await this.loadConnectionState(); if (!state) { console.log('RemoteServerClient: No previous connection state to restore'); return false; } const timeSinceLastSave = Date.now() - (state.lastSaveTime || 0); const maxRestoreAge = 24 * 60 * 60 * 1000; // 24 hours if (timeSinceLastSave < maxRestoreAge) { console.log('RemoteServerClient: Attempting to restore connection from saved state...'); try { await this.connect(); return true; } catch (error) { console.log('RemoteServerClient: Failed to restore previous connection:', error); return false; } } else { console.log('RemoteServerClient: Previous connection state too old, clearing...'); await this.clearConnectionState(); return false; } } /** * Clear saved connection state */ private async clearConnectionState(): Promise { try { await chrome.storage.local.remove(this.connectionStateKey); console.log('RemoteServerClient: Connection state cleared'); } catch (error) { console.warn('RemoteServerClient: Failed to clear connection state:', error); } } private notifyStatusUpdate(status: any) { if (this.statusUpdateCallback) { this.statusUpdateCallback(status); } // Also send to popup if available try { chrome.runtime.sendMessage( { type: 'remoteServerStatusUpdate', payload: status, }, (response) => { // Handle response or check for errors if (chrome.runtime.lastError) { // Silently ignore "Receiving end does not exist" errors // This happens when popup is not open } }, ); } catch (error) { // Ignore errors if popup is not available } } getStatus(): RemoteServerStatus { return { connected: this.isConnected(), connecting: this.isConnecting, reconnectAttempts: this.reconnectAttempts, connectionTime: this.connectionTime, serverUrl: this.config.serverUrl, sessionInfo: this.sessionInfo, }; } /** * Get current session information */ getSessionInfo(): SessionInfo | null { return this.sessionInfo; } /** * Check if session is active */ hasActiveSession(): boolean { return this.sessionInfo !== null && this.isConnected(); } /** * Generate or retrieve persistent user ID for this Chrome extension */ private async generateOrRetrieveUserId(): Promise { const storageKey = 'chrome_extension_user_id'; try { // Try to get existing user ID from storage first const result = await chrome.storage.local.get([storageKey]); if (result[storageKey]) { console.log(`Chrome Extension: Retrieved existing user ID: ${result[storageKey]}`); return result[storageKey]; } } catch (error) { console.warn('Failed to retrieve user ID from storage:', error); } // Generate new user ID if none exists const timestamp = Date.now(); const randomSuffix = Math.random().toString(36).substring(2, 15); const userId = `user_${timestamp}_${randomSuffix}`; try { // Save the new user ID to storage for persistence await chrome.storage.local.set({ [storageKey]: userId }); console.log(`Chrome Extension: Generated and saved new user ID: ${userId}`); } catch (error) { console.warn('Failed to save user ID to storage:', error); } return userId; } /** * Get the current user ID (public method) */ public async getCurrentUserId(): Promise { const storageKey = 'chrome_extension_user_id'; try { const result = await chrome.storage.local.get([storageKey]); return result[storageKey] || null; } catch (error) { console.warn('Failed to get current user ID:', error); return null; } } async connect(): Promise { // Check if already connected or connecting if (this.isConnecting) { console.log('Connection attempt already in progress'); return; } if (this.ws && this.ws.readyState === WebSocket.OPEN) { console.log('Already connected to remote server'); return; } // Clean up any existing connection in bad state if (this.ws && this.ws.readyState !== WebSocket.CLOSED) { console.log('Cleaning up existing connection in bad state'); this.ws.close(); this.ws = null; } this.isConnecting = true; this.notifyStatusUpdate({ connecting: true, error: undefined }); try { console.log(`Attempting to connect to: ${this.config.serverUrl}`); this.ws = new WebSocket(this.config.serverUrl); this.ws.onopen = async () => { console.log('Connected to remote MCP server'); this.isConnecting = false; this.reconnectAttempts = 0; this.connectionTime = Date.now(); // Generate or retrieve persistent user ID for this Chrome extension const userId = await this.generateOrRetrieveUserId(); // Send connection info to server for session management with user ID const connectionInfo = { type: 'connection_info', userId: userId, // Include the Chrome extension's user ID userAgent: navigator.userAgent, timestamp: Date.now(), extensionId: chrome.runtime.id, }; console.log(`Chrome Extension: Connecting with user ID: ${userId}`); this.ws?.send(JSON.stringify(connectionInfo)); this.notifyStatusUpdate({ connected: true, connecting: false, connectionTime: this.connectionTime, reconnectAttempts: this.reconnectAttempts, }); // Start keep-alive mechanism this.setupKeepAlive(); // Save connection state for persistence this.saveConnectionState(); }; this.ws.onmessage = (event) => { try { // Handle ping/pong for connection keep-alive if (event.data === 'ping') { this.ws?.send('pong'); return; } const data = JSON.parse(event.data); // Check if this is a session initialization message if (data.type === 'session_info' && data.sessionInfo) { this.sessionInfo = data.sessionInfo; console.log('Session info received:', this.sessionInfo); this.notifyStatusUpdate({ connected: true, connecting: false, connectionTime: this.connectionTime, reconnectAttempts: this.reconnectAttempts, sessionInfo: this.sessionInfo, }); return; } this.handleMessage(data); } catch (error) { console.error('Error parsing message from remote server:', error); } }; this.ws.onclose = (event) => { const wasConnected = this.connectionTime !== null; console.log( `Disconnected from remote MCP server (code: ${event.code}, reason: ${event.reason})`, ); this.isConnecting = false; this.connectionTime = null; // Stop keep-alive mechanism this.clearKeepAlive(); // Determine if this was an unexpected disconnection const wasUnexpected = wasConnected && event.code !== 1000; // 1000 = normal closure this.notifyStatusUpdate({ connected: false, connecting: false, connectionTime: null, error: wasUnexpected ? `Connection lost (code: ${event.code})` : undefined, }); // Only schedule reconnect for unexpected disconnections if (wasUnexpected) { this.scheduleReconnect(); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.isConnecting = false; // Provide more specific error messages based on the error let errorMessage = 'Connection error'; if (this.ws?.readyState === WebSocket.CONNECTING) { errorMessage = 'Failed to connect to server'; } else if (this.ws?.readyState === WebSocket.OPEN) { errorMessage = 'Connection error during communication'; } this.notifyStatusUpdate({ connected: false, connecting: false, error: errorMessage, }); }; // Wait for connection to open with enhanced error handling await new Promise((resolve, reject) => { if (!this.ws) { reject(new Error('WebSocket not initialized')); return; } const timeout = setTimeout(() => { this.isConnecting = false; if (this.ws && this.ws.readyState === WebSocket.CONNECTING) { this.ws.close(); } this.notifyStatusUpdate({ connected: false, connecting: false, error: 'Connection timeout - Server may be unreachable', }); reject(new Error('Connection timeout - Server may be unreachable')); }, TIMEOUTS.REMOTE_SERVER_CONNECTION); // Override the onopen handler for the promise const originalOnOpen = this.ws.onopen; this.ws.onopen = (event) => { clearTimeout(timeout); // Call the original handler if (originalOnOpen) { originalOnOpen.call(this.ws, event); } resolve(); }; // Override the onerror handler for the promise const originalOnError = this.ws.onerror; this.ws.onerror = (event) => { clearTimeout(timeout); this.isConnecting = false; // Determine error type based on readyState let errorMessage = 'Connection failed'; if (this.ws?.readyState === WebSocket.CONNECTING) { errorMessage = 'Failed to connect - Server may be offline'; } this.notifyStatusUpdate({ connected: false, connecting: false, error: errorMessage, }); // Call the original handler if (originalOnError) { originalOnError.call(this.ws, event); } reject(new Error(errorMessage)); }; }); } catch (error) { this.isConnecting = false; this.notifyStatusUpdate({ connected: false, connecting: false, error: error instanceof Error ? error.message : 'Unknown error', }); throw error; } } private scheduleReconnect(): void { // For persistent connections, never give up - keep trying indefinitely const maxAttempts = this.persistentConnectionEnabled ? Infinity : 10; // Only stop if persistent connections are disabled and max attempts reached if (!this.persistentConnectionEnabled && this.reconnectAttempts >= maxAttempts) { console.error(`Max reconnection attempts reached (${maxAttempts})`); this.notifyStatusUpdate({ connected: false, connecting: false, reconnectAttempts: this.reconnectAttempts, error: `Connection failed after ${maxAttempts} attempts`, }); this.clearConnectionState(); return; } this.reconnectAttempts++; // Use exponential backoff for reconnection delays, but cap at reasonable intervals const baseDelay = this.config.reconnectInterval; const maxDelay = this.persistentConnectionEnabled ? 30000 : 30000; // Cap at 30s for both const exponentialDelay = Math.min( baseDelay * Math.pow(1.5, Math.min(this.reconnectAttempts - 1, 10)), // Slower growth, cap exponential part maxDelay, ); const attemptsDisplay = this.persistentConnectionEnabled ? this.reconnectAttempts : `${this.reconnectAttempts}/${maxAttempts}`; console.log( `Scheduling reconnection attempt ${attemptsDisplay} in ${exponentialDelay}ms (persistent: ${this.persistentConnectionEnabled})`, ); this.notifyStatusUpdate({ connected: false, connecting: false, reconnectAttempts: this.reconnectAttempts, error: `Reconnecting... (attempt ${attemptsDisplay})`, }); setTimeout(() => { // Check if we should still attempt reconnection if (this.reconnectAttempts <= maxAttempts && !this.isConnected()) { console.log(`Executing reconnection attempt ${this.reconnectAttempts}`); this.connect().catch((error) => { console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error); // The error will trigger another reconnection attempt via onclose handler }); } else { console.log('Skipping reconnection attempt - already connected or max attempts reached'); } }, exponentialDelay); } private handleMessage(data: any): void { console.log('🟡 [Chrome Extension] Received message from remote server:', { action: data.action, id: data.id, hasParams: !!data.params, fullMessage: data, }); if (data.id && this.messageHandlers.has(data.id)) { const handler = this.messageHandlers.get(data.id); if (handler) { console.log( '🟡 [Chrome Extension] Handling message with existing handler for ID:', data.id, ); handler(data); this.messageHandlers.delete(data.id); } return; } // Handle different types of messages switch (data.action) { // General tool call handler - routes to the extension's tool system case 'callTool': console.log('🟡 [Chrome Extension] Handling callTool action:', data.params); this.handleToolCall(data); break; // Legacy actions for backward compatibility case 'navigate': this.handleNavigate(data); break; case 'getContent': this.handleGetContent(data); break; case 'click': this.handleClick(data); break; case 'fillInput': this.handleFillInput(data); break; case 'screenshot': this.handleScreenshot(data); break; case 'executeScript': this.handleExecuteScript(data); break; case 'getCurrentTab': this.handleGetCurrentTab(data); break; case 'getAllTabs': this.handleGetAllTabs(data); break; case 'switchTab': this.handleSwitchTab(data); break; case 'createTab': this.handleCreateTab(data); break; case 'closeTab': this.handleCloseTab(data); break; // Browser automation tools matching native server case 'get_windows_and_tabs': this.handleGetWindowsAndTabs(data); break; case 'search_tabs_content': this.handleSearchTabsContent(data); break; case 'chrome_navigate': this.handleChromeNavigate(data); break; case 'chrome_screenshot': this.handleChromeScreenshot(data); break; case 'chrome_close_tabs': this.handleChromeCloseTabs(data); break; case 'chrome_go_back_or_forward': this.handleChromeGoBackOrForward(data); break; case 'chrome_get_web_content': this.handleChromeGetWebContent(data); break; case 'chrome_click_element': this.handleChromeClickElement(data); break; case 'chrome_fill_or_select': this.handleChromeFillOrSelect(data); break; case 'chrome_get_interactive_elements': this.handleChromeGetInteractiveElements(data); break; case 'chrome_network_capture_start': this.handleChromeNetworkCaptureStart(data); break; case 'chrome_network_capture_stop': this.handleChromeNetworkCaptureStop(data); break; case 'chrome_network_request': this.handleChromeNetworkRequest(data); break; case 'chrome_network_debugger_start': this.handleChromeNetworkDebuggerStart(data); break; case 'chrome_network_debugger_stop': this.handleChromeNetworkDebuggerStop(data); break; case 'chrome_keyboard': this.handleChromeKeyboard(data); break; case 'chrome_history': this.handleChromeHistory(data); break; case 'chrome_bookmark_search': this.handleChromeBookmarkSearch(data); break; case 'chrome_bookmark_add': this.handleChromeBookmarkAdd(data); break; case 'chrome_bookmark_delete': this.handleChromeBookmarkDelete(data); break; case 'chrome_inject_script': this.handleChromeInjectScript(data); break; case 'chrome_send_command_to_inject_script': this.handleChromeSendCommandToInjectScript(data); break; case 'chrome_console': this.handleChromeConsole(data); break; default: console.warn('Unknown action:', data.action); this.sendResponse(data.id, { error: 'Unknown action' }); } } private sendResponse(id: string, result: any): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { try { console.log(`📤 [Chrome Extension] Sending response for ID ${id}:`, { messageId: id, hasResult: !!result, isError: result?.isError, result, }); this.ws.send(JSON.stringify({ id, result })); } catch (error) { console.error(`📤 [Chrome Extension] Failed to send response for ID ${id}:`, error); } } else { console.error( `📤 [Chrome Extension] Cannot send response for ID ${id}: WebSocket not open (readyState: ${this.ws?.readyState})`, ); } } private sendError(id: string, error: string): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { try { console.log(`📤 [Chrome Extension] Sending error for ID ${id}:`, error); this.ws.send(JSON.stringify({ id, error })); } catch (sendError) { console.error(`📤 [Chrome Extension] Failed to send error for ID ${id}:`, sendError); } } else { console.error( `📤 [Chrome Extension] Cannot send error for ID ${id}: WebSocket not open (readyState: ${this.ws?.readyState})`, ); } } // Chrome API handlers private async handleNavigate(data: any): Promise { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { await chrome.tabs.update(tabs[0].id, { url: data.params.url }); this.sendResponse(data.id, { success: true, url: data.params.url }); } else { this.sendError(data.id, 'No active tab found'); } } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Navigation failed'); } } private async handleGetContent(data: any): Promise { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { const results = await chrome.scripting.executeScript({ target: { tabId: tabs[0].id! }, func: (selector?: string) => { if (selector) { const element = document.querySelector(selector); return element ? element.textContent : null; } return document.body.textContent; }, args: [data.params.selector], }); this.sendResponse(data.id, { content: results[0].result }); } else { this.sendError(data.id, 'No active tab found'); } } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to get content'); } } private async handleClick(data: any): Promise { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { const results = await chrome.scripting.executeScript({ target: { tabId: tabs[0].id! }, func: (selector: string) => { const element = document.querySelector(selector) as HTMLElement; if (element) { element.click(); return { success: true }; } return { success: false, error: 'Element not found' }; }, args: [data.params.selector], }); this.sendResponse(data.id, results[0].result); } else { this.sendError(data.id, 'No active tab found'); } } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Click failed'); } } private async handleFillInput(data: any): Promise { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { const results = await chrome.scripting.executeScript({ target: { tabId: tabs[0].id! }, func: (selector: string, value: string) => { const element = document.querySelector(selector) as HTMLInputElement; if (element) { element.value = value; element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); return { success: true }; } return { success: false, error: 'Element not found' }; }, args: [data.params.selector, data.params.value], }); this.sendResponse(data.id, results[0].result); } else { this.sendError(data.id, 'No active tab found'); } } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Fill input failed'); } } private async handleScreenshot(data: any): Promise { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { const dataUrl = await chrome.tabs.captureVisibleTab(undefined, { format: 'png', quality: 90, }); this.sendResponse(data.id, { screenshot: dataUrl }); } else { this.sendError(data.id, 'No active tab found'); } } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Screenshot failed'); } } private async handleExecuteScript(data: any): Promise { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { const results = await chrome.scripting.executeScript({ target: { tabId: tabs[0].id! }, func: new Function('return ' + data.params.script)(), }); this.sendResponse(data.id, { result: results[0].result }); } else { this.sendError(data.id, 'No active tab found'); } } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Script execution failed'); } } private async handleGetCurrentTab(data: any): Promise { try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); this.sendResponse(data.id, { tab: tabs[0] || null }); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to get current tab'); } } private async handleGetAllTabs(data: any): Promise { try { const tabs = await chrome.tabs.query({}); this.sendResponse(data.id, { tabs }); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to get all tabs'); } } private async handleSwitchTab(data: any): Promise { try { await chrome.tabs.update(data.params.tabId, { active: true }); this.sendResponse(data.id, { success: true }); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to switch tab'); } } private async handleCreateTab(data: any): Promise { try { const tab = await chrome.tabs.create({ url: data.params.url }); this.sendResponse(data.id, { tab }); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to create tab'); } } private async handleCloseTab(data: any): Promise { try { const tabId = data.params.tabId; if (tabId) { await chrome.tabs.remove(tabId); } else { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs[0]) { await chrome.tabs.remove(tabs[0].id!); } } this.sendResponse(data.id, { success: true }); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to close tab'); } } // Browser automation tool handlers matching native server functionality private async handleGetWindowsAndTabs(data: any): Promise { try { const result = await handleCallTool({ name: 'get_windows_and_tabs', args: data.params }); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to get windows and tabs', ); } } private async handleSearchTabsContent(data: any): Promise { try { // Import handleCallTool directly to avoid message passing issues const { handleCallTool } = await import('../entrypoints/background/tools'); const result = await handleCallTool({ name: 'search_tabs_content', args: data.params, }); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to search tabs content', ); } } private async handleChromeNavigate(data: any): Promise { try { console.log('🔧 [Chrome Extension] handleChromeNavigate called with:', data.params); console.log( '🔧 [Chrome Extension] handleCallTool function available:', typeof handleCallTool, ); const result = await handleCallTool({ name: 'chrome_navigate', args: data.params }); console.log('🔧 [Chrome Extension] handleCallTool result:', result); this.sendResponse(data.id, result); } catch (error) { console.error('🔧 [Chrome Extension] handleChromeNavigate error:', error); this.sendError(data.id, error instanceof Error ? error.message : 'Failed to navigate'); } } private async handleChromeScreenshot(data: any): Promise { try { const result = await handleCallTool({ name: 'chrome_screenshot', args: data.params }); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to take screenshot'); } } private async handleChromeCloseTabs(data: any): Promise { try { const result = await this.callToolHandler('chrome_close_tabs', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to close tabs'); } } private async handleChromeGoBackOrForward(data: any): Promise { try { const result = await this.callToolHandler('chrome_go_back_or_forward', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to go back or forward', ); } } private async handleChromeGetWebContent(data: any): Promise { try { const result = await this.callToolHandler('chrome_get_web_content', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to get web content'); } } private async handleChromeClickElement(data: any): Promise { try { const result = await this.callToolHandler('chrome_click_element', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to click element'); } } private async handleChromeFillOrSelect(data: any): Promise { try { const result = await this.callToolHandler('chrome_fill_or_select', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to fill or select'); } } private async handleChromeGetInteractiveElements(data: any): Promise { try { // Import handleCallTool directly to avoid message passing issues const { handleCallTool } = await import('../entrypoints/background/tools'); const result = await handleCallTool({ name: 'chrome_get_interactive_elements', args: data.params, }); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to get interactive elements', ); } } private async handleChromeNetworkCaptureStart(data: any): Promise { try { const result = await this.callToolHandler('chrome_network_capture_start', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to start network capture', ); } } private async handleChromeNetworkCaptureStop(data: any): Promise { try { const result = await this.callToolHandler('chrome_network_capture_stop', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to stop network capture', ); } } private async handleChromeNetworkRequest(data: any): Promise { try { const result = await this.callToolHandler('chrome_network_request', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to make network request', ); } } private async handleChromeNetworkDebuggerStart(data: any): Promise { try { const result = await this.callToolHandler('chrome_network_debugger_start', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to start network debugger', ); } } private async handleChromeNetworkDebuggerStop(data: any): Promise { try { const result = await this.callToolHandler('chrome_network_debugger_stop', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to stop network debugger', ); } } private async handleChromeKeyboard(data: any): Promise { try { const result = await this.callToolHandler('chrome_keyboard', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to simulate keyboard', ); } } private async handleChromeHistory(data: any): Promise { try { const result = await this.callToolHandler('chrome_history', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to get history'); } } private async handleChromeBookmarkSearch(data: any): Promise { try { const result = await this.callToolHandler('chrome_bookmark_search', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to search bookmarks', ); } } private async handleChromeBookmarkAdd(data: any): Promise { try { const result = await this.callToolHandler('chrome_bookmark_add', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to add bookmark'); } } private async handleChromeBookmarkDelete(data: any): Promise { try { const result = await this.callToolHandler('chrome_bookmark_delete', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to delete bookmark'); } } private async handleChromeInjectScript(data: any): Promise { try { const result = await this.callToolHandler('chrome_inject_script', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to inject script'); } } private async handleChromeSendCommandToInjectScript(data: any): Promise { try { const result = await this.callToolHandler( 'chrome_send_command_to_inject_script', data.params, ); this.sendResponse(data.id, result); } catch (error) { this.sendError( data.id, error instanceof Error ? error.message : 'Failed to send command to inject script', ); } } private async handleChromeConsole(data: any): Promise { try { const result = await this.callToolHandler('chrome_console', data.params); this.sendResponse(data.id, result); } catch (error) { this.sendError(data.id, error instanceof Error ? error.message : 'Failed to get console'); } } // Helper method to call the extension's tool handler directly (avoiding message passing issues) private async callToolHandler(toolName: string, params: any): Promise { try { // Import handleCallTool directly to avoid message passing issues const { handleCallTool } = await import('../entrypoints/background/tools'); return await handleCallTool({ name: toolName, args: params, }); } catch (error) { throw new Error(error instanceof Error ? error.message : 'Tool execution failed'); } } // Handle general tool calls from remote server private async handleToolCall(data: any): Promise { try { console.log('🔧 [Chrome Extension] Handling tool call:', { toolName: data.params?.name, hasArgs: !!(data.params?.arguments || data.params?.args), messageId: data.id, fullParams: data.params, }); const result = await handleCallTool({ name: data.params.name, args: data.params.arguments || data.params.args, }); console.log('🔧 [Chrome Extension] Tool call completed, sending response:', { messageId: data.id, hasResult: !!result, isError: result?.isError, result, }); this.sendResponse(data.id, result); } catch (error) { console.error('🔧 [Chrome Extension] Tool call failed:', error); this.sendError(data.id, error instanceof Error ? error.message : 'Tool execution failed'); } } disconnect(): void { console.log('Disconnecting from remote server...'); // Stop any ongoing connection attempts this.isConnecting = false; // Stop keep-alive mechanism this.clearKeepAlive(); if (this.ws) { const currentState = this.ws.readyState; console.log(`Closing WebSocket connection (current state: ${currentState})`); // Only close if not already closed/closing if (currentState === WebSocket.OPEN || currentState === WebSocket.CONNECTING) { try { this.ws.close(1000, 'User initiated disconnect'); // Normal closure } catch (error) { console.error('Error closing WebSocket:', error); } } this.ws = null; } this.connectionTime = null; this.reconnectAttempts = 0; this.notifyStatusUpdate({ connected: false, connecting: false, connectionTime: null, reconnectAttempts: 0, error: undefined, // Clear any previous errors }); // Clear saved connection state since this is a manual disconnect this.clearConnectionState(); console.log('Successfully disconnected from remote server'); } isConnected(): boolean { const connected = this.ws !== null && this.ws.readyState === WebSocket.OPEN; // Only log if there's a state change or for debugging if (this.ws) { const stateNames = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; const stateName = stateNames[this.ws.readyState] || 'UNKNOWN'; console.log(`RemoteServerClient.isConnected(): ${connected} (state: ${stateName})`); } return connected; } /** * Enable or disable persistent connection behavior */ setPersistentConnection(enabled: boolean): void { this.persistentConnectionEnabled = enabled; console.log(`RemoteServerClient: Persistent connection ${enabled ? 'enabled' : 'disabled'}`); if (!enabled) { // Clear saved state if disabling persistence this.clearConnectionState(); } } /** * Get current persistent connection setting */ isPersistentConnectionEnabled(): boolean { return this.persistentConnectionEnabled; } }