From dce3a8a53edf6bfa9af1cbe28dbc8cda806c060f Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Tue, 25 Mar 2025 12:31:13 -0400 Subject: [PATCH 1/3] refactor: merge SessionTracker and SessionManager, convert BrowserDetector to functional approach --- packages/agent/src/index.ts | 3 +- .../agent/src/tools/session/SessionTracker.ts | 613 +++++++++++++++++- .../tools/session/lib/BrowserAutomation.ts | 36 - .../src/tools/session/lib/BrowserDetector.ts | 257 -------- .../src/tools/session/lib/SessionManager.ts | 290 --------- .../tools/session/lib/browser-manager.test.ts | 64 +- .../tools/session/lib/element-state.test.ts | 8 +- .../session/lib/form-interaction.test.ts | 8 +- .../src/tools/session/lib/navigation.test.ts | 8 +- .../tools/session/lib/wait-behavior.test.ts | 8 +- .../agent/src/tools/session/sessionMessage.ts | 293 +++++---- .../agent/src/tools/session/sessionStart.ts | 42 +- 12 files changed, 838 insertions(+), 792 deletions(-) delete mode 100644 packages/agent/src/tools/session/lib/BrowserAutomation.ts delete mode 100644 packages/agent/src/tools/session/lib/BrowserDetector.ts delete mode 100644 packages/agent/src/tools/session/lib/SessionManager.ts diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 6c8b016..2d84ff2 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -12,14 +12,13 @@ export * from './tools/shell/listShells.js'; export * from './tools/shell/ShellTracker.js'; // Tools - Browser -export * from './tools/session/lib/SessionManager.js'; export * from './tools/session/lib/types.js'; export * from './tools/session/sessionMessage.js'; export * from './tools/session/sessionStart.js'; export * from './tools/session/lib/PageController.js'; -export * from './tools/session/lib/BrowserAutomation.js'; export * from './tools/session/listSessions.js'; export * from './tools/session/SessionTracker.js'; +// Export browser detector functions export * from './tools/agent/AgentTracker.js'; // Tools - Interaction diff --git a/packages/agent/src/tools/session/SessionTracker.ts b/packages/agent/src/tools/session/SessionTracker.ts index 2b4fa92..f0871e7 100644 --- a/packages/agent/src/tools/session/SessionTracker.ts +++ b/packages/agent/src/tools/session/SessionTracker.ts @@ -1,7 +1,253 @@ +// Import browser detection functions directly +import { execSync } from 'child_process'; +import fs from 'fs'; +import { homedir } from 'os'; +import path from 'path'; + +import { chromium, firefox, webkit } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; -import { SessionManager } from './lib/SessionManager.js'; -import { browserSessions } from './lib/types.js'; +import { Logger } from '../../utils/logger.js'; + +// Browser info interface +interface BrowserInfo { + name: string; + type: 'chromium' | 'firefox' | 'webkit'; + path: string; +} + +// Browser detection functions +function canAccess(filePath: string): boolean { + try { + fs.accessSync(filePath); + return true; + } catch { + return false; + } +} + +async function detectMacOSBrowsers(): Promise { + const browsers: BrowserInfo[] = []; + + // Chrome paths + const chromePaths = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + `${homedir()}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, + `${homedir()}/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary`, + ]; + + // Edge paths + const edgePaths = [ + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', + `${homedir()}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`, + ]; + + // Firefox paths + const firefoxPaths = [ + '/Applications/Firefox.app/Contents/MacOS/firefox', + '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', + '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', + `${homedir()}/Applications/Firefox.app/Contents/MacOS/firefox`, + ]; + + // Check Chrome paths + for (const chromePath of chromePaths) { + if (canAccess(chromePath)) { + browsers.push({ + name: 'Chrome', + type: 'chromium', + path: chromePath, + }); + } + } + + // Check Edge paths + for (const edgePath of edgePaths) { + if (canAccess(edgePath)) { + browsers.push({ + name: 'Edge', + type: 'chromium', // Edge is Chromium-based + path: edgePath, + }); + } + } + + // Check Firefox paths + for (const firefoxPath of firefoxPaths) { + if (canAccess(firefoxPath)) { + browsers.push({ + name: 'Firefox', + type: 'firefox', + path: firefoxPath, + }); + } + } + + return browsers; +} + +async function detectWindowsBrowsers(): Promise { + const browsers: BrowserInfo[] = []; + + // Common installation paths for Chrome + const chromePaths = [ + path.join( + process.env.LOCALAPPDATA || '', + 'Google/Chrome/Application/chrome.exe', + ), + path.join( + process.env.PROGRAMFILES || '', + 'Google/Chrome/Application/chrome.exe', + ), + path.join( + process.env['PROGRAMFILES(X86)'] || '', + 'Google/Chrome/Application/chrome.exe', + ), + ]; + + // Common installation paths for Edge + const edgePaths = [ + path.join( + process.env.LOCALAPPDATA || '', + 'Microsoft/Edge/Application/msedge.exe', + ), + path.join( + process.env.PROGRAMFILES || '', + 'Microsoft/Edge/Application/msedge.exe', + ), + path.join( + process.env['PROGRAMFILES(X86)'] || '', + 'Microsoft/Edge/Application/msedge.exe', + ), + ]; + + // Common installation paths for Firefox + const firefoxPaths = [ + path.join(process.env.PROGRAMFILES || '', 'Mozilla Firefox/firefox.exe'), + path.join( + process.env['PROGRAMFILES(X86)'] || '', + 'Mozilla Firefox/firefox.exe', + ), + ]; + + // Check Chrome paths + for (const chromePath of chromePaths) { + if (canAccess(chromePath)) { + browsers.push({ + name: 'Chrome', + type: 'chromium', + path: chromePath, + }); + } + } + + // Check Edge paths + for (const edgePath of edgePaths) { + if (canAccess(edgePath)) { + browsers.push({ + name: 'Edge', + type: 'chromium', // Edge is Chromium-based + path: edgePath, + }); + } + } + + // Check Firefox paths + for (const firefoxPath of firefoxPaths) { + if (canAccess(firefoxPath)) { + browsers.push({ + name: 'Firefox', + type: 'firefox', + path: firefoxPath, + }); + } + } + + return browsers; +} + +async function detectLinuxBrowsers(): Promise { + const browsers: BrowserInfo[] = []; + + // Try to find Chrome/Chromium using the 'which' command + const chromiumExecutables = [ + 'google-chrome-stable', + 'google-chrome', + 'chromium-browser', + 'chromium', + ]; + + // Try to find Firefox using the 'which' command + const firefoxExecutables = ['firefox']; + + // Check for Chrome/Chromium + for (const executable of chromiumExecutables) { + try { + const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) + .toString() + .trim(); + if (canAccess(browserPath)) { + browsers.push({ + name: executable, + type: 'chromium', + path: browserPath, + }); + } + } catch { + // Not installed + } + } + + // Check for Firefox + for (const executable of firefoxExecutables) { + try { + const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) + .toString() + .trim(); + if (canAccess(browserPath)) { + browsers.push({ + name: 'Firefox', + type: 'firefox', + path: browserPath, + }); + } + } catch { + // Not installed + } + } + + return browsers; +} + +async function detectBrowsers(): Promise { + const platform = process.platform; + let browsers: BrowserInfo[] = []; + + switch (platform) { + case 'darwin': + browsers = await detectMacOSBrowsers(); + break; + case 'win32': + browsers = await detectWindowsBrowsers(); + break; + case 'linux': + browsers = await detectLinuxBrowsers(); + break; + default: + console.log(`Unsupported platform: ${platform}`); + break; + } + + return browsers; +} +import { + BrowserConfig, + Session, + BrowserError, + BrowserErrorCode, + browserSessions, +} from './lib/types.js'; // Status of a browser session export enum SessionStatus { @@ -27,12 +273,79 @@ export interface SessionInfo { } /** - * Registry to keep track of browser sessions + * Creates, manages, and tracks browser sessions */ export class SessionTracker { + // Map to track session info for reporting private sessions: Map = new Map(); + // Map to track actual browser sessions + private browserSessions: Map = new Map(); + private readonly defaultConfig: BrowserConfig = { + headless: true, + defaultTimeout: 30000, + useSystemBrowsers: true, + preferredType: 'chromium', + }; + private detectedBrowsers: Array<{ + name: string; + type: 'chromium' | 'firefox' | 'webkit'; + path: string; + }> = []; + private browserDetectionPromise: Promise | null = null; - constructor(public ownerAgentId: string | undefined) {} + constructor( + public ownerAgentId: string | undefined, + private logger?: Logger, + ) { + // Store a reference to the instance globally for cleanup + // This allows the CLI to access the instance for cleanup + (globalThis as any).__BROWSER_MANAGER__ = this; + + // Set up cleanup handlers for graceful shutdown + this.setupGlobalCleanup(); + + // Start browser detection in the background if logger is provided + if (this.logger) { + this.browserDetectionPromise = this.detectBrowsers(); + } + } + + /** + * Detect available browsers on the system + */ + private async detectBrowsers(): Promise { + if (!this.logger) { + this.detectedBrowsers = []; + return; + } + + try { + this.detectedBrowsers = await detectBrowsers(); + if (this.logger) { + this.logger.info( + `Detected ${this.detectedBrowsers.length} browsers on the system`, + ); + } + if (this.detectedBrowsers.length > 0 && this.logger) { + this.logger.info('Available browsers:'); + this.detectedBrowsers.forEach((browser) => { + if (this.logger) { + this.logger.info( + `- ${browser.name} (${browser.type}) at ${browser.path}`, + ); + } + }); + } + } catch (error) { + if (this.logger) { + this.logger.error( + 'Failed to detect system browsers, disabling browser session tools:', + error, + ); + } + this.detectedBrowsers = []; + } + } // Register a new browser session public registerBrowser(url?: string): string { @@ -77,12 +390,12 @@ export class SessionTracker { return true; } - // Get all browser sessions + // Get all browser sessions info public getSessions(): SessionInfo[] { return Array.from(this.sessions.values()); } - // Get a specific browser session by ID + // Get a specific browser session info by ID public getSessionById(id: string): SessionInfo | undefined { return this.sessions.get(id); } @@ -93,48 +406,276 @@ export class SessionTracker { } /** - * Cleans up all browser sessions associated with this tracker - * @returns A promise that resolves when cleanup is complete + * Create a new browser session */ - public async cleanup(): Promise { - const sessions = this.getSessionsByStatus(SessionStatus.RUNNING); + public async createSession(config?: BrowserConfig): Promise { + try { + // Wait for browser detection to complete if it's still running + if (this.browserDetectionPromise) { + await this.browserDetectionPromise; + this.browserDetectionPromise = null; + } - // Create cleanup promises for each session - const cleanupPromises = sessions.map((session) => - this.cleanupSession(session), - ); + const sessionConfig = { ...this.defaultConfig, ...config }; + + // Determine if we should try to use system browsers + const useSystemBrowsers = sessionConfig.useSystemBrowsers !== false; + + // If a specific executable path is provided, use that + if (sessionConfig.executablePath) { + console.log( + `Using specified browser executable: ${sessionConfig.executablePath}`, + ); + return this.launchWithExecutablePath( + sessionConfig.executablePath, + sessionConfig.preferredType || 'chromium', + sessionConfig, + ); + } - // Wait for all cleanup operations to complete in parallel - await Promise.all(cleanupPromises); + // Try to use a system browser if enabled and any were detected + if (useSystemBrowsers && this.detectedBrowsers.length > 0) { + const preferredType = sessionConfig.preferredType || 'chromium'; + + // First try to find a browser of the preferred type + let browserInfo = this.detectedBrowsers.find( + (b) => b.type === preferredType, + ); + + // If no preferred browser type found, use any available browser + if (!browserInfo) { + browserInfo = this.detectedBrowsers[0]; + } + + if (browserInfo) { + console.log( + `Using system browser: ${browserInfo.name} (${browserInfo.type}) at ${browserInfo.path}`, + ); + return this.launchWithExecutablePath( + browserInfo.path, + browserInfo.type, + sessionConfig, + ); + } + } + + // Fall back to Playwright's bundled browser + console.log('Using Playwright bundled browser'); + const browser = await chromium.launch({ + headless: sessionConfig.headless, + }); + + // Create a new context (equivalent to incognito) + const context = await browser.newContext({ + viewport: null, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + }); + + const page = await context.newPage(); + page.setDefaultTimeout(sessionConfig.defaultTimeout ?? 30000); + + const session: Session = { + browser, + page, + id: uuidv4(), + }; + + this.browserSessions.set(session.id, session); + // Also store in global browserSessions for compatibility + browserSessions.set(session.id, session); + + this.setupCleanup(session); + + return session; + } catch (error) { + throw new BrowserError( + 'Failed to create browser session', + BrowserErrorCode.LAUNCH_FAILED, + error, + ); + } } /** - * Cleans up a browser session - * @param session The browser session to clean up + * Launch a browser with a specific executable path */ - private async cleanupSession(session: SessionInfo): Promise { + private async launchWithExecutablePath( + executablePath: string, + browserType: 'chromium' | 'firefox' | 'webkit', + config: BrowserConfig, + ): Promise { + let browser; + + // Launch the browser using the detected executable path + switch (browserType) { + case 'chromium': + browser = await chromium.launch({ + headless: config.headless, + executablePath: executablePath, + }); + break; + case 'firefox': + browser = await firefox.launch({ + headless: config.headless, + executablePath: executablePath, + }); + break; + case 'webkit': + browser = await webkit.launch({ + headless: config.headless, + executablePath: executablePath, + }); + break; + default: + throw new BrowserError( + `Unsupported browser type: ${browserType}`, + BrowserErrorCode.LAUNCH_FAILED, + ); + } + + // Create a new context (equivalent to incognito) + const context = await browser.newContext({ + viewport: null, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + }); + + const page = await context.newPage(); + page.setDefaultTimeout(config.defaultTimeout ?? 30000); + + const session: Session = { + browser, + page, + id: uuidv4(), + }; + + this.browserSessions.set(session.id, session); + // Also store in global browserSessions for compatibility + browserSessions.set(session.id, session); + + this.setupCleanup(session); + + return session; + } + + /** + * Get a browser session by ID + */ + public getSession(sessionId: string): Session { + const session = this.browserSessions.get(sessionId); + if (!session) { + throw new BrowserError( + 'Session not found', + BrowserErrorCode.SESSION_ERROR, + ); + } + return session; + } + + /** + * Close a specific browser session + */ + public async closeSession(sessionId: string): Promise { + const session = this.browserSessions.get(sessionId); + if (!session) { + throw new BrowserError( + 'Session not found', + BrowserErrorCode.SESSION_ERROR, + ); + } + try { - const browserManager = ( - globalThis as unknown as { __BROWSER_MANAGER__?: SessionManager } - ).__BROWSER_MANAGER__; - - if (browserManager) { - await browserManager.closeSession(session.id); - } else { - // Fallback to closing via browserSessions if SessionManager is not available - const browserSession = browserSessions.get(session.id); - if (browserSession) { - await browserSession.page.context().close(); - await browserSession.browser.close(); - browserSessions.delete(session.id); - } - } + // In Playwright, we should close the context which will automatically close its pages + await session.page.context().close(); + await session.browser.close(); + + // Remove from both maps + this.browserSessions.delete(sessionId); + browserSessions.delete(sessionId); - this.updateSessionStatus(session.id, SessionStatus.COMPLETED); + // Update status + this.updateSessionStatus(sessionId, SessionStatus.COMPLETED, { + closedExplicitly: true, + }); } catch (error) { - this.updateSessionStatus(session.id, SessionStatus.ERROR, { + this.updateSessionStatus(sessionId, SessionStatus.ERROR, { error: error instanceof Error ? error.message : String(error), }); + + throw new BrowserError( + 'Failed to close session', + BrowserErrorCode.SESSION_ERROR, + error, + ); } } + + /** + * Cleans up all browser sessions associated with this tracker + */ + public async cleanup(): Promise { + await this.closeAllSessions(); + } + + /** + * Close all browser sessions + */ + public async closeAllSessions(): Promise { + const closePromises = Array.from(this.browserSessions.keys()).map( + (sessionId) => this.closeSession(sessionId).catch(() => {}), + ); + await Promise.all(closePromises); + } + + private setupCleanup(session: Session): void { + // Handle browser disconnection + session.browser.on('disconnected', () => { + this.browserSessions.delete(session.id); + browserSessions.delete(session.id); + + // Update session status + this.updateSessionStatus(session.id, SessionStatus.TERMINATED); + }); + } + + /** + * Sets up global cleanup handlers for all browser sessions + */ + private setupGlobalCleanup(): void { + // Use beforeExit for async cleanup + process.on('beforeExit', () => { + this.closeAllSessions().catch((err) => { + console.error('Error closing browser sessions:', err); + }); + }); + + // Use exit for synchronous cleanup (as a fallback) + process.on('exit', () => { + // Can only do synchronous operations here + for (const session of this.browserSessions.values()) { + try { + // Attempt synchronous close - may not fully work + session.browser.close(); + } catch { + // Ignore errors during exit + } + } + }); + + // Handle SIGINT (Ctrl+C) + process.on('SIGINT', () => { + this.closeAllSessions() + .catch(() => { + return false; + }) + .finally(() => { + // Give a moment for cleanup to complete + setTimeout(() => process.exit(0), 500); + }) + .catch(() => { + // Additional catch for any unexpected errors in the finally block + }); + }); + } } diff --git a/packages/agent/src/tools/session/lib/BrowserAutomation.ts b/packages/agent/src/tools/session/lib/BrowserAutomation.ts deleted file mode 100644 index f3794aa..0000000 --- a/packages/agent/src/tools/session/lib/BrowserAutomation.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PageController } from './PageController.js'; -import { SessionManager } from './SessionManager.js'; - -export class BrowserAutomation { - private static instance: BrowserAutomation; - private browserManager: SessionManager; - - private constructor() { - this.browserManager = new SessionManager(); - } - - static getInstance(): BrowserAutomation { - if (!BrowserAutomation.instance) { - BrowserAutomation.instance = new BrowserAutomation(); - } - return BrowserAutomation.instance; - } - - async createSession(headless: boolean = true) { - const session = await this.browserManager.createSession({ headless }); - const pageController = new PageController(session.page); - - return { - sessionId: session.id, - pageController, - close: () => this.browserManager.closeSession(session.id), - }; - } - - async cleanup() { - await this.browserManager.closeAllSessions(); - } -} - -// Export singleton instance -export const browserAutomation = BrowserAutomation.getInstance(); diff --git a/packages/agent/src/tools/session/lib/BrowserDetector.ts b/packages/agent/src/tools/session/lib/BrowserDetector.ts deleted file mode 100644 index 59f4bdd..0000000 --- a/packages/agent/src/tools/session/lib/BrowserDetector.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { execSync } from 'child_process'; -import fs from 'fs'; -import { homedir } from 'os'; -import path from 'path'; - -export interface BrowserInfo { - name: string; - type: 'chromium' | 'firefox' | 'webkit'; - path: string; -} - -/** - * Utility class to detect system-installed browsers across platforms - */ -export class BrowserDetector { - /** - * Detect available browsers on the system - * Returns an array of browser information objects sorted by preference - */ - static async detectBrowsers(): Promise { - const platform = process.platform; - - let browsers: BrowserInfo[] = []; - - switch (platform) { - case 'darwin': - browsers = await this.detectMacOSBrowsers(); - break; - case 'win32': - browsers = await this.detectWindowsBrowsers(); - break; - case 'linux': - browsers = await this.detectLinuxBrowsers(); - break; - default: - console.log(`Unsupported platform: ${platform}`); - break; - } - - return browsers; - } - - /** - * Detect browsers on macOS - */ - private static async detectMacOSBrowsers(): Promise { - const browsers: BrowserInfo[] = []; - - // Chrome paths - const chromePaths = [ - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', - `${homedir()}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, - `${homedir()}/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary`, - ]; - - // Edge paths - const edgePaths = [ - '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', - `${homedir()}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`, - ]; - - // Firefox paths - const firefoxPaths = [ - '/Applications/Firefox.app/Contents/MacOS/firefox', - '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', - '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', - `${homedir()}/Applications/Firefox.app/Contents/MacOS/firefox`, - ]; - - // Check Chrome paths - for (const chromePath of chromePaths) { - if (this.canAccess(chromePath)) { - browsers.push({ - name: 'Chrome', - type: 'chromium', - path: chromePath, - }); - } - } - - // Check Edge paths - for (const edgePath of edgePaths) { - if (this.canAccess(edgePath)) { - browsers.push({ - name: 'Edge', - type: 'chromium', // Edge is Chromium-based - path: edgePath, - }); - } - } - - // Check Firefox paths - for (const firefoxPath of firefoxPaths) { - if (this.canAccess(firefoxPath)) { - browsers.push({ - name: 'Firefox', - type: 'firefox', - path: firefoxPath, - }); - } - } - - return browsers; - } - - /** - * Detect browsers on Windows - */ - private static async detectWindowsBrowsers(): Promise { - const browsers: BrowserInfo[] = []; - - // Common installation paths for Chrome - const chromePaths = [ - path.join( - process.env.LOCALAPPDATA || '', - 'Google/Chrome/Application/chrome.exe', - ), - path.join( - process.env.PROGRAMFILES || '', - 'Google/Chrome/Application/chrome.exe', - ), - path.join( - process.env['PROGRAMFILES(X86)'] || '', - 'Google/Chrome/Application/chrome.exe', - ), - ]; - - // Common installation paths for Edge - const edgePaths = [ - path.join( - process.env.LOCALAPPDATA || '', - 'Microsoft/Edge/Application/msedge.exe', - ), - path.join( - process.env.PROGRAMFILES || '', - 'Microsoft/Edge/Application/msedge.exe', - ), - path.join( - process.env['PROGRAMFILES(X86)'] || '', - 'Microsoft/Edge/Application/msedge.exe', - ), - ]; - - // Common installation paths for Firefox - const firefoxPaths = [ - path.join(process.env.PROGRAMFILES || '', 'Mozilla Firefox/firefox.exe'), - path.join( - process.env['PROGRAMFILES(X86)'] || '', - 'Mozilla Firefox/firefox.exe', - ), - ]; - - // Check Chrome paths - for (const chromePath of chromePaths) { - if (this.canAccess(chromePath)) { - browsers.push({ - name: 'Chrome', - type: 'chromium', - path: chromePath, - }); - } - } - - // Check Edge paths - for (const edgePath of edgePaths) { - if (this.canAccess(edgePath)) { - browsers.push({ - name: 'Edge', - type: 'chromium', // Edge is Chromium-based - path: edgePath, - }); - } - } - - // Check Firefox paths - for (const firefoxPath of firefoxPaths) { - if (this.canAccess(firefoxPath)) { - browsers.push({ - name: 'Firefox', - type: 'firefox', - path: firefoxPath, - }); - } - } - - return browsers; - } - - /** - * Detect browsers on Linux - */ - private static async detectLinuxBrowsers(): Promise { - const browsers: BrowserInfo[] = []; - - // Try to find Chrome/Chromium using the 'which' command - const chromiumExecutables = [ - 'google-chrome-stable', - 'google-chrome', - 'chromium-browser', - 'chromium', - ]; - - // Try to find Firefox using the 'which' command - const firefoxExecutables = ['firefox']; - - // Check for Chrome/Chromium - for (const executable of chromiumExecutables) { - try { - const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) - .toString() - .trim(); - if (this.canAccess(browserPath)) { - browsers.push({ - name: executable, - type: 'chromium', - path: browserPath, - }); - } - } catch { - // Not installed - } - } - - // Check for Firefox - for (const executable of firefoxExecutables) { - try { - const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) - .toString() - .trim(); - if (this.canAccess(browserPath)) { - browsers.push({ - name: 'Firefox', - type: 'firefox', - path: browserPath, - }); - } - } catch { - // Not installed - } - } - - return browsers; - } - - /** - * Check if a file exists and is accessible - */ - private static canAccess(filePath: string): boolean { - try { - fs.accessSync(filePath); - return true; - } catch { - return false; - } - } -} diff --git a/packages/agent/src/tools/session/lib/SessionManager.ts b/packages/agent/src/tools/session/lib/SessionManager.ts deleted file mode 100644 index 4500c2b..0000000 --- a/packages/agent/src/tools/session/lib/SessionManager.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { chromium, firefox, webkit } from '@playwright/test'; -import { v4 as uuidv4 } from 'uuid'; - -import { BrowserDetector, BrowserInfo } from './BrowserDetector.js'; -import { - BrowserConfig, - Session, - BrowserError, - BrowserErrorCode, -} from './types.js'; - -export class SessionManager { - private sessions: Map = new Map(); - private readonly defaultConfig: BrowserConfig = { - headless: true, - defaultTimeout: 30000, - useSystemBrowsers: true, - preferredType: 'chromium', - }; - private detectedBrowsers: BrowserInfo[] = []; - private browserDetectionPromise: Promise | null = null; - - constructor() { - // Store a reference to the instance globally for cleanup - // This allows the CLI to access the instance for cleanup - (globalThis as any).__BROWSER_MANAGER__ = this; - - // Set up cleanup handlers for graceful shutdown - this.setupGlobalCleanup(); - - // Start browser detection in the background - this.browserDetectionPromise = this.detectBrowsers(); - } - - /** - * Detect available browsers on the system - */ - private async detectBrowsers(): Promise { - try { - this.detectedBrowsers = await BrowserDetector.detectBrowsers(); - console.log( - `Detected ${this.detectedBrowsers.length} browsers on the system`, - ); - if (this.detectedBrowsers.length > 0) { - console.log('Available browsers:'); - this.detectedBrowsers.forEach((browser) => { - console.log(`- ${browser.name} (${browser.type}) at ${browser.path}`); - }); - } - } catch (error) { - console.error('Failed to detect system browsers:', error); - this.detectedBrowsers = []; - } - } - - async createSession(config?: BrowserConfig): Promise { - try { - // Wait for browser detection to complete if it's still running - if (this.browserDetectionPromise) { - await this.browserDetectionPromise; - this.browserDetectionPromise = null; - } - - const sessionConfig = { ...this.defaultConfig, ...config }; - - // Determine if we should try to use system browsers - const useSystemBrowsers = sessionConfig.useSystemBrowsers !== false; - - // If a specific executable path is provided, use that - if (sessionConfig.executablePath) { - console.log( - `Using specified browser executable: ${sessionConfig.executablePath}`, - ); - return this.launchWithExecutablePath( - sessionConfig.executablePath, - sessionConfig.preferredType || 'chromium', - sessionConfig, - ); - } - - // Try to use a system browser if enabled and any were detected - if (useSystemBrowsers && this.detectedBrowsers.length > 0) { - const preferredType = sessionConfig.preferredType || 'chromium'; - - // First try to find a browser of the preferred type - let browserInfo = this.detectedBrowsers.find( - (b) => b.type === preferredType, - ); - - // If no preferred browser type found, use any available browser - if (!browserInfo) { - browserInfo = this.detectedBrowsers[0]; - } - - if (browserInfo) { - console.log( - `Using system browser: ${browserInfo.name} (${browserInfo.type}) at ${browserInfo.path}`, - ); - return this.launchWithExecutablePath( - browserInfo.path, - browserInfo.type, - sessionConfig, - ); - } - } - - // Fall back to Playwright's bundled browser - console.log('Using Playwright bundled browser'); - const browser = await chromium.launch({ - headless: sessionConfig.headless, - }); - - // Create a new context (equivalent to incognito) - const context = await browser.newContext({ - viewport: null, - userAgent: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - }); - - const page = await context.newPage(); - page.setDefaultTimeout(sessionConfig.defaultTimeout ?? 30000); - - const session: Session = { - browser, - page, - id: uuidv4(), - }; - - this.sessions.set(session.id, session); - this.setupCleanup(session); - - return session; - } catch (error) { - throw new BrowserError( - 'Failed to create browser session', - BrowserErrorCode.LAUNCH_FAILED, - error, - ); - } - } - - /** - * Launch a browser with a specific executable path - */ - private async launchWithExecutablePath( - executablePath: string, - browserType: 'chromium' | 'firefox' | 'webkit', - config: BrowserConfig, - ): Promise { - let browser; - - // Launch the browser using the detected executable path - switch (browserType) { - case 'chromium': - browser = await chromium.launch({ - headless: config.headless, - executablePath: executablePath, - }); - break; - case 'firefox': - browser = await firefox.launch({ - headless: config.headless, - executablePath: executablePath, - }); - break; - case 'webkit': - browser = await webkit.launch({ - headless: config.headless, - executablePath: executablePath, - }); - break; - default: - throw new BrowserError( - `Unsupported browser type: ${browserType}`, - BrowserErrorCode.LAUNCH_FAILED, - ); - } - - // Create a new context (equivalent to incognito) - const context = await browser.newContext({ - viewport: null, - userAgent: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - }); - - const page = await context.newPage(); - page.setDefaultTimeout(config.defaultTimeout ?? 30000); - - const session: Session = { - browser, - page, - id: uuidv4(), - }; - - this.sessions.set(session.id, session); - this.setupCleanup(session); - - return session; - } - - async closeSession(sessionId: string): Promise { - const session = this.sessions.get(sessionId); - if (!session) { - throw new BrowserError( - 'Session not found', - BrowserErrorCode.SESSION_ERROR, - ); - } - - try { - // In Playwright, we should close the context which will automatically close its pages - await session.page.context().close(); - await session.browser.close(); - this.sessions.delete(sessionId); - } catch (error) { - throw new BrowserError( - 'Failed to close session', - BrowserErrorCode.SESSION_ERROR, - error, - ); - } - } - - private setupCleanup(session: Session): void { - // Handle browser disconnection - session.browser.on('disconnected', () => { - this.sessions.delete(session.id); - }); - - // No need to add individual process handlers for each session - // We'll handle all sessions in the global cleanup - } - - /** - * Sets up global cleanup handlers for all browser sessions - */ - private setupGlobalCleanup(): void { - // Use beforeExit for async cleanup - process.on('beforeExit', () => { - this.closeAllSessions().catch((err) => { - console.error('Error closing browser sessions:', err); - }); - }); - - // Use exit for synchronous cleanup (as a fallback) - process.on('exit', () => { - // Can only do synchronous operations here - for (const session of this.sessions.values()) { - try { - // Attempt synchronous close - may not fully work - session.browser.close(); - // eslint-disable-next-line unused-imports/no-unused-vars - } catch (e) { - // Ignore errors during exit - } - } - }); - - // Handle SIGINT (Ctrl+C) - process.on('SIGINT', () => { - // eslint-disable-next-line promise/catch-or-return - this.closeAllSessions() - .catch(() => { - return false; - }) - .finally(() => { - // Give a moment for cleanup to complete - setTimeout(() => process.exit(0), 500); - }); - }); - } - - async closeAllSessions(): Promise { - const closePromises = Array.from(this.sessions.keys()).map((sessionId) => - this.closeSession(sessionId).catch(() => {}), - ); - await Promise.all(closePromises); - } - - getSession(sessionId: string): Session { - const session = this.sessions.get(sessionId); - if (!session) { - throw new BrowserError( - 'Session not found', - BrowserErrorCode.SESSION_ERROR, - ); - } - return session; - } -} diff --git a/packages/agent/src/tools/session/lib/browser-manager.test.ts b/packages/agent/src/tools/session/lib/browser-manager.test.ts index f89de0b..601e8e5 100644 --- a/packages/agent/src/tools/session/lib/browser-manager.test.ts +++ b/packages/agent/src/tools/session/lib/browser-manager.test.ts @@ -1,35 +1,38 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { SessionManager } from './SessionManager.js'; +import { MockLogger } from '../../../utils/mockLogger.js'; +import { SessionTracker, SessionStatus } from '../SessionTracker.js'; + import { BrowserError, BrowserErrorCode } from './types.js'; -describe('SessionManager', () => { - let browserManager: SessionManager; +describe('SessionTracker', () => { + let browserTracker: SessionTracker; + const mockLogger = new MockLogger(); beforeEach(() => { - browserManager = new SessionManager(); + browserTracker = new SessionTracker('test-agent', mockLogger); }); afterEach(async () => { - await browserManager.closeAllSessions(); + await browserTracker.closeAllSessions(); }); describe('createSession', () => { it('should create a new browser session', async () => { - const session = await browserManager.createSession(); + const session = await browserTracker.createSession(); expect(session.id).toBeDefined(); expect(session.browser).toBeDefined(); expect(session.page).toBeDefined(); }); it('should create a headless session when specified', async () => { - const session = await browserManager.createSession({ headless: true }); + const session = await browserTracker.createSession({ headless: true }); expect(session.id).toBeDefined(); }); it('should apply custom timeout when specified', async () => { const customTimeout = 500; - const session = await browserManager.createSession({ + const session = await browserTracker.createSession({ defaultTimeout: customTimeout, }); // Verify timeout by attempting to wait for a non-existent element @@ -46,16 +49,16 @@ describe('SessionManager', () => { describe('closeSession', () => { it('should close an existing session', async () => { - const session = await browserManager.createSession(); - await browserManager.closeSession(session.id); + const session = await browserTracker.createSession(); + await browserTracker.closeSession(session.id); expect(() => { - browserManager.getSession(session.id); + browserTracker.getSession(session.id); }).toThrow(BrowserError); }); it('should throw error when closing non-existent session', async () => { - await expect(browserManager.closeSession('invalid-id')).rejects.toThrow( + await expect(browserTracker.closeSession('invalid-id')).rejects.toThrow( new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR), ); }); @@ -63,17 +66,46 @@ describe('SessionManager', () => { describe('getSession', () => { it('should return existing session', async () => { - const session = await browserManager.createSession(); - const retrieved = browserManager.getSession(session.id); - expect(retrieved).toBe(session); + const session = await browserTracker.createSession(); + const retrieved = browserTracker.getSession(session.id); + expect(retrieved.id).toBe(session.id); }); it('should throw error for non-existent session', () => { expect(() => { - browserManager.getSession('invalid-id'); + browserTracker.getSession('invalid-id'); }).toThrow( new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR), ); }); }); + + describe('session tracking', () => { + it('should register and track browser sessions', async () => { + const instanceId = browserTracker.registerBrowser('https://example.com'); + expect(instanceId).toBeDefined(); + + const sessionInfo = browserTracker.getSessionById(instanceId); + expect(sessionInfo).toBeDefined(); + expect(sessionInfo?.status).toBe('running'); + expect(sessionInfo?.metadata.url).toBe('https://example.com'); + }); + + it('should update session status', async () => { + const instanceId = browserTracker.registerBrowser(); + const updated = browserTracker.updateSessionStatus( + instanceId, + SessionStatus.COMPLETED, + { + closedExplicitly: true, + }, + ); + + expect(updated).toBe(true); + + const sessionInfo = browserTracker.getSessionById(instanceId); + expect(sessionInfo?.status).toBe('completed'); + expect(sessionInfo?.metadata.closedExplicitly).toBe(true); + }); + }); }); diff --git a/packages/agent/src/tools/session/lib/element-state.test.ts b/packages/agent/src/tools/session/lib/element-state.test.ts index d2078b2..6fb43bc 100644 --- a/packages/agent/src/tools/session/lib/element-state.test.ts +++ b/packages/agent/src/tools/session/lib/element-state.test.ts @@ -8,19 +8,21 @@ import { vi, } from 'vitest'; -import { SessionManager } from './SessionManager.js'; +import { MockLogger } from '../../../utils/mockLogger.js'; +import { SessionTracker } from '../SessionTracker.js'; + import { Session } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); describe('Element State Tests', () => { - let browserManager: SessionManager; + let browserManager: SessionTracker; let session: Session; const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { - browserManager = new SessionManager(); + browserManager = new SessionTracker('test-agent', new MockLogger()); session = await browserManager.createSession({ headless: true }); }); diff --git a/packages/agent/src/tools/session/lib/form-interaction.test.ts b/packages/agent/src/tools/session/lib/form-interaction.test.ts index 5a7a7de..7c5f5de 100644 --- a/packages/agent/src/tools/session/lib/form-interaction.test.ts +++ b/packages/agent/src/tools/session/lib/form-interaction.test.ts @@ -8,19 +8,21 @@ import { vi, } from 'vitest'; -import { SessionManager } from './SessionManager.js'; +import { MockLogger } from '../../../utils/mockLogger.js'; +import { SessionTracker } from '../SessionTracker.js'; + import { Session } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); describe('Form Interaction Tests', () => { - let browserManager: SessionManager; + let browserManager: SessionTracker; let session: Session; const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { - browserManager = new SessionManager(); + browserManager = new SessionTracker('test-agent', new MockLogger()); session = await browserManager.createSession({ headless: true }); }); diff --git a/packages/agent/src/tools/session/lib/navigation.test.ts b/packages/agent/src/tools/session/lib/navigation.test.ts index 7cf887c..3b2e2d5 100644 --- a/packages/agent/src/tools/session/lib/navigation.test.ts +++ b/packages/agent/src/tools/session/lib/navigation.test.ts @@ -1,18 +1,20 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { SessionManager } from './SessionManager.js'; +import { MockLogger } from '../../../utils/mockLogger.js'; +import { SessionTracker } from '../SessionTracker.js'; + import { Session } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); describe('Browser Navigation Tests', () => { - let browserManager: SessionManager; + let browserManager: SessionTracker; let session: Session; const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { - browserManager = new SessionManager(); + browserManager = new SessionTracker('test-agent', new MockLogger()); session = await browserManager.createSession({ headless: true }); }); diff --git a/packages/agent/src/tools/session/lib/wait-behavior.test.ts b/packages/agent/src/tools/session/lib/wait-behavior.test.ts index a456c39..a2a76f2 100644 --- a/packages/agent/src/tools/session/lib/wait-behavior.test.ts +++ b/packages/agent/src/tools/session/lib/wait-behavior.test.ts @@ -8,19 +8,21 @@ import { vi, } from 'vitest'; -import { SessionManager } from './SessionManager.js'; +import { MockLogger } from '../../../utils/mockLogger.js'; +import { SessionTracker } from '../SessionTracker.js'; + import { Session } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); describe('Wait Behavior Tests', () => { - let browserManager: SessionManager; + let browserManager: SessionTracker; let session: Session; const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { - browserManager = new SessionManager(); + browserManager = new SessionTracker('test-agent', new MockLogger()); session = await browserManager.createSession({ headless: true }); }); diff --git a/packages/agent/src/tools/session/sessionMessage.ts b/packages/agent/src/tools/session/sessionMessage.ts index fd1c971..ab42d3d 100644 --- a/packages/agent/src/tools/session/sessionMessage.ts +++ b/packages/agent/src/tools/session/sessionMessage.ts @@ -6,7 +6,7 @@ import { errorToString } from '../../utils/errorToString.js'; import { sleep } from '../../utils/sleep.js'; import { filterPageContent } from './lib/filterPageContent.js'; -import { browserSessions, SelectorType } from './lib/types.js'; +import { SelectorType } from './lib/types.js'; import { SessionStatus } from './SessionTracker.js'; // Main parameter schema @@ -62,8 +62,13 @@ const getSelector = (selector: string, type?: SelectorType): string => { return `xpath=${selector}`; case SelectorType.TEXT: return `text=${selector}`; + case SelectorType.ROLE: + return `role=${selector}`; + case SelectorType.TESTID: + return `data-testid=${selector}`; + case SelectorType.CSS: default: - return selector; // CSS selector is default + return selector; } }; @@ -82,154 +87,192 @@ export const sessionMessageTool: Tool = { actionType, url, selector, - selectorType, + selectorType = SelectorType.CSS, text, - contentFilter = 'raw', + contentFilter, }, context, ): Promise => { const { logger, browserTracker } = context; + const effectiveContentFilter = contentFilter || 'raw'; - // Validate action format - if (!actionType) { - logger.error('Invalid action format: actionType is required'); - return { - status: 'error', - error: 'Invalid action format: actionType is required', - }; - } - - logger.debug(`Executing browser action: ${actionType}`); - logger.debug(`Webpage processing mode: ${contentFilter}`); + logger.debug( + `Browser action: ${actionType} on session ${instanceId.slice(0, 8)}`, + ); try { - const session = browserSessions.get(instanceId); - if (!session) { - throw new Error(`No browser session found with ID ${instanceId}`); + // Get the session info + const sessionInfo = browserTracker.getSessionById(instanceId); + if (!sessionInfo) { + throw new Error(`Session ${instanceId} not found`); } - const { page } = session; + // Get the browser session + const session = browserTracker.getSession(instanceId); + const page = session.page; + + // Update session metadata + browserTracker.updateSessionStatus(instanceId, SessionStatus.RUNNING, { + actionType, + }); + // Execute the appropriate action based on actionType switch (actionType) { case 'goto': { if (!url) { - throw new Error('URL required for goto action'); + throw new Error('URL is required for goto action'); } + // Navigate to the URL try { - // Try with 'domcontentloaded' first which is more reliable than 'networkidle' - logger.debug( - `Navigating to ${url} with 'domcontentloaded' waitUntil`, - ); - await page.goto(url, { waitUntil: 'domcontentloaded' }); - await sleep(3000); - const content = await filterPageContent( - page, - contentFilter, - context, - ); - logger.debug(`Content: ${content}`); - logger.debug('Navigation completed with domcontentloaded strategy'); - logger.debug(`Content length: ${content.length} characters`); - return { status: 'success', content }; - } catch (navError) { - // If that fails, try with no waitUntil option + await page.goto(url, { + waitUntil: 'domcontentloaded', + timeout: 30000, + }); + await sleep(1000); + } catch (error) { logger.warn( - `Failed with domcontentloaded strategy: ${errorToString(navError)}`, + `Failed to navigate with domcontentloaded: ${errorToString( + error, + )}`, ); - logger.debug( - `Retrying navigation to ${url} with no waitUntil option`, - ); - - try { - await page.goto(url); - await sleep(3000); - const content = await filterPageContent( - page, - contentFilter, - context, - ); - logger.debug(`Content: ${content}`); - logger.debug('Navigation completed with basic strategy'); - return { status: 'success', content }; - } catch (innerError) { - logger.error( - `Failed with basic navigation strategy: ${errorToString(innerError)}`, - ); - throw innerError; // Re-throw to be caught by outer catch block - } + // Try again with no waitUntil + await page.goto(url, { timeout: 30000 }); + await sleep(1000); } + + // Get content after navigation + const content = await filterPageContent( + page, + effectiveContentFilter, + context, + ); + + return { + status: 'success', + content, + }; } case 'click': { if (!selector) { - throw new Error('Selector required for click action'); + throw new Error('Selector is required for click action'); } - const clickSelector = getSelector(selector, selectorType); - await page.click(clickSelector); - await sleep(1000); // Wait for any content changes after click - const content = await filterPageContent(page, contentFilter, context); - logger.debug(`Click action completed on selector: ${clickSelector}`); - return { status: 'success', content }; + + const fullSelector = getSelector(selector, selectorType); + logger.debug(`Clicking element with selector: ${fullSelector}`); + + // Wait for the element to be visible + await page.waitForSelector(fullSelector, { state: 'visible' }); + await page.click(fullSelector); + await sleep(1000); + + // Get content after click + const content = await filterPageContent( + page, + effectiveContentFilter, + context, + ); + + return { + status: 'success', + content, + }; } case 'type': { - if (!selector || !text) { - throw new Error('Selector and text required for type action'); + if (!selector) { + throw new Error('Selector is required for type action'); } - const typeSelector = getSelector(selector, selectorType); - await page.fill(typeSelector, text); - logger.debug(`Type action completed on selector: ${typeSelector}`); - return { status: 'success' }; + if (!text) { + throw new Error('Text is required for type action'); + } + + const fullSelector = getSelector(selector, selectorType); + logger.debug( + `Typing "${text.substring(0, 20)}${ + text.length > 20 ? '...' : '' + }" into element with selector: ${fullSelector}`, + ); + + // Wait for the element to be visible + await page.waitForSelector(fullSelector, { state: 'visible' }); + await page.fill(fullSelector, text); + await sleep(500); + + // Get content after typing + const content = await filterPageContent( + page, + effectiveContentFilter, + context, + ); + + return { + status: 'success', + content, + }; } case 'wait': { if (!selector) { - throw new Error('Selector required for wait action'); + throw new Error('Selector is required for wait action'); } - const waitSelector = getSelector(selector, selectorType); - await page.waitForSelector(waitSelector); - logger.debug(`Wait action completed for selector: ${waitSelector}`); - return { status: 'success' }; + + const fullSelector = getSelector(selector, selectorType); + logger.debug(`Waiting for element with selector: ${fullSelector}`); + + // Wait for the element to be visible + await page.waitForSelector(fullSelector, { state: 'visible' }); + await sleep(500); + + // Get content after waiting + const content = await filterPageContent( + page, + effectiveContentFilter, + context, + ); + + return { + status: 'success', + content, + }; } case 'content': { - const content = await filterPageContent(page, contentFilter, context); - logger.debug('Page content retrieved successfully'); - logger.debug(`Content length: ${content.length} characters`); - return { status: 'success', content }; + // Just get the current page content + const content = await filterPageContent( + page, + effectiveContentFilter, + context, + ); + + return { + status: 'success', + content, + }; } case 'close': { - await session.page.context().close(); - await session.browser.close(); - browserSessions.delete(instanceId); - - // Update browser tracker when browser is explicitly closed - browserTracker.updateSessionStatus( - instanceId, - SessionStatus.COMPLETED, - { - closedExplicitly: true, - }, - ); + // Close the browser session + await browserTracker.closeSession(instanceId); - logger.debug('Browser session closed successfully'); - return { status: 'closed' }; + return { + status: 'closed', + }; } - default: { + default: throw new Error(`Unsupported action type: ${actionType}`); - } } } catch (error) { - logger.error('Browser action failed:', { error }); + logger.error(`Browser action failed: ${errorToString(error)}`); - // Update browser tracker with error status if action fails - browserTracker.updateSessionStatus(instanceId, SessionStatus.ERROR, { - error: errorToString(error), - actionType, - }); + // Update session status if we have a valid instanceId + if (instanceId) { + browserTracker.updateSessionStatus(instanceId, SessionStatus.ERROR, { + error: errorToString(error), + }); + } return { status: 'error', @@ -238,18 +281,50 @@ export const sessionMessageTool: Tool = { } }, - logParameters: ({ actionType, description, contentFilter }, { logger }) => { - const effectiveContentFilter = contentFilter || 'raw'; - logger.log( - `Performing browser action: ${actionType} with ${effectiveContentFilter} processing, ${description}`, - ); + logParameters: ( + { actionType, instanceId, url, selector, text: _text, description }, + { logger }, + ) => { + const shortId = instanceId.substring(0, 8); + switch (actionType) { + case 'goto': + logger.log(`Navigating browser ${shortId} to ${url}, ${description}`); + break; + case 'click': + logger.log( + `Clicking element "${selector}" in browser ${shortId}, ${description}`, + ); + break; + case 'type': + logger.log( + `Typing into element "${selector}" in browser ${shortId}, ${description}`, + ); + break; + case 'wait': + logger.log( + `Waiting for element "${selector}" in browser ${shortId}, ${description}`, + ); + break; + case 'content': + logger.log(`Getting content from browser ${shortId}, ${description}`); + break; + case 'close': + logger.log(`Closing browser ${shortId}, ${description}`); + break; + } }, logReturns: (output, { logger }) => { if (output.error) { logger.error(`Browser action failed: ${output.error}`); } else { - logger.log(`Browser action completed with status: ${output.status}`); + logger.log( + `Browser action completed with status: ${output.status}${ + output.content + ? ` (content length: ${output.content.length} characters)` + : '' + }`, + ); } }, }; diff --git a/packages/agent/src/tools/session/sessionStart.ts b/packages/agent/src/tools/session/sessionStart.ts index 1405080..2433a8a 100644 --- a/packages/agent/src/tools/session/sessionStart.ts +++ b/packages/agent/src/tools/session/sessionStart.ts @@ -5,10 +5,9 @@ import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; import { sleep } from '../../utils/sleep.js'; -import { BrowserDetector } from './lib/BrowserDetector.js'; +// Use detectBrowsers directly from SessionTracker since we've inlined browser detection import { filterPageContent } from './lib/filterPageContent.js'; -import { SessionManager } from './lib/SessionManager.js'; -import { browserSessions, BrowserConfig } from './lib/types.js'; +import { BrowserConfig } from './lib/types.js'; import { SessionStatus } from './SessionTracker.js'; const parameterSchema = z.object({ @@ -82,47 +81,22 @@ export const sessionStartTool: Tool = { sessionConfig.useSystemBrowsers = true; sessionConfig.preferredType = 'chromium'; - // Try to detect Chrome browser - const browsers = await BrowserDetector.detectBrowsers(); - const chrome = browsers.find((b) => - b.name.toLowerCase().includes('chrome'), - ); - if (chrome) { - logger.debug(`Found system Chrome at ${chrome.path}`); - sessionConfig.executablePath = chrome.path; - } + // Try to detect Chrome browser using browserTracker + // No need to detect browsers here, the SessionTracker will handle it + // Chrome detection is now handled by SessionTracker } logger.debug(`Browser config: ${JSON.stringify(sessionConfig)}`); - // Create a session manager and launch browser - const sessionManager = new SessionManager(); - const session = await sessionManager.createSession(sessionConfig); + // Create a session directly using the browserTracker + const session = await browserTracker.createSession(sessionConfig); // Set the default timeout session.page.setDefaultTimeout(timeout); - // Get references to the browser and page - const browser = session.browser; + // Get reference to the page const page = session.page; - // Store the session in the browserSessions map for compatibility - browserSessions.set(instanceId, { - browser, - page, - id: instanceId, - }); - - // Setup cleanup handlers - browser.on('disconnected', () => { - browserSessions.delete(instanceId); - // Update browser tracker when browser disconnects - browserTracker.updateSessionStatus( - instanceId, - SessionStatus.TERMINATED, - ); - }); - // Navigate to URL if provided let content = ''; if (url) { From e3384b39755bb66aea45cbbeb640b4a64e7feabb Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Tue, 25 Mar 2025 12:35:11 -0400 Subject: [PATCH 2/3] refactor: extract browser detection functions to separate file --- packages/agent/src/index.ts | 4 +- .../agent/src/tools/session/SessionTracker.ts | 259 +----------------- .../src/tools/session/lib/browserDetectors.ts | 254 +++++++++++++++++ .../agent/src/tools/session/sessionStart.ts | 16 +- 4 files changed, 276 insertions(+), 257 deletions(-) create mode 100644 packages/agent/src/tools/session/lib/browserDetectors.ts diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 2d84ff2..8dff129 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -18,7 +18,7 @@ export * from './tools/session/sessionStart.js'; export * from './tools/session/lib/PageController.js'; export * from './tools/session/listSessions.js'; export * from './tools/session/SessionTracker.js'; -// Export browser detector functions +export * from './tools/session/lib/browserDetectors.js'; export * from './tools/agent/AgentTracker.js'; // Tools - Interaction @@ -49,4 +49,4 @@ export * from './utils/logger.js'; export * from './utils/mockLogger.js'; export * from './utils/stringifyLimited.js'; export * from './utils/userPrompt.js'; -export * from './utils/interactiveInput.js'; +export * from './utils/interactiveInput.js'; \ No newline at end of file diff --git a/packages/agent/src/tools/session/SessionTracker.ts b/packages/agent/src/tools/session/SessionTracker.ts index f0871e7..9d818f5 100644 --- a/packages/agent/src/tools/session/SessionTracker.ts +++ b/packages/agent/src/tools/session/SessionTracker.ts @@ -1,246 +1,9 @@ -// Import browser detection functions directly -import { execSync } from 'child_process'; -import fs from 'fs'; -import { homedir } from 'os'; -import path from 'path'; - import { chromium, firefox, webkit } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from '../../utils/logger.js'; -// Browser info interface -interface BrowserInfo { - name: string; - type: 'chromium' | 'firefox' | 'webkit'; - path: string; -} - -// Browser detection functions -function canAccess(filePath: string): boolean { - try { - fs.accessSync(filePath); - return true; - } catch { - return false; - } -} - -async function detectMacOSBrowsers(): Promise { - const browsers: BrowserInfo[] = []; - - // Chrome paths - const chromePaths = [ - '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', - `${homedir()}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, - `${homedir()}/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary`, - ]; - - // Edge paths - const edgePaths = [ - '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', - `${homedir()}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`, - ]; - - // Firefox paths - const firefoxPaths = [ - '/Applications/Firefox.app/Contents/MacOS/firefox', - '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', - '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', - `${homedir()}/Applications/Firefox.app/Contents/MacOS/firefox`, - ]; - - // Check Chrome paths - for (const chromePath of chromePaths) { - if (canAccess(chromePath)) { - browsers.push({ - name: 'Chrome', - type: 'chromium', - path: chromePath, - }); - } - } - - // Check Edge paths - for (const edgePath of edgePaths) { - if (canAccess(edgePath)) { - browsers.push({ - name: 'Edge', - type: 'chromium', // Edge is Chromium-based - path: edgePath, - }); - } - } - - // Check Firefox paths - for (const firefoxPath of firefoxPaths) { - if (canAccess(firefoxPath)) { - browsers.push({ - name: 'Firefox', - type: 'firefox', - path: firefoxPath, - }); - } - } - - return browsers; -} - -async function detectWindowsBrowsers(): Promise { - const browsers: BrowserInfo[] = []; - - // Common installation paths for Chrome - const chromePaths = [ - path.join( - process.env.LOCALAPPDATA || '', - 'Google/Chrome/Application/chrome.exe', - ), - path.join( - process.env.PROGRAMFILES || '', - 'Google/Chrome/Application/chrome.exe', - ), - path.join( - process.env['PROGRAMFILES(X86)'] || '', - 'Google/Chrome/Application/chrome.exe', - ), - ]; - - // Common installation paths for Edge - const edgePaths = [ - path.join( - process.env.LOCALAPPDATA || '', - 'Microsoft/Edge/Application/msedge.exe', - ), - path.join( - process.env.PROGRAMFILES || '', - 'Microsoft/Edge/Application/msedge.exe', - ), - path.join( - process.env['PROGRAMFILES(X86)'] || '', - 'Microsoft/Edge/Application/msedge.exe', - ), - ]; - - // Common installation paths for Firefox - const firefoxPaths = [ - path.join(process.env.PROGRAMFILES || '', 'Mozilla Firefox/firefox.exe'), - path.join( - process.env['PROGRAMFILES(X86)'] || '', - 'Mozilla Firefox/firefox.exe', - ), - ]; - - // Check Chrome paths - for (const chromePath of chromePaths) { - if (canAccess(chromePath)) { - browsers.push({ - name: 'Chrome', - type: 'chromium', - path: chromePath, - }); - } - } - - // Check Edge paths - for (const edgePath of edgePaths) { - if (canAccess(edgePath)) { - browsers.push({ - name: 'Edge', - type: 'chromium', // Edge is Chromium-based - path: edgePath, - }); - } - } - - // Check Firefox paths - for (const firefoxPath of firefoxPaths) { - if (canAccess(firefoxPath)) { - browsers.push({ - name: 'Firefox', - type: 'firefox', - path: firefoxPath, - }); - } - } - - return browsers; -} - -async function detectLinuxBrowsers(): Promise { - const browsers: BrowserInfo[] = []; - - // Try to find Chrome/Chromium using the 'which' command - const chromiumExecutables = [ - 'google-chrome-stable', - 'google-chrome', - 'chromium-browser', - 'chromium', - ]; - - // Try to find Firefox using the 'which' command - const firefoxExecutables = ['firefox']; - - // Check for Chrome/Chromium - for (const executable of chromiumExecutables) { - try { - const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) - .toString() - .trim(); - if (canAccess(browserPath)) { - browsers.push({ - name: executable, - type: 'chromium', - path: browserPath, - }); - } - } catch { - // Not installed - } - } - - // Check for Firefox - for (const executable of firefoxExecutables) { - try { - const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) - .toString() - .trim(); - if (canAccess(browserPath)) { - browsers.push({ - name: 'Firefox', - type: 'firefox', - path: browserPath, - }); - } - } catch { - // Not installed - } - } - - return browsers; -} - -async function detectBrowsers(): Promise { - const platform = process.platform; - let browsers: BrowserInfo[] = []; - - switch (platform) { - case 'darwin': - browsers = await detectMacOSBrowsers(); - break; - case 'win32': - browsers = await detectWindowsBrowsers(); - break; - case 'linux': - browsers = await detectLinuxBrowsers(); - break; - default: - console.log(`Unsupported platform: ${platform}`); - break; - } - - return browsers; -} +import { detectBrowsers, BrowserInfo } from './lib/browserDetectors.js'; import { BrowserConfig, Session, @@ -286,11 +49,7 @@ export class SessionTracker { useSystemBrowsers: true, preferredType: 'chromium', }; - private detectedBrowsers: Array<{ - name: string; - type: 'chromium' | 'firefox' | 'webkit'; - path: string; - }> = []; + private detectedBrowsers: BrowserInfo[] = []; private browserDetectionPromise: Promise | null = null; constructor( @@ -484,7 +243,7 @@ export class SessionTracker { this.browserSessions.set(session.id, session); // Also store in global browserSessions for compatibility browserSessions.set(session.id, session); - + this.setupCleanup(session); return session; @@ -553,7 +312,7 @@ export class SessionTracker { this.browserSessions.set(session.id, session); // Also store in global browserSessions for compatibility browserSessions.set(session.id, session); - + this.setupCleanup(session); return session; @@ -589,11 +348,11 @@ export class SessionTracker { // In Playwright, we should close the context which will automatically close its pages await session.page.context().close(); await session.browser.close(); - + // Remove from both maps this.browserSessions.delete(sessionId); browserSessions.delete(sessionId); - + // Update status this.updateSessionStatus(sessionId, SessionStatus.COMPLETED, { closedExplicitly: true, @@ -602,7 +361,7 @@ export class SessionTracker { this.updateSessionStatus(sessionId, SessionStatus.ERROR, { error: error instanceof Error ? error.message : String(error), }); - + throw new BrowserError( 'Failed to close session', BrowserErrorCode.SESSION_ERROR, @@ -633,7 +392,7 @@ export class SessionTracker { session.browser.on('disconnected', () => { this.browserSessions.delete(session.id); browserSessions.delete(session.id); - + // Update session status this.updateSessionStatus(session.id, SessionStatus.TERMINATED); }); @@ -678,4 +437,4 @@ export class SessionTracker { }); }); } -} +} \ No newline at end of file diff --git a/packages/agent/src/tools/session/lib/browserDetectors.ts b/packages/agent/src/tools/session/lib/browserDetectors.ts new file mode 100644 index 0000000..df53121 --- /dev/null +++ b/packages/agent/src/tools/session/lib/browserDetectors.ts @@ -0,0 +1,254 @@ +import { execSync } from 'child_process'; +import fs from 'fs'; +import { homedir } from 'os'; +import path from 'path'; + +/** + * Browser information interface + */ +export interface BrowserInfo { + name: string; + type: 'chromium' | 'firefox' | 'webkit'; + path: string; +} + +/** + * Check if a file exists and is accessible + */ +export function canAccess(filePath: string): boolean { + try { + fs.accessSync(filePath); + return true; + } catch { + return false; + } +} + +/** + * Detect browsers on macOS + */ +export async function detectMacOSBrowsers(): Promise { + const browsers: BrowserInfo[] = []; + + // Chrome paths + const chromePaths = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + `${homedir()}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, + `${homedir()}/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary`, + ]; + + // Edge paths + const edgePaths = [ + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', + `${homedir()}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`, + ]; + + // Firefox paths + const firefoxPaths = [ + '/Applications/Firefox.app/Contents/MacOS/firefox', + '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', + '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', + `${homedir()}/Applications/Firefox.app/Contents/MacOS/firefox`, + ]; + + // Check Chrome paths + for (const chromePath of chromePaths) { + if (canAccess(chromePath)) { + browsers.push({ + name: 'Chrome', + type: 'chromium', + path: chromePath, + }); + } + } + + // Check Edge paths + for (const edgePath of edgePaths) { + if (canAccess(edgePath)) { + browsers.push({ + name: 'Edge', + type: 'chromium', // Edge is Chromium-based + path: edgePath, + }); + } + } + + // Check Firefox paths + for (const firefoxPath of firefoxPaths) { + if (canAccess(firefoxPath)) { + browsers.push({ + name: 'Firefox', + type: 'firefox', + path: firefoxPath, + }); + } + } + + return browsers; +} + +/** + * Detect browsers on Windows + */ +export async function detectWindowsBrowsers(): Promise { + const browsers: BrowserInfo[] = []; + + // Common installation paths for Chrome + const chromePaths = [ + path.join( + process.env.LOCALAPPDATA || '', + 'Google/Chrome/Application/chrome.exe', + ), + path.join( + process.env.PROGRAMFILES || '', + 'Google/Chrome/Application/chrome.exe', + ), + path.join( + process.env['PROGRAMFILES(X86)'] || '', + 'Google/Chrome/Application/chrome.exe', + ), + ]; + + // Common installation paths for Edge + const edgePaths = [ + path.join( + process.env.LOCALAPPDATA || '', + 'Microsoft/Edge/Application/msedge.exe', + ), + path.join( + process.env.PROGRAMFILES || '', + 'Microsoft/Edge/Application/msedge.exe', + ), + path.join( + process.env['PROGRAMFILES(X86)'] || '', + 'Microsoft/Edge/Application/msedge.exe', + ), + ]; + + // Common installation paths for Firefox + const firefoxPaths = [ + path.join(process.env.PROGRAMFILES || '', 'Mozilla Firefox/firefox.exe'), + path.join( + process.env['PROGRAMFILES(X86)'] || '', + 'Mozilla Firefox/firefox.exe', + ), + ]; + + // Check Chrome paths + for (const chromePath of chromePaths) { + if (canAccess(chromePath)) { + browsers.push({ + name: 'Chrome', + type: 'chromium', + path: chromePath, + }); + } + } + + // Check Edge paths + for (const edgePath of edgePaths) { + if (canAccess(edgePath)) { + browsers.push({ + name: 'Edge', + type: 'chromium', // Edge is Chromium-based + path: edgePath, + }); + } + } + + // Check Firefox paths + for (const firefoxPath of firefoxPaths) { + if (canAccess(firefoxPath)) { + browsers.push({ + name: 'Firefox', + type: 'firefox', + path: firefoxPath, + }); + } + } + + return browsers; +} + +/** + * Detect browsers on Linux + */ +export async function detectLinuxBrowsers(): Promise { + const browsers: BrowserInfo[] = []; + + // Try to find Chrome/Chromium using the 'which' command + const chromiumExecutables = [ + 'google-chrome-stable', + 'google-chrome', + 'chromium-browser', + 'chromium', + ]; + + // Try to find Firefox using the 'which' command + const firefoxExecutables = ['firefox']; + + // Check for Chrome/Chromium + for (const executable of chromiumExecutables) { + try { + const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) + .toString() + .trim(); + if (canAccess(browserPath)) { + browsers.push({ + name: executable, + type: 'chromium', + path: browserPath, + }); + } + } catch { + // Not installed + } + } + + // Check for Firefox + for (const executable of firefoxExecutables) { + try { + const browserPath = execSync(`which ${executable}`, { stdio: 'pipe' }) + .toString() + .trim(); + if (canAccess(browserPath)) { + browsers.push({ + name: 'Firefox', + type: 'firefox', + path: browserPath, + }); + } + } catch { + // Not installed + } + } + + return browsers; +} + +/** + * Detect available browsers on the system + * Returns an array of browser information objects sorted by preference + */ +export async function detectBrowsers(): Promise { + const platform = process.platform; + let browsers: BrowserInfo[] = []; + + switch (platform) { + case 'darwin': + browsers = await detectMacOSBrowsers(); + break; + case 'win32': + browsers = await detectWindowsBrowsers(); + break; + case 'linux': + browsers = await detectLinuxBrowsers(); + break; + default: + console.log(`Unsupported platform: ${platform}`); + break; + } + + return browsers; +} \ No newline at end of file diff --git a/packages/agent/src/tools/session/sessionStart.ts b/packages/agent/src/tools/session/sessionStart.ts index 2433a8a..221bc2f 100644 --- a/packages/agent/src/tools/session/sessionStart.ts +++ b/packages/agent/src/tools/session/sessionStart.ts @@ -5,7 +5,7 @@ import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; import { sleep } from '../../utils/sleep.js'; -// Use detectBrowsers directly from SessionTracker since we've inlined browser detection +import { detectBrowsers } from './lib/browserDetectors.js'; import { filterPageContent } from './lib/filterPageContent.js'; import { BrowserConfig } from './lib/types.js'; import { SessionStatus } from './SessionTracker.js'; @@ -81,9 +81,15 @@ export const sessionStartTool: Tool = { sessionConfig.useSystemBrowsers = true; sessionConfig.preferredType = 'chromium'; - // Try to detect Chrome browser using browserTracker - // No need to detect browsers here, the SessionTracker will handle it - // Chrome detection is now handled by SessionTracker + // Try to detect Chrome browser + const browsers = await detectBrowsers(); + const chrome = browsers.find((b) => + b.name.toLowerCase().includes('chrome'), + ); + if (chrome) { + logger.debug(`Found system Chrome at ${chrome.path}`); + sessionConfig.executablePath = chrome.path; + } } logger.debug(`Browser config: ${JSON.stringify(sessionConfig)}`); @@ -184,4 +190,4 @@ export const sessionStartTool: Tool = { logger.log(`Browser session started with ID: ${output.instanceId}`); } }, -}; +}; \ No newline at end of file From b6c779d9acd5f6f5bcccba99513cc35364eb4e8c Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Tue, 25 Mar 2025 12:44:06 -0400 Subject: [PATCH 3/3] chore: format and lint --- packages/agent/src/index.ts | 2 +- packages/agent/src/tools/session/SessionTracker.ts | 14 +++++++------- .../src/tools/session/lib/browserDetectors.ts | 2 +- packages/agent/src/tools/session/sessionStart.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 8dff129..13c520a 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -49,4 +49,4 @@ export * from './utils/logger.js'; export * from './utils/mockLogger.js'; export * from './utils/stringifyLimited.js'; export * from './utils/userPrompt.js'; -export * from './utils/interactiveInput.js'; \ No newline at end of file +export * from './utils/interactiveInput.js'; diff --git a/packages/agent/src/tools/session/SessionTracker.ts b/packages/agent/src/tools/session/SessionTracker.ts index 9d818f5..260c41d 100644 --- a/packages/agent/src/tools/session/SessionTracker.ts +++ b/packages/agent/src/tools/session/SessionTracker.ts @@ -243,7 +243,7 @@ export class SessionTracker { this.browserSessions.set(session.id, session); // Also store in global browserSessions for compatibility browserSessions.set(session.id, session); - + this.setupCleanup(session); return session; @@ -312,7 +312,7 @@ export class SessionTracker { this.browserSessions.set(session.id, session); // Also store in global browserSessions for compatibility browserSessions.set(session.id, session); - + this.setupCleanup(session); return session; @@ -348,11 +348,11 @@ export class SessionTracker { // In Playwright, we should close the context which will automatically close its pages await session.page.context().close(); await session.browser.close(); - + // Remove from both maps this.browserSessions.delete(sessionId); browserSessions.delete(sessionId); - + // Update status this.updateSessionStatus(sessionId, SessionStatus.COMPLETED, { closedExplicitly: true, @@ -361,7 +361,7 @@ export class SessionTracker { this.updateSessionStatus(sessionId, SessionStatus.ERROR, { error: error instanceof Error ? error.message : String(error), }); - + throw new BrowserError( 'Failed to close session', BrowserErrorCode.SESSION_ERROR, @@ -392,7 +392,7 @@ export class SessionTracker { session.browser.on('disconnected', () => { this.browserSessions.delete(session.id); browserSessions.delete(session.id); - + // Update session status this.updateSessionStatus(session.id, SessionStatus.TERMINATED); }); @@ -437,4 +437,4 @@ export class SessionTracker { }); }); } -} \ No newline at end of file +} diff --git a/packages/agent/src/tools/session/lib/browserDetectors.ts b/packages/agent/src/tools/session/lib/browserDetectors.ts index df53121..f9a3735 100644 --- a/packages/agent/src/tools/session/lib/browserDetectors.ts +++ b/packages/agent/src/tools/session/lib/browserDetectors.ts @@ -251,4 +251,4 @@ export async function detectBrowsers(): Promise { } return browsers; -} \ No newline at end of file +} diff --git a/packages/agent/src/tools/session/sessionStart.ts b/packages/agent/src/tools/session/sessionStart.ts index 221bc2f..384f2ad 100644 --- a/packages/agent/src/tools/session/sessionStart.ts +++ b/packages/agent/src/tools/session/sessionStart.ts @@ -190,4 +190,4 @@ export const sessionStartTool: Tool = { logger.log(`Browser session started with ID: ${output.instanceId}`); } }, -}; \ No newline at end of file +};