// Minimal Chrome DevTools Protocol client (Node 22 global WebSocket, no npm deps). const fs = require('fs'); const path = require('path'); class CDP { constructor(wsUrl) { this.wsUrl = wsUrl; this.id = 0; this.pending = new Map(); this.closed = false; } connect(timeoutMs = 10000) { return new Promise((resolve, reject) => { const ws = new WebSocket(this.wsUrl); this.ws = ws; const timer = setTimeout(() => { try { ws.close(); } catch {} reject(new Error('connect timeout')); }, timeoutMs); ws.onopen = () => { clearTimeout(timer); resolve(this); }; ws.onerror = () => { clearTimeout(timer); reject(new Error('ws error')); }; ws.onclose = () => { this.closed = true; for (const [, p] of this.pending) p.rej(new Error('ws closed')); this.pending.clear(); }; ws.onmessage = (ev) => { let m; try { m = JSON.parse(ev.data); } catch { return; } if (m.id && this.pending.has(m.id)) { const p = this.pending.get(m.id); this.pending.delete(m.id); if (m.error) p.rej(new Error(m.error.message || 'cdp error')); else p.res(m.result); } }; }); } send(method, params = {}, timeoutMs = 15000) { if (this.closed) return Promise.reject(new Error('ws closed')); const id = ++this.id; return new Promise((res, rej) => { this.pending.set(id, { res, rej }); this.ws.send(JSON.stringify({ id, method, params })); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); rej(new Error('cdp timeout: ' + method)); } }, timeoutMs); }); } async eval(expression, timeoutMs = 15000) { const r = await this.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true, userGesture: true }, timeoutMs); if (r.exceptionDetails) { throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text || 'eval failed'); } return r.result ? r.result.value : undefined; } close() { try { this.ws.close(); } catch {} this.closed = true; } } async function listPages(port, urlMatch) { const res = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(5000) }); const all = await res.json(); return all.filter(t => t.type === 'page' && typeof t.url === 'string' && t.url.includes(urlMatch) && !t.url.startsWith('devtools://')); } const EXPR = fs.readFileSync(path.join(__dirname, 'probe-expression.js'), 'utf8'); module.exports = { CDP, listPages, EXPR };