Docs/Skills and code/skill video-visual-production
.claude/skills/video-visual-production/scripts/lib/cdp.js
What the file says about itself
Minimal Chrome DevTools Protocol client over Node's built-in WebSocket (Node 22+). No dependencies.
// Minimal Chrome DevTools Protocol client over Node's built-in WebSocket (Node 22+). No dependencies.
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');
const CHROME_CANDIDATES = [
process.env.CHROME_PATH,
'C:/Program Files/Google/Chrome/Application/chrome.exe',
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
].filter(Boolean);
function findChrome() {
for (const p of CHROME_CANDIDATES) if (fs.existsSync(p)) return p;
throw new Error('No Chrome or Edge found; set CHROME_PATH');
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function launch(port = 9333) {
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'vvp-chrome-'));
const proc = spawn(findChrome(), [
'--headless=new', `--remote-debugging-port=${port}`, `--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check',
'--hide-scrollbars', '--disable-extensions', '--mute-audio', '--window-size=1280,900', 'about:blank',
], { stdio: 'ignore', windowsHide: true });
let target = null;
for (let i = 0; i < 60 && !target; i++) {
await sleep(250);
try {
const list = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
target = list.find((t) => t.type === 'page');
} catch (_) { /* not up yet */ }
}
if (!target) { proc.kill(); throw new Error('Chrome did not expose a page target'); }
const ws = new WebSocket(target.webSocketDebuggerUrl);
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = () => rej(new Error('websocket error')); });
let id = 0;
const pending = new Map();
const listeners = [];
ws.onmessage = (ev) => {
const m = JSON.parse(ev.data);
if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id); m.error ? rej(new Error(m.error.message)) : res(m.result); }
else if (m.method) listeners.forEach((l) => l(m));
};
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
const waitEvent = (name, ms = 30000) => new Promise((res, rej) => {
const t = setTimeout(() => rej(new Error('timeout waiting for ' + name)), ms);
const l = (m) => { if (m.method === name) { clearTimeout(t); listeners.splice(listeners.indexOf(l), 1); res(m.params); } };
listeners.push(l);
});
const close = async () => {
try { ws.close(); } catch (_) { /* ignore */ }
proc.kill();
await sleep(500);
try { fs.rmSync(profile, { recursive: true, force: true }); } catch (_) { /* profile may be locked briefly */ }
};
return { send, waitEvent, close };
}
module.exports = { launch, findChrome, sleep };
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.