/* * Veskslop — minimal Chrome DevTools Protocol client. * * Connects to the Discord page target exposed by Vesktop's * --remote-debugging-port, evaluates expressions in the page, and receives * pushed events through a CDP binding. This is the entire integration surface: * Vesktop itself is unmodified. */ import WebSocket from "ws"; export type CdpListener = (event: any) => void; interface PendingRequest { resolve: (value: any) => void; reject: (error: Error) => void; } export class CdpClient { private ws!: WebSocket; private nextId = 0; private readonly pending = new Map(); private readonly listeners = new Set(); static async connect(cdpUrl = process.env.CDP_URL ?? "http://127.0.0.1:9222"): Promise { const targets = (await (await fetch(`${cdpUrl}/json`)).json()) as any[]; const page = targets.find(target => target.type === "page" && target.url.includes("discord.com")); if (!page) throw new Error(`No Discord page target at ${cdpUrl}`); const client = new CdpClient(); client.ws = new WebSocket(page.webSocketDebuggerUrl); await new Promise((resolve, reject) => { client.ws.once("open", resolve); client.ws.once("error", reject); }); client.ws.on("message", (raw: WebSocket.RawData) => client.onMessage(raw)); await client.send("Runtime.enable"); await client.send("Runtime.addBinding", { name: "veskslopEmit" }); return client; } private onMessage(raw: WebSocket.RawData) { const message = JSON.parse(raw.toString()); if (message.id && this.pending.has(message.id)) { const request = this.pending.get(message.id)!; this.pending.delete(message.id); if (message.error) request.reject(new Error(message.error.message)); else request.resolve(message.result); return; } if (message.method === "Runtime.bindingCalled" && message.params?.name === "veskslopEmit") { try { const event = JSON.parse(message.params.payload); for (const listener of this.listeners) listener(event); } catch { // ignore malformed payloads } } } send(method: string, params?: unknown): Promise { const id = ++this.nextId; return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); this.ws.send(JSON.stringify({ id, method, params })); }); } async evaluate(expression: string): Promise { const result = await this.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }); if (result.exceptionDetails) { const description = result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "evaluation failed"; throw new Error(description); } return result.result?.value as T; } onEvent(listener: CdpListener): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } close() { this.ws.close(); } }