Files
broswer-automation/app/chrome-extension/utils/remote-server-client.ts

1284 lines
41 KiB
TypeScript

/**
* 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<string, (data: any) => 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<RemoteServerConfig> = {}) {
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<void> {
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<any> {
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<boolean> {
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<void> {
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<string> {
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<string | null> {
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<void> {
// 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<void>((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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<any> {
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<void> {
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;
}
}