type ToolResult = { content: Array<{ type: string; text?: string; source?: { type: string; media_type: string; data: string } }>; isError?: boolean; }; type Params = { action: string; tab_id?: string; url?: string; expression?: string; }; type TabInfo = { id: string; title: string; url: string; }; const RELAY_AUTH_HEADER = 'x-officer-relay-token'; function getConfig(): { port: number; token: string } | null { const port = process.env.OFFICER_BROWSER_RELAY_PORT; const token = process.env.OFFICER_BROWSER_RELAY_TOKEN; if (!port || !token) return null; return { port: Number(port), token }; } async function listTabs(port: number, token: string): Promise { const res = await fetch(`http://127.0.0.1:${port}/json/list`, { headers: { [RELAY_AUTH_HEADER]: token }, }); if (!res.ok) throw new Error(`Failed to list tabs (${res.status})`); const tabs = (await res.json()) as Array<{ id: string; title: string; url: string }>; return tabs.map((t) => ({ id: t.id, title: t.title, url: t.url })); } async function resolveTab(port: number, token: string, tabId?: string): Promise { const tabs = await listTabs(port, token); if (tabs.length === 0) throw new Error('No browser tabs connected. The user needs to attach tabs via the Browser Relay extension.'); if (tabId) { const tab = tabs.find((t) => t.id === tabId); if (!tab) throw new Error(`Tab ${tabId} not found. Available tabs: ${tabs.map((t) => t.id).join(', ')}`); return tab; } return tabs[0]!; } async function sendCdpCommand(port: number, token: string, tabId: string, method: string, params?: unknown): Promise { const url = `ws://127.0.0.1:${port}/cdp?token=${encodeURIComponent(token)}`; return await new Promise((resolve, reject) => { const ws = new WebSocket(url); let settled = false; const timeout = setTimeout(() => { if (settled) return; settled = true; ws.close(); reject(new Error(`CDP command timeout: ${method}`)); }, 30_000); ws.addEventListener('open', () => { const cmd: Record = { id: 1, method }; if (params) cmd.params = params; cmd.sessionId = tabId; ws.send(JSON.stringify(cmd)); }); ws.addEventListener('message', (event) => { if (settled) return; try { const msg = JSON.parse(String(event.data)) as { id?: number; result?: unknown; error?: { message: string } }; if (msg.id === 1) { settled = true; clearTimeout(timeout); ws.close(); if (msg.error) reject(new Error(msg.error.message)); else resolve(msg.result); } } catch { // ignore parse errors, wait for correct message } }); ws.addEventListener('error', () => { if (settled) return; settled = true; clearTimeout(timeout); reject(new Error('CDP WebSocket connection failed — is the Browser Relay running?')); }); ws.addEventListener('close', () => { if (settled) return; settled = true; clearTimeout(timeout); reject(new Error('CDP WebSocket closed before response')); }); }); } // --- Actions --- async function actionListTabs(port: number, token: string): Promise { const tabs = await listTabs(port, token); if (tabs.length === 0) { return { content: [{ type: 'text', text: 'No browser tabs connected. The user needs to attach tabs via the Browser Relay extension.' }] }; } const lines = tabs.map((t, i) => `${i + 1}. ${t.title}\n URL: ${t.url}\n ID: ${t.id}`); return { content: [{ type: 'text', text: `Connected tabs (${tabs.length}):\n\n${lines.join('\n\n')}` }] }; } async function actionScreenshot(port: number, token: string, tabId?: string): Promise { const tab = await resolveTab(port, token, tabId); const result = (await sendCdpCommand(port, token, tab.id, 'Page.captureScreenshot', { format: 'png' })) as { data: string }; return { content: [ { type: 'text', text: `Screenshot of "${tab.title}" (${tab.url})` }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: result.data } }, ], }; } async function actionNavigate(port: number, token: string, url: string, tabId?: string): Promise { const tab = await resolveTab(port, token, tabId); await sendCdpCommand(port, token, tab.id, 'Page.navigate', { url }); return { content: [{ type: 'text', text: `Navigated tab "${tab.title}" to ${url}` }] }; } async function actionEvaluate(port: number, token: string, expression: string, tabId?: string): Promise { const tab = await resolveTab(port, token, tabId); const result = (await sendCdpCommand(port, token, tab.id, 'Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true, })) as { result?: { value?: unknown; description?: string }; exceptionDetails?: { text?: string } }; if (result.exceptionDetails) { return { content: [{ type: 'text', text: `Error evaluating JS: ${result.exceptionDetails.text ?? 'Evaluation failed'}` }], isError: true }; } const value = result.result?.value; const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2); return { content: [{ type: 'text', text: `Result from "${tab.title}":\n${text}` }] }; } async function actionPageInfo(port: number, token: string, tabId?: string): Promise { const tab = await resolveTab(port, token, tabId); const result = (await sendCdpCommand(port, token, tab.id, 'Runtime.evaluate', { expression: 'JSON.stringify({ title: document.title, url: location.href })', returnByValue: true, })) as { result?: { value?: string } }; let info: { title: string; url: string }; try { info = JSON.parse(result.result?.value ?? '{}'); } catch { info = { title: tab.title, url: tab.url }; } return { content: [{ type: 'text', text: `Title: ${info.title}\nURL: ${info.url}\nTab ID: ${tab.id}` }] }; } async function actionActivate(port: number, token: string, tabId?: string): Promise { const tab = await resolveTab(port, token, tabId); const res = await fetch(`http://127.0.0.1:${port}/json/activate/${encodeURIComponent(tab.id)}`, { headers: { [RELAY_AUTH_HEADER]: token }, }); if (!res.ok) throw new Error(`Failed to activate tab (${res.status})`); return { content: [{ type: 'text', text: `Activated tab "${tab.title}"` }] }; } async function actionClose(port: number, token: string, tabId?: string): Promise { const tab = await resolveTab(port, token, tabId); const res = await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(tab.id)}`, { headers: { [RELAY_AUTH_HEADER]: token }, }); if (!res.ok) throw new Error(`Failed to close tab (${res.status})`); return { content: [{ type: 'text', text: `Closed tab "${tab.title}"` }] }; } // --- Main --- export async function execute(_toolCallId: string, params: Params): Promise { const config = getConfig(); if (!config) { return { content: [{ type: 'text', text: 'Browser Relay is not available. The user needs to connect the Browser Relay extension in Settings → Integrations.' }], isError: true, }; } const { port, token } = config; const { action, tab_id, url, expression } = params; try { switch (action) { case 'list_tabs': return await actionListTabs(port, token); case 'screenshot': return await actionScreenshot(port, token, tab_id); case 'navigate': if (!url) return { content: [{ type: 'text', text: 'url is required for navigate action' }], isError: true }; return await actionNavigate(port, token, url, tab_id); case 'evaluate': if (!expression) return { content: [{ type: 'text', text: 'expression is required for evaluate action' }], isError: true }; return await actionEvaluate(port, token, expression, tab_id); case 'page_info': return await actionPageInfo(port, token, tab_id); case 'activate': return await actionActivate(port, token, tab_id); case 'close': return await actionClose(port, token, tab_id); default: return { content: [{ type: 'text', text: `Unknown action: ${action}. Use list_tabs, screenshot, navigate, evaluate, page_info, activate, or close.` }], isError: true, }; } } catch (err) { const message = err instanceof Error ? err.message : String(err); return { content: [{ type: 'text', text: `Browser error: ${message}` }], isError: true }; } }