type CdpCommandOptions = { relayPort: number; userToken: string; targetId?: string; }; async function sendCdpCommand(opts: CdpCommandOptions, method: string, params?: unknown): Promise { const { relayPort, userToken, targetId } = opts; const url = `ws://127.0.0.1:${relayPort}/cdp?token=${encodeURIComponent(userToken)}`; 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; if (targetId) cmd.sessionId = targetId; 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')); }); ws.addEventListener('close', () => { if (settled) return; settled = true; clearTimeout(timeout); reject(new Error('CDP WebSocket closed before response')); }); }); } export async function captureScreenshot( opts: CdpCommandOptions, format: 'png' | 'jpeg' = 'png', quality?: number, ): Promise { const params: Record = { format }; if (quality !== undefined) params.quality = quality; const result = (await sendCdpCommand(opts, 'Page.captureScreenshot', params)) as { data: string }; return result.data; } export async function evaluateJS(opts: CdpCommandOptions, expression: string): Promise { const result = (await sendCdpCommand(opts, 'Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true, })) as { result?: { value?: unknown; description?: string }; exceptionDetails?: { text?: string } }; if (result.exceptionDetails) { throw new Error(result.exceptionDetails.text ?? 'Evaluation failed'); } return result.result?.value; } export async function navigateTo(opts: CdpCommandOptions, url: string): Promise { await sendCdpCommand(opts, 'Page.navigate', { url }); } export async function getPageInfo(opts: CdpCommandOptions): Promise<{ title: string; url: string }> { const result = (await sendCdpCommand(opts, 'Runtime.evaluate', { expression: 'JSON.stringify({ title: document.title, url: location.href })', returnByValue: true, })) as { result?: { value?: string } }; try { return JSON.parse(result.result?.value ?? '{}'); } catch { return { title: '', url: '' }; } }