diff --git a/.env.example b/.env.example index ba110f8..7104ae6 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,10 @@ HTTP_MCP_PORT=9100 AGENT_PORT=9200 EXTENSION_PORT=9300 +# Optional directories +# RESOURCES_DIR=./resources +# EXECUTION_DIR=./out/ + # Agent Configuration MAX_SESSIONS=5 SESSION_IDLE_TIMEOUT_MS=90000 @@ -23,7 +27,7 @@ EVENT_GAP_TIMEOUT_MS=60000 BROWSEROS_BINARY=/Applications/BrowserOS.app/Contents/MacOS/BrowserOS -# PostHog +# PostHog POSTHOG_API_KEY= POSTHOG_ENDPOINT= diff --git a/packages/agent/src/agent/ClaudeSDKAgent.ts b/packages/agent/src/agent/ClaudeSDKAgent.ts index 04a8a27..de723a9 100644 --- a/packages/agent/src/agent/ClaudeSDKAgent.ts +++ b/packages/agent/src/agent/ClaudeSDKAgent.ts @@ -245,9 +245,7 @@ export class ClaudeSDKAgent extends BaseAgent { this.startExecution(); this.abortController = new AbortController(); - logger.info('🤖 ClaudeSDKAgent executing', { - message: message.substring(0, 100), - }); + logger.info('🤖 ClaudeSDKAgent executing', {message}); try { const options: any = { @@ -329,11 +327,7 @@ export class ClaudeSDKAgent extends BaseAgent { subtype: (event as any).subtype, is_error: (event as any).is_error, num_turns: numTurns, - result: (event as any).result - ? typeof (event as any).result === 'string' - ? (event as any).result.substring(0, 200) - : JSON.stringify((event as any).result).substring(0, 200) - : 'N/A', + result: (event as any).result ?? 'N/A', }); } diff --git a/packages/agent/src/agent/CodexSDKAgent.ts b/packages/agent/src/agent/CodexSDKAgent.ts index 60753c0..66017f2 100644 --- a/packages/agent/src/agent/CodexSDKAgent.ts +++ b/packages/agent/src/agent/CodexSDKAgent.ts @@ -320,7 +320,7 @@ export class CodexSDKAgent extends BaseAgent { this.abortController = new AbortController(); logger.info('🤖 CodexSDKAgent executing', { - message: message.substring(0, 100), + message, }); try { @@ -415,12 +415,11 @@ export class CodexSDKAgent extends BaseAgent { const event = result.value; - // Log Codex events for debugging - const eventData = JSON.stringify(event).substring(0, 100); + // Log Codex events for debugging (console view truncates automatically) if (event.type === 'error' || event.type === 'turn.failed') { - logger.error('Codex event', {type: event.type, data: eventData}); + logger.error('Codex event', event); } else { - logger.debug('Codex event', {type: event.type, data: eventData}); + logger.debug('Codex event', event); } // Update event time diff --git a/packages/common/src/logger.ts b/packages/common/src/logger.ts index da771b8..da5672c 100644 --- a/packages/common/src/logger.ts +++ b/packages/common/src/logger.ts @@ -6,6 +6,7 @@ import fs from 'node:fs'; import path from 'node:path'; type LogLevel = 'debug' | 'info' | 'warn' | 'error'; +type FormatOptions = {useColor?: boolean; truncateStrings?: boolean}; const COLORS = { debug: '\x1b[36m', @@ -15,6 +16,7 @@ const COLORS = { }; const RESET = '\x1b[0m'; +const CONSOLE_META_CHAR_LIMIT = 100; class Logger { private level: LogLevel; @@ -28,21 +30,45 @@ class Logger { this.logFilePath = path.join(logDir, 'browseros-server.log'); } - private format(level: LogLevel, message: string, meta?: object): string { + private format( + level: LogLevel, + message: string, + meta?: object, + {useColor = true, truncateStrings = false}: FormatOptions = {}, + ): string { const timestamp = new Date().toISOString(); - const color = COLORS[level]; - const metaStr = meta ? `\n${JSON.stringify(meta, null, 2)}` : ''; - return `${color}[${timestamp}] [${level.toUpperCase()}]${RESET} ${message}${metaStr}`; + const prefix = useColor + ? `${COLORS[level]}[${timestamp}] [${level.toUpperCase()}]${RESET}` + : `[${timestamp}] [${level.toUpperCase()}]`; + const metaStr = meta + ? `\n${this.stringifyMeta(meta, truncateStrings)}` + : ''; + return `${prefix} ${message}${metaStr}`; } - private formatPlain(level: LogLevel, message: string, meta?: object): string { - const timestamp = new Date().toISOString(); - const metaStr = meta ? `\n${JSON.stringify(meta, null, 2)}` : ''; - return `[${timestamp}] [${level.toUpperCase()}] ${message}${metaStr}`; + private stringifyMeta(meta: object, truncateStrings: boolean): string { + return JSON.stringify( + meta, + (key, value) => { + if ( + truncateStrings && + typeof value === 'string' && + value.length > CONSOLE_META_CHAR_LIMIT + ) { + const extra = value.length - CONSOLE_META_CHAR_LIMIT; + return `${value.slice(0, CONSOLE_META_CHAR_LIMIT)}... (+${extra} chars)`; + } + return value; + }, + 2, + ); } private log(level: LogLevel, message: string, meta?: object) { - const formatted = this.format(level, message, meta); + const formatted = this.format(level, message, meta, { + useColor: true, + truncateStrings: true, + }); switch (level) { case 'error': @@ -56,7 +82,10 @@ class Logger { } if (this.logFilePath) { - const plainFormatted = this.formatPlain(level, message, meta); + const plainFormatted = this.format(level, message, meta, { + useColor: false, + truncateStrings: false, + }); try { fs.appendFileSync(this.logFilePath, plainFormatted + '\n'); } catch (error) { diff --git a/packages/controller-ext/manifest.json b/packages/controller-ext/manifest.json index acdfbfe..57300aa 100644 --- a/packages/controller-ext/manifest.json +++ b/packages/controller-ext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "BrowserOS Controller", - "version": "1.0.0.5", + "version": "1.0.0.8", "description": "BrowserOS API bridge for BrowserOS Server", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhlh9i/c2A3f0PL86hXhGPzguLIOQ+sPf3/Y8RD11gmdvoU6XqnUqv7GgBvm7SW7316uPnS58AYZY13jGtF4rFrscdda5H2CjZrtOyOycmKp2KzibJLwibXNm/JwKhZ3QEfgsW/orh1SMY2kNj62JemkWLcLyn3E1T+KTcTVyFOxiJS3hyQ+Y0/Jp1HOqGh5lYS58YYzwhId5rrJjfL7wFYtALgt2dEA2r7p4qpe+SW0QLA+ayjRAjS+yt+qitR0eWg+XgqcIk1f1KblN8/yDISssSD4LWiPofe5CmJPnqlHIuI0CpgvAFv9dvgR/w8OFkXxK5h06i6saum1xExj+IwIDAQAB", "permissions": [ diff --git a/packages/controller-ext/src/background/BrowserOSController.ts b/packages/controller-ext/src/background/BrowserOSController.ts index ec837a4..5629bb5 100644 --- a/packages/controller-ext/src/background/BrowserOSController.ts +++ b/packages/controller-ext/src/background/BrowserOSController.ts @@ -111,6 +111,18 @@ export class BrowserOSController { return this.wsClient.isConnected(); } + notifyWindowFocused(windowId?: number): void { + try { + this.wsClient.send({type: 'focused', windowId}); + logger.debug('Sent focused event', {windowId}); + } catch (error) { + logger.warn('Failed to send focused event', { + windowId, + error: error instanceof Error ? error.message : String(error), + }); + } + } + private registerActions(): void { logger.info('Registering actions...'); diff --git a/packages/controller-ext/src/background/index.ts b/packages/controller-ext/src/background/index.ts index 88f21af..118a2b2 100644 --- a/packages/controller-ext/src/background/index.ts +++ b/packages/controller-ext/src/background/index.ts @@ -100,7 +100,7 @@ async function getOrCreateController(): Promise { } async function shutdownController(reason: string): Promise { - logger.info(`[BrowserOS Controller] Shutdown requested: ${reason}`); + logger.info('Controller shutdown requested', {reason}); if (controllerState.initPromise) { try { @@ -138,27 +138,42 @@ function ensureControllerRunning(trigger: string): void { getOrCreateController().catch(error => { const message = error instanceof Error ? error.message : JSON.stringify(error); - logger.error( - `[BrowserOS Controller] Failed to start (trigger=${trigger}): ${message}`, - ); + logger.error('Controller failed to start', {trigger, error: message}); }); } -logger.info('[BrowserOS Controller] Extension loaded'); +logger.info('Extension loaded'); chrome.runtime.onInstalled.addListener(() => { - logger.info('[BrowserOS Controller] Extension installed'); + logger.info('Extension installed'); }); chrome.runtime.onStartup.addListener(() => { - logger.info('[BrowserOS Controller] Browser startup event'); + logger.info('Browser startup event'); ensureControllerRunning('runtime.onStartup'); }); // Immediately attempt to start the controller when the service worker initializes ensureControllerRunning('service-worker-init'); +chrome.windows.onFocusChanged.addListener(windowId => { + if (windowId === chrome.windows.WINDOW_ID_NONE) { + return; + } + + notifyWindowFocused(windowId).catch(error => { + const message = + error instanceof Error ? error.message : JSON.stringify(error); + logger.warn('Failed to notify focus change', {windowId, error: message}); + }); +}); + chrome.runtime.onSuspend?.addListener(() => { - logger.info('[BrowserOS Controller] Extension suspending'); + logger.info('Extension suspending'); void shutdownController('runtime.onSuspend'); }); + +async function notifyWindowFocused(windowId: number): Promise { + const controller = await getOrCreateController(); + controller.notifyWindowFocused(windowId); +} diff --git a/packages/controller-ext/src/config/constants.ts b/packages/controller-ext/src/config/constants.ts index 342dc67..69512af 100644 --- a/packages/controller-ext/src/config/constants.ts +++ b/packages/controller-ext/src/config/constants.ts @@ -53,5 +53,5 @@ export const CONCURRENCY_CONFIG: ConcurrencyConfig = { export const LOGGING_CONFIG: LoggingConfig = { enabled: true, level: 'info', - prefix: '[BrowserOS Controller]', + prefix: '', }; diff --git a/packages/controller-ext/src/utils/Logger.ts b/packages/controller-ext/src/utils/Logger.ts index 56c681a..bec02c5 100644 --- a/packages/controller-ext/src/utils/Logger.ts +++ b/packages/controller-ext/src/utils/Logger.ts @@ -14,44 +14,45 @@ export class Logger { this.prefix = prefix; } - log(message: string, level: LogLevel = 'info'): void { + log(message: string, level: LogLevel = 'info', data?: object): void { if (!LOGGING_CONFIG.enabled) return; const timestamp = new Date().toISOString(); const logMessage = `${this.prefix} [${timestamp}] ${message}`; + const formattedData = data ? `\n${JSON.stringify(data, null, 2)}` : ''; switch (level) { case 'debug': - if (LOGGING_CONFIG.level === 'debug') console.log(logMessage); + if (LOGGING_CONFIG.level === 'debug') console.log(logMessage + formattedData); break; case 'info': if (['debug', 'info'].includes(LOGGING_CONFIG.level)) - console.info(logMessage); + console.info(logMessage + formattedData); break; case 'warn': if (['debug', 'info', 'warn'].includes(LOGGING_CONFIG.level)) - console.warn(logMessage); + console.warn(logMessage + formattedData); break; case 'error': - console.error(logMessage); + console.error(logMessage + formattedData); break; } } - debug(message: string): void { - this.log(message, 'debug'); + debug(message: string, data?: object): void { + this.log(message, 'debug', data); } - info(message: string): void { - this.log(message, 'info'); + info(message: string, data?: object): void { + this.log(message, 'info', data); } - warn(message: string): void { - this.log(message, 'warn'); + warn(message: string, data?: object): void { + this.log(message, 'warn', data); } - error(message: string): void { - this.log(message, 'error'); + error(message: string, data?: object): void { + this.log(message, 'error', data); } } diff --git a/packages/controller-ext/src/websocket/WebSocketClient.ts b/packages/controller-ext/src/websocket/WebSocketClient.ts index d27bbb6..2250650 100644 --- a/packages/controller-ext/src/websocket/WebSocketClient.ts +++ b/packages/controller-ext/src/websocket/WebSocketClient.ts @@ -68,18 +68,10 @@ export class WebSocketClient { this._setStatus(ConnectionStatus.DISCONNECTED); } - send(message: ProtocolRequest | ProtocolResponse): void { - if (this.status !== ConnectionStatus.CONNECTED) { - throw new Error('WebSocket not connected'); - } - - if (!this.ws) { - throw new Error('WebSocket instance is null'); - } - - const messageStr = JSON.stringify(message); - logger.debug(`Sending: ${messageStr.substring(0, 100)}...`); - this.ws.send(messageStr); + send( + message: ProtocolRequest | ProtocolResponse | Record, + ): void { + this._sendSerialized(message); } onMessage(handler: (msg: ProtocolResponse) => void): void { @@ -220,8 +212,7 @@ export class WebSocketClient { // Send ping try { - const pingMessage = JSON.stringify({type: 'ping'}); - this.ws.send(pingMessage); + this._sendSerialized({type: 'ping'}); this.pendingPing = true; logger.debug('Sent heartbeat ping'); @@ -282,4 +273,20 @@ export class WebSocketClient { // Emit to all status handlers this.statusHandlers.forEach(handler => handler(status)); } + + private _sendSerialized( + message: ProtocolRequest | ProtocolResponse | Record, + ): void { + if (this.status !== ConnectionStatus.CONNECTED) { + throw new Error('WebSocket not connected'); + } + + if (!this.ws) { + throw new Error('WebSocket instance is null'); + } + + const messageStr = JSON.stringify(message); + logger.debug(`Sending: ${messageStr.substring(0, 100)}...`); + this.ws.send(messageStr); + } } diff --git a/packages/controller-server/src/ControllerBridge.ts b/packages/controller-server/src/ControllerBridge.ts index d71df92..b20ce74 100644 --- a/packages/controller-server/src/ControllerBridge.ts +++ b/packages/controller-server/src/ControllerBridge.ts @@ -60,10 +60,15 @@ export class ControllerBridge { ws.send(JSON.stringify({type: 'pong'})); return; } + if (parsed.type === 'focused') { + this.handleFocusEvent(clientId, parsed.windowId); + return; + } - this.logger.debug( - `Received message from ${clientId}: ${message.substring(0, 100)}${message.length > 100 ? '...' : ''}`, - ); + this.logger.debug('Received message from controller client', { + clientId, + message, + }); const response = parsed as ControllerResponse; this.handleResponse(response); } catch (error) { @@ -117,7 +122,9 @@ export class ControllerBridge { const request: ControllerRequest = {id, action, payload}; try { const message = JSON.stringify(request); - this.logger.debug(`Sending request to ${this.primaryClientId}: ${message}`); + this.logger.debug( + `Sending request to ${this.primaryClientId}: ${message}`, + ); client.send(message); } catch (error) { clearTimeout(timeout); @@ -180,7 +187,10 @@ export class ControllerBridge { this.primaryClientId = clientId; this.logger.info('Primary controller assigned', {clientId}); } else { - this.logger.info('Controller connected in standby mode', {clientId, primaryClientId: this.primaryClientId}); + this.logger.info('Controller connected in standby mode', { + clientId, + primaryClientId: this.primaryClientId, + }); } return clientId; @@ -218,6 +228,26 @@ export class ControllerBridge { } this.primaryClientId = nextEntry.value; - this.logger.info('Promoted controller to primary', {clientId: this.primaryClientId}); + this.logger.info('Promoted controller to primary', { + clientId: this.primaryClientId, + }); + } + + private handleFocusEvent(clientId: string, windowId?: number): void { + if (this.primaryClientId === clientId) { + this.logger.debug('Focus event from current primary', { + clientId, + windowId, + }); + return; + } + + const previousPrimary = this.primaryClientId; + this.primaryClientId = clientId; + this.logger.info('Primary controller reassigned due to focus event', { + clientId, + previousPrimary, + windowId, + }); } } diff --git a/packages/server/src/args.ts b/packages/server/src/args.ts index 667166c..b029a25 100644 --- a/packages/server/src/args.ts +++ b/packages/server/src/args.ts @@ -2,6 +2,7 @@ * @license * Copyright 2025 BrowserOS */ +import path from 'node:path'; import {Command, InvalidArgumentError} from 'commander'; import {version} from '../../../package.json' assert {type: 'json'}; @@ -12,8 +13,8 @@ export interface ServerPorts { agentPort: number; extensionPort: number; mcpServerEnabled: boolean; - resourcesDir?: string; - executionDir?: string; + resourcesDir: string; + executionDir: string; // Future: httpsMcpPort?: number; } @@ -90,9 +91,15 @@ export function parseArguments(argv = process.argv): ServerPorts { ? parsePort(process.env.EXTENSION_PORT) : undefined); - const executionDir = - options.executionDir ?? - (process.env.EXECUTION_DIR ? process.env.EXECUTION_DIR : undefined); + const cwd = process.cwd(); + const resolvedResourcesDir = resolvePath( + options.resourcesDir ?? process.env.RESOURCES_DIR, + cwd, + ); + const resolvedExecutionDir = resolvePath( + options.executionDir ?? process.env.EXECUTION_DIR, + resolvedResourcesDir, + ); const missing: string[] = []; if (!httpMcpPort) missing.push('HTTP_MCP_PORT'); @@ -113,7 +120,12 @@ export function parseArguments(argv = process.argv): ServerPorts { agentPort: agentPort!, extensionPort: extensionPort!, mcpServerEnabled: !options.disableMcpServer, - resourcesDir: options.resourcesDir, - executionDir, + resourcesDir: resolvedResourcesDir, + executionDir: resolvedExecutionDir, }; } + +function resolvePath(target: string | undefined, baseDir: string): string { + if (!target) return baseDir; + return path.isAbsolute(target) ? target : path.resolve(baseDir, target); +} diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index c20bd31..bdfe0d0 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -5,6 +5,8 @@ * Main server orchestration */ import type http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; import { createAgentServer, @@ -37,10 +39,7 @@ import {parseArguments} from './args.js'; const version = readVersion(); const ports = parseArguments(); -const logDir = ports.executionDir || ports.resourcesDir; -if (logDir) { - logger.setLogFile(logDir); -} +configureLogDirectory(ports.executionDir); void (async () => { logger.info(`Starting BrowserOS Server v${version}`); @@ -252,13 +251,10 @@ async function startAgentServer( const llmConfig = await getLLMConfig(); - const resourcesDir = ports.resourcesDir || process.cwd(); - const executionDir = ports.executionDir || resourcesDir; - const agentConfig: AgentServerConfig = { port: ports.agentPort, - resourcesDir, - executionDir, + resourcesDir: ports.resourcesDir, + executionDir: ports.executionDir, mcpServerPort: ports.httpMcpPort, apiKey: llmConfig.apiKey, baseUrl: llmConfig.baseUrl, @@ -309,3 +305,20 @@ function createShutdownHandler( process.exit(0); }; } + +function configureLogDirectory(logDirCandidate: string): void { + const resolvedDir = path.isAbsolute(logDirCandidate) + ? logDirCandidate + : path.resolve(process.cwd(), logDirCandidate); + + try { + fs.mkdirSync(resolvedDir, {recursive: true}); + logger.setLogFile(resolvedDir); + } catch (error) { + console.warn( + `Failed to configure log directory ${resolvedDir}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } +}