Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Fetch any URL via Chrome CDP and convert the rendered page to clean markdown with YouTube transcript support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/lib/browser/cdp-client.ts
1import { EventEmitter } from "node:events";2import WebSocket from "ws";34type JsonObject = Record<string, unknown>;56interface CdpPendingCommand {7resolve(value: unknown): void;8reject(error: unknown): void;9method: string;10}1112interface CdpErrorShape {13message?: string;14}1516interface CdpCommandResult<T> {17result?: T;18error?: CdpErrorShape;19}2021interface CreatePageSessionOptions {22initialUrl?: string;23visible?: boolean;24}2526export class TargetSession extends EventEmitter {27constructor(28private readonly client: CdpClient,29public readonly targetId: string,30public readonly sessionId: string,31) {32super();33}3435async send<T>(method: string, params: JsonObject = {}): Promise<T> {36return this.client.sendSessionCommand<T>(this.sessionId, method, params);37}3839handleEvent(method: string, params: JsonObject): void {40this.emit(method, params);41this.emit("event", { method, params });42}4344async waitForEvent<T extends JsonObject>(45method: string,46predicate?: (params: T) => boolean,47timeoutMs = 30_000,48): Promise<T> {49return new Promise<T>((resolve, reject) => {50const timeout = setTimeout(() => {51this.off(method, listener);52reject(new Error(`Timed out waiting for ${method}`));53}, timeoutMs);5455const listener = (params: T): void => {56if (predicate && !predicate(params)) {57return;58}59clearTimeout(timeout);60this.off(method, listener);61resolve(params);62};6364this.on(method, listener);65});66}67}6869export class CdpClient {70private readonly ws: WebSocket;71private readonly pending = new Map<number, CdpPendingCommand>();72private readonly sessions = new Map<string, TargetSession>();73private nextId = 1;7475private constructor(ws: WebSocket) {76this.ws = ws;77this.ws.on("message", (raw) => {78this.handleMessage(raw.toString());79});80}8182static async connect(browserWsUrl: string): Promise<CdpClient> {83const ws = await new Promise<WebSocket>((resolve, reject) => {84const socket = new WebSocket(browserWsUrl);85socket.once("open", () => resolve(socket));86socket.once("error", (error) => reject(error));87});8889return new CdpClient(ws);90}9192private handleMessage(rawMessage: string): void {93const message = JSON.parse(rawMessage) as {94id?: number;95sessionId?: string;96method?: string;97params?: JsonObject;98result?: unknown;99error?: CdpErrorShape;100};101102if (typeof message.id === "number") {103const pending = this.pending.get(message.id);104if (!pending) {105return;106}107this.pending.delete(message.id);108if (message.error) {109pending.reject(new Error(`${pending.method}: ${message.error.message ?? "Unknown CDP error"}`));110return;111}112pending.resolve(message.result);113return;114}115116if (typeof message.sessionId === "string" && typeof message.method === "string") {117const session = this.sessions.get(message.sessionId);118if (session) {119session.handleEvent(message.method, (message.params ?? {}) as JsonObject);120}121}122}123124private async sendCommand<T>(125method: string,126params: JsonObject = {},127sessionId?: string,128): Promise<T> {129const id = this.nextId;130this.nextId += 1;131132const payload = sessionId ? { id, method, params, sessionId } : { id, method, params };133134const result = new Promise<T>((resolve, reject) => {135this.pending.set(id, {136resolve: (value) => resolve(value as T),137reject,138method,139});140});141142this.ws.send(JSON.stringify(payload));143return result;144}145146async sendBrowserCommand<T>(method: string, params: JsonObject = {}): Promise<T> {147return this.sendCommand<T>(method, params);148}149150async sendSessionCommand<T>(sessionId: string, method: string, params: JsonObject = {}): Promise<T> {151return this.sendCommand<T>(method, params, sessionId);152}153154private async createPageTarget(initialUrl: string, visible = false): Promise<{ targetId: string }> {155const attempts: JsonObject[] = visible156? [157{158url: initialUrl,159newWindow: true,160focus: true,161},162{163url: initialUrl,164focus: true,165},166{167url: initialUrl,168},169]170: [171{172url: initialUrl,173hidden: true,174},175{176url: initialUrl,177background: true,178focus: false,179},180{181url: initialUrl,182},183];184185let lastError: unknown;186187for (const params of attempts) {188try {189return await this.sendBrowserCommand<{ targetId: string }>("Target.createTarget", params);190} catch (error) {191lastError = error;192}193}194195throw lastError instanceof Error ? lastError : new Error("Target.createTarget failed");196}197198async createPageSession(options: CreatePageSessionOptions = {}): Promise<TargetSession> {199const initialUrl = options.initialUrl ?? "about:blank";200const created = await this.createPageTarget(initialUrl, Boolean(options.visible));201const attached = await this.sendBrowserCommand<{ sessionId: string }>("Target.attachToTarget", {202targetId: created.targetId,203flatten: true,204});205206const session = new TargetSession(this, created.targetId, attached.sessionId);207this.sessions.set(attached.sessionId, session);208209if (options.visible) {210await this.sendBrowserCommand("Target.activateTarget", {211targetId: created.targetId,212}).catch(() => {});213}214215await session.send("Page.enable");216await session.send("Runtime.enable");217await session.send("DOM.enable");218219if (options.visible) {220await session.send("Page.bringToFront").catch(() => {});221}222223return session;224}225226async closeTarget(targetId: string): Promise<void> {227try {228await this.sendBrowserCommand("Target.closeTarget", { targetId });229} catch {230// Target may already be gone.231}232}233234async close(): Promise<void> {235await new Promise<void>((resolve) => {236if (this.ws.readyState === WebSocket.CLOSED) {237resolve();238return;239}240this.ws.once("close", () => resolve());241this.ws.close();242});243}244}245246export async function evaluateRuntime<T>(session: TargetSession, expression: string): Promise<T> {247const response = await session.send<CdpCommandResult<{ value?: T; description?: string }>>("Runtime.evaluate", {248expression,249awaitPromise: true,250returnByValue: true,251});252253if (response.error) {254throw new Error(response.error.message ?? "Runtime.evaluate failed");255}256257return (response.result?.value as T | undefined) ?? (undefined as T);258}259