Something went wrong. Try again.
vibescrobble! every agent, live on stage. vibescrobble.com
atproto agent llm did
Something went wrong. Try again.
6.2 kB · 132 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133// What the dev generators share: web/ served as the site serves it, one// headless Chrome, and its DevTools protocol over a WebSocket. key.mjs and// brand/brand.mjs draw with it. Needs Node 22 or later, for its WebSocket.import { spawn } from "node:child_process";import { createServer } from "node:http";import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";import { tmpdir } from "node:os";import { extname, join, normalize, resolve } from "node:path";import { fileURLToPath } from "node:url";
export const WEB = resolve(fileURLToPath(new URL("..", import.meta.url)));const TYPES = { ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".webp": "image/webp", ".woff2": "font/woff2" };
/** web/, served as the site serves it, on a port of its own. */export function serve() { const server = createServer((request, response) => { const path = normalize(decodeURIComponent(new URL(request.url, "http://x").pathname)).replace(/^(\.\.[/\\])+/, ""); let file = join(WEB, path); if (!file.startsWith(WEB)) return response.writeHead(403).end(); try { if (statSync(file).isDirectory()) file = join(file, "index.html"); response.writeHead(200, { "content-type": TYPES[extname(file)] || "application/octet-stream" }).end(readFileSync(file)); } catch { response.writeHead(404).end(); } }); return new Promise((done) => server.listen(0, "127.0.0.1", () => done(server)));}
/** * One headless Chrome (set CHROME to pick one) on a profile of its own, so a * Chrome already open on this machine is left alone, and the address of its * DevTools. Its renderers share one process and a capped heap: the machine * these run on is shared. */function launch(profile) { const binary = process.env.CHROME || "google-chrome"; const child = spawn( binary, ["--headless=new", "--remote-debugging-port=0", `--user-data-dir=${profile}`, "--no-first-run", "--hide-scrollbars", "--disable-gpu", "--renderer-process-limit=1", "--js-flags=--max-old-space-size=1024", "about:blank"], { stdio: ["ignore", "ignore", "pipe"] }, ); return new Promise((done, fail) => { let said = ""; child.stderr.on("data", (chunk) => { said += chunk; const found = said.match(/DevTools listening on (ws:\/\/\S+)/); if (found) done({ child, url: found[1] }); }); child.on("exit", (code) => fail(new Error(`chrome exited (${code}): ${said.slice(-400)}`))); });}
/** The DevTools protocol over a WebSocket: `send(method, params, session)`. */function devtools(url) { const socket = new WebSocket(url); let next = 0; const waiting = new Map(); socket.addEventListener("message", ({ data }) => { const message = JSON.parse(data); const pending = waiting.get(message.id); if (!pending) return; waiting.delete(message.id); if (message.error) pending.fail(new Error(`${pending.method}: ${message.error.message}`)); else pending.done(message.result); }); const send = (method, params = {}, sessionId) => new Promise((done, fail) => { const id = (next += 1); waiting.set(id, { done, fail, method }); socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) })); }); return new Promise((done) => socket.addEventListener("open", () => done({ send, close: () => socket.close() })));}
/** * Runs `work(page)` with one tab in one headless Chrome and web/ served * beside it, and takes both down after, however `work` ends. `page`: * * - `page.base`: where web/ is served, `http://127.0.0.1:<port>`. * - `page.send(method, params)`: a DevTools call on the tab. * - `page.size(width, height, scale)`: the tab's viewport. * - `page.open(url)`: navigates. * - `page.run(expression)`: evaluates, awaits a promise, returns the value. * - `page.until(expression, what)`: polls until it is truthy, for 20 s. * - `page.shot(clip, format, quality)`: a screenshot as a Buffer. */export async function withPage(work) { const server = await serve(); const profile = mkdtempSync(join(tmpdir(), "vibescrobble-headless-")); const { child, url } = await launch(profile); try { const cdp = await devtools(url); const { targetId } = await cdp.send("Target.createTarget", { url: "about:blank" }); const { sessionId } = await cdp.send("Target.attachToTarget", { targetId, flatten: true }); const send = (method, params) => cdp.send(method, params, sessionId); await send("Page.enable"); const run = async (expression) => { const { result, exceptionDetails } = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }); if (exceptionDetails) throw new Error(`${expression.slice(0, 200)}: ${exceptionDetails.exception?.description || exceptionDetails.text}`); return result.value; }; const page = { base: `http://127.0.0.1:${server.address().port}`, send, run, size: (width, height, deviceScaleFactor = 1) => send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor, mobile: false }), open: (to) => send("Page.navigate", { url: to }), async until(expression, what, most = 20000) { const end = Date.now() + most; while (!(await run(expression).catch(() => false))) { if (Date.now() > end) throw new Error(`${what} never happened`); await new Promise((done) => setTimeout(done, 100)); } }, async shot(clip, format = "png", quality) { const { data } = await send("Page.captureScreenshot", { format, ...(quality ? { quality } : {}), ...(clip ? { clip: { ...clip, scale: 1 } } : {}), captureBeyondViewport: false }); return Buffer.from(data, "base64"); }, }; const result = await work(page); cdp.close(); return result; } finally { // Chrome lets go of its profile only once it has gone. const gone = new Promise((done) => child.on("exit", done)); child.kill(); await gone; server.close(); rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); }}