#!/usr/bin/env node /** * Load the local web host in the headless browser, capture console + exceptions, * then dump the resulting page state and a screenshot. * * node host/cdp-load.mjs --port 9333 --url http://127.0.0.1:8095/ --out /tmp/shot.png */ import fs from "node:fs"; const argv = process.argv.slice(2); const argOf = (name, fallback) => { const i = argv.indexOf(name); return i !== -1 && argv[i + 1] ? argv[i + 1] : fallback; }; const PORT = argOf("--port", "9333"); const URL_ = argOf("--url", "http://127.0.0.1:8095/"); const OUT = argOf("--out", "/tmp/codexweb.png"); const SETTLE = Number(argOf("--settle", "6000")); const version = await (await fetch(`http://127.0.0.1:${PORT}/json/version`)).json(); const ws = new WebSocket(version.webSocketDebuggerUrl); let seq = 0; const pending = new Map(); const events = []; const send = (method, params = {}, sessionId) => new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject }); const msg = { id, method, params }; if (sessionId) msg.sessionId = sessionId; ws.send(JSON.stringify(msg)); }); ws.addEventListener("message", (ev) => { const msg = JSON.parse(ev.data); if (msg.method) { if (msg.method === "Runtime.consoleAPICalled") { const text = (msg.params.args || []) .map((a) => a.value ?? a.description ?? a.unserializableValue ?? "") .join(" ") .slice(0, 600); events.push(`[${msg.params.type}] ${text}`); } else if (msg.method === "Runtime.exceptionThrown") { const d = msg.params.exceptionDetails; events.push(`[EXCEPTION] ${d.exception?.description ?? d.text}`.slice(0, 2200)); } else if (msg.method === "Log.entryAdded") { const e = msg.params.entry; events.push(`[log:${e.level}] ${e.text}`.slice(0, 500)); } } if (!msg.id || !pending.has(msg.id)) return; const { resolve, reject } = pending.get(msg.id); pending.delete(msg.id); msg.error ? reject(new Error(JSON.stringify(msg.error).slice(0, 400))) : resolve(msg.result); }); await new Promise((r) => ws.addEventListener("open", r, { once: true })); const { targetInfos } = await send("Target.getTargets"); const pages = targetInfos.filter((t) => t.type === "page"); const main = pages.find((t) => t.url.includes("127.0.0.1")) ?? pages[0]; if (!main) { console.error("no page target"); process.exit(1); } const { sessionId } = await send("Target.attachToTarget", { targetId: main.targetId, flatten: true }); await send("Page.enable", {}, sessionId); await send("Runtime.enable", {}, sessionId); await send("Log.enable", {}, sessionId); await send("Page.navigate", { url: URL_ }, sessionId); // Runtime.enable replays the previous page's console buffer; drop it so the // report only covers this load. events.length = 0; await new Promise((r) => setTimeout(r, SETTLE)); const state = await send( "Runtime.evaluate", { expression: `(() => { const root = document.querySelector('#root'); const text = (root?.innerText || '').replace(/\\s+/g, ' ').trim(); return JSON.stringify({ url: location.href, title: document.title, rootChildren: root ? root.children.length : -1, rootBytes: root ? root.innerHTML.length : 0, textLength: text.length, textSample: text.slice(0, 500), bootstrapped: !!globalThis.__codexBootstrapped, hasShim: typeof globalThis.__codexHost, }, null, 1); })()`, returnByValue: true, }, sessionId, ); console.log("STATE:", state.result.value); console.log(`\nEVENTS (${events.length}):`); for (const e of events.slice(0, 60)) console.log(" " + e); const shot = await send("Page.captureScreenshot", { format: "png" }, sessionId); fs.writeFileSync(OUT, Buffer.from(shot.data, "base64")); console.log(`\nscreenshot: ${OUT} (${fs.statSync(OUT).size} bytes)`); ws.close();