From 0b4059dda9a34ddd49bd726d69abc52b4bc4e897 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:08:58 +0000 Subject: [PATCH 1/2] Add CDP connection manager for direct Chrome DevTools Protocol support - Implement CDPConnectionManager class with WebSocket-based CDP communication - Add launchLocal() method to launch Chrome locally with chrome-launcher - Add connect() method to connect to remote CDP endpoints via WebSocket - Add createSession() method for CDP session management with Target domain - Add sendCommand() method with proper error handling and timeouts - Implement CDP Target domain methods: listTargets, createTarget, attachToTarget, closeTarget - Add reconnection logic for dropped connections - Add comprehensive error handling and [CDP] prefixed logging - Add ws, chrome-launcher, and @types/ws dependencies This is foundational infrastructure for replacing Playwright's browser launching and connection handling. Co-Authored-By: devin@hyperbrowser.ai --- package.json | 3 + src/cdp/connection-manager.ts | 461 ++++++++++++++++++++++++++++++++++ yarn.lock | 58 ++++- 3 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 src/cdp/connection-manager.ts diff --git a/package.json b/package.json index c789e48..33c3bfc 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@types/crypto-js": "^4.2.2", "boxen": "5.1.2", "chalk": "4.1.2", + "chrome-launcher": "^1.2.1", "commander": "^13.1.0", "crypto-js": "^4.2.0", "dotenv": "^16.4.5", @@ -58,12 +59,14 @@ "playwright-core": "^1.56.1", "readline": "^1.3.0", "turndown": "^7.2.0", + "ws": "^8.18.3", "zod": "^4.1.8" }, "devDependencies": { "@types/lodash": "^4.17.16", "@types/node": "^22.9.1", "@types/turndown": "^5.0.5", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "axios": "^1.8.4", diff --git a/src/cdp/connection-manager.ts b/src/cdp/connection-manager.ts new file mode 100644 index 0000000..e8cb790 --- /dev/null +++ b/src/cdp/connection-manager.ts @@ -0,0 +1,461 @@ +import WebSocket from "ws"; +import { EventEmitter } from "events"; +import * as chromeLauncher from "chrome-launcher"; +import { Protocol } from "devtools-protocol"; + +export interface CDPSession { + sessionId: string; + targetId: string; + ws: WebSocket; + messageId: number; + pendingCommands: Map void; + reject: (error: Error) => void; + method: string; + timeout: NodeJS.Timeout; + }>; +} + +export interface LaunchOptions { + headless?: boolean; + port?: number; + chromeFlags?: string[]; + chromePath?: string; + userDataDir?: string; +} + +export interface ConnectionOptions { + timeout?: number; + reconnectAttempts?: number; + reconnectDelay?: number; +} + +export type ConnectionStatus = "connected" | "disconnected" | "error" | "reconnecting"; + +export class CDPConnectionManager extends EventEmitter { + private chromeProcess: chromeLauncher.LaunchedChrome | null = null; + private browserWs: WebSocket | null = null; + private sessions: Map = new Map(); + private browserSession: CDPSession | null = null; + private browserEndpoint: string | null = null; + private connectionStatus: ConnectionStatus = "disconnected"; + private reconnectAttempts: number = 0; + private maxReconnectAttempts: number = 3; + private reconnectDelay: number = 1000; + private commandTimeout: number = 30000; + private browserMessageId: number = 0; + + constructor(options?: ConnectionOptions) { + super(); + if (options?.reconnectAttempts !== undefined) { + this.maxReconnectAttempts = options.reconnectAttempts; + } + if (options?.reconnectDelay !== undefined) { + this.reconnectDelay = options.reconnectDelay; + } + if (options?.timeout !== undefined) { + this.commandTimeout = options.timeout; + } + } + + async launchLocal(options: LaunchOptions = {}): Promise { + console.log("[CDP] Launching local Chrome instance..."); + + try { + const chromeFlags = [ + "--disable-blink-features=AutomationControlled", + "--no-first-run", + "--no-default-browser-check", + ...(options.chromeFlags || []) + ]; + + const launchOptions: chromeLauncher.Options = { + chromeFlags, + port: options.port, + chromePath: options.chromePath, + userDataDir: options.userDataDir, + logLevel: "error" + }; + + this.chromeProcess = await chromeLauncher.launch(launchOptions); + + const port = this.chromeProcess.port; + this.browserEndpoint = `http://localhost:${port}`; + + console.log(`[CDP] Chrome launched successfully on port ${port}`); + console.log(`[CDP] Debug endpoint: ${this.browserEndpoint}`); + + const versionUrl = `${this.browserEndpoint}/json/version`; + let wsEndpoint: string | null = null; + let retries = 10; + + while (retries > 0 && !wsEndpoint) { + try { + const response = await fetch(versionUrl); + const versionInfo = await response.json(); + wsEndpoint = versionInfo.webSocketDebuggerUrl; + } catch (error) { + retries--; + if (retries === 0) { + throw error; + } + console.log(`[CDP] Waiting for Chrome to be ready... (${retries} retries left)`); + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + if (!wsEndpoint) { + throw new Error("Failed to get WebSocket endpoint from Chrome"); + } + + console.log(`[CDP] WebSocket endpoint: ${wsEndpoint}`); + + return wsEndpoint; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[CDP] Failed to launch Chrome: ${errorMessage}`); + throw new Error(`Failed to launch Chrome: ${errorMessage}`); + } + } + + async connect(endpoint: string): Promise { + console.log(`[CDP] Connecting to endpoint: ${endpoint}`); + + return new Promise((resolve, reject) => { + try { + this.browserWs = new WebSocket(endpoint); + + const connectionTimeout = setTimeout(() => { + if (this.browserWs) { + this.browserWs.close(); + } + reject(new Error("Connection timeout")); + }, this.commandTimeout); + + this.browserWs.on("open", () => { + clearTimeout(connectionTimeout); + this.connectionStatus = "connected"; + this.reconnectAttempts = 0; + + this.browserSession = { + sessionId: "browser", + targetId: "browser", + ws: this.browserWs!, + messageId: 0, + pendingCommands: new Map() + }; + this.sessions.set("browser", this.browserSession); + + console.log("[CDP] Connected successfully"); + this.emit("connected"); + resolve(); + }); + + this.browserWs.on("message", (data: WebSocket.Data) => { + this.handleBrowserMessage(data); + }); + + this.browserWs.on("error", (error: Error) => { + clearTimeout(connectionTimeout); + console.error(`[CDP] WebSocket error: ${error.message}`); + this.connectionStatus = "error"; + this.emit("error", error); + reject(error); + }); + + this.browserWs.on("close", () => { + console.log("[CDP] Connection closed"); + this.connectionStatus = "disconnected"; + this.emit("disconnected"); + this.handleDisconnection(); + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[CDP] Connection failed: ${errorMessage}`); + reject(new Error(`Connection failed: ${errorMessage}`)); + } + }); + } + + private handleBrowserMessage(data: WebSocket.Data): void { + try { + const message = JSON.parse(data.toString()); + + if (message.id !== undefined) { + const session = Array.from(this.sessions.values()).find(s => + s.pendingCommands.has(message.id) + ); + + if (session) { + const pending = session.pendingCommands.get(message.id); + if (pending) { + clearTimeout(pending.timeout); + session.pendingCommands.delete(message.id); + + if (message.error) { + console.error(`[CDP] Command error: ${pending.method}`, message.error); + pending.reject(new Error(`CDP Error: ${message.error.message || JSON.stringify(message.error)}`)); + } else { + pending.resolve(message.result); + } + } + } + } + } catch (error) { + console.error("[CDP] Failed to parse message:", error); + } + } + + private async handleDisconnection(): Promise { + if (this.reconnectAttempts < this.maxReconnectAttempts && this.browserEndpoint) { + this.reconnectAttempts++; + this.connectionStatus = "reconnecting"; + console.log(`[CDP] Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); + + await new Promise(resolve => setTimeout(resolve, this.reconnectDelay)); + + try { + const versionUrl = `${this.browserEndpoint}/json/version`; + const response = await fetch(versionUrl); + const versionInfo = await response.json(); + const wsEndpoint = versionInfo.webSocketDebuggerUrl; + await this.connect(wsEndpoint); + } catch { + console.error(`[CDP] Reconnection attempt ${this.reconnectAttempts} failed`); + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error("[CDP] Max reconnection attempts reached"); + this.emit("error", new Error("Max reconnection attempts reached")); + } + } + } + } + + async createSession(targetId?: string): Promise { + if (!this.browserWs || this.connectionStatus !== "connected") { + throw new Error("Not connected to browser"); + } + + let actualTargetId = targetId; + + if (!actualTargetId) { + console.log("[CDP] No target ID provided, creating new page target..."); + const newTarget = await this.createTarget("about:blank"); + actualTargetId = newTarget.targetId; + } + + console.log(`[CDP] Creating session for target: ${actualTargetId}`); + + const attachResult = await this.sendBrowserCommand("Target.attachToTarget", { + targetId: actualTargetId, + flatten: true + }); + + const sessionId = attachResult.sessionId; + + const session: CDPSession = { + sessionId, + targetId: actualTargetId, + ws: this.browserWs, + messageId: 0, + pendingCommands: new Map() + }; + + this.sessions.set(sessionId, session); + console.log(`[CDP] Session created: ${sessionId}`); + + return session; + } + + private async sendBrowserCommand(method: string, params?: Record): Promise { + if (!this.browserWs || this.connectionStatus !== "connected" || !this.browserSession) { + throw new Error("Not connected to browser"); + } + + return new Promise((resolve, reject) => { + this.browserMessageId++; + const id = this.browserMessageId; + + const message = { + id, + method, + params: params || {} + }; + + const timeout = setTimeout(() => { + this.browserSession!.pendingCommands.delete(id); + reject(new Error(`Command timeout: ${method}`)); + }, this.commandTimeout); + + this.browserSession!.pendingCommands.set(id, { + resolve, + reject, + method, + timeout + }); + + this.browserWs!.send(JSON.stringify(message), (error) => { + if (error) { + clearTimeout(timeout); + this.browserSession!.pendingCommands.delete(id); + reject(new Error(`Failed to send command: ${error.message}`)); + } + }); + }); + } + + async sendCommand(session: CDPSession, method: string, params?: Record): Promise { + if (!session.ws || this.connectionStatus !== "connected") { + throw new Error("Session not connected"); + } + + console.log(`[CDP] Sending command: ${method}`); + + return new Promise((resolve, reject) => { + session.messageId++; + const id = session.messageId; + + const message = { + id, + method, + params: params || {}, + sessionId: session.sessionId + }; + + const timeout = setTimeout(() => { + session.pendingCommands.delete(id); + reject(new Error(`Command timeout: ${method}`)); + }, this.commandTimeout); + + session.pendingCommands.set(id, { + resolve, + reject, + method, + timeout + }); + + session.ws.send(JSON.stringify(message), (error) => { + if (error) { + clearTimeout(timeout); + session.pendingCommands.delete(id); + console.error(`[CDP] Failed to send command ${method}: ${error.message}`); + reject(new Error(`Failed to send command: ${error.message}`)); + } + }); + }); + } + + async listTargets(): Promise { + console.log("[CDP] Listing targets..."); + + try { + const result = await this.sendBrowserCommand("Target.getTargets"); + const targets = result.targetInfos || []; + console.log(`[CDP] Found ${targets.length} targets`); + return targets; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[CDP] Failed to list targets: ${errorMessage}`); + throw new Error(`Failed to list targets: ${errorMessage}`); + } + } + + async createTarget(url: string): Promise { + console.log(`[CDP] Creating new target with URL: ${url}`); + + try { + const result = await this.sendBrowserCommand("Target.createTarget", { + url + }); + console.log(`[CDP] Target created: ${result.targetId}`); + return result; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[CDP] Failed to create target: ${errorMessage}`); + throw new Error(`Failed to create target: ${errorMessage}`); + } + } + + async attachToTarget(targetId: string): Promise { + console.log(`[CDP] Attaching to target: ${targetId}`); + + try { + return await this.createSession(targetId); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[CDP] Failed to attach to target: ${errorMessage}`); + throw new Error(`Failed to attach to target: ${errorMessage}`); + } + } + + async closeTarget(targetId: string): Promise { + console.log(`[CDP] Closing target: ${targetId}`); + + try { + await this.sendBrowserCommand("Target.closeTarget", { targetId }); + + const sessionsToRemove = Array.from(this.sessions.entries()) + .filter(([, session]) => session.targetId === targetId) + .map(([sessionId]) => sessionId); + + for (const sessionId of sessionsToRemove) { + this.sessions.delete(sessionId); + } + + console.log(`[CDP] Target closed: ${targetId}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[CDP] Failed to close target: ${errorMessage}`); + throw new Error(`Failed to close target: ${errorMessage}`); + } + } + + async close(): Promise { + console.log("[CDP] Closing CDP connection manager..."); + + for (const [, session] of this.sessions.entries()) { + for (const [, pending] of session.pendingCommands.entries()) { + clearTimeout(pending.timeout); + pending.reject(new Error("Connection closed")); + } + session.pendingCommands.clear(); + } + + this.sessions.clear(); + + if (this.browserWs) { + this.browserWs.removeAllListeners(); + if (this.browserWs.readyState === WebSocket.OPEN) { + this.browserWs.close(); + } + this.browserWs = null; + } + + if (this.chromeProcess) { + try { + await this.chromeProcess.kill(); + console.log("[CDP] Chrome process terminated"); + } catch (error) { + console.error("[CDP] Error killing Chrome process:", error); + } + this.chromeProcess = null; + } + + this.connectionStatus = "disconnected"; + this.browserEndpoint = null; + this.reconnectAttempts = 0; + + console.log("[CDP] Connection manager closed"); + } + + getStatus(): ConnectionStatus { + return this.connectionStatus; + } + + getSession(sessionId: string): CDPSession | undefined { + return this.sessions.get(sessionId); + } + + getAllSessions(): CDPSession[] { + return Array.from(this.sessions.values()); + } +} diff --git a/yarn.lock b/yarn.lock index 92516cc..467aa31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -802,6 +802,13 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA== +"@types/node@*": + version "24.10.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.0.tgz#6b79086b0dfc54e775a34ba8114dcc4e0221f31f" + integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A== + dependencies: + undici-types "~7.16.0" + "@types/node@16.9.1": version "16.9.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" @@ -819,6 +826,13 @@ resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.6.tgz#42a27397298a312d6088f29c0ff4819c518c1ecb" integrity sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg== +"@types/ws@^8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^8.15.0": version "8.46.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz#dc4ab93ee3d7e6c8e38820a0d6c7c93c7183e2dc" @@ -1205,6 +1219,16 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chrome-launcher@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-1.2.1.tgz#a84877c123192bdadb40dc274b74caf164e98032" + integrity sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^2.0.1" + cli-boxes@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" @@ -1327,7 +1351,7 @@ data-uri-to-buffer@^4.0.0: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== -debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0: +debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2074,6 +2098,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -2111,6 +2140,13 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2242,6 +2278,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lighthouse-logger@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz#c0b39daee22035ce28551f3503c5935d0b5e1bf3" + integrity sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg== + dependencies: + debug "^4.4.1" + marky "^1.2.2" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -2277,6 +2321,11 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +marky@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/marky/-/marky-1.3.0.tgz#422b63b0baf65022f02eda61a238eccdbbc14997" + integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" @@ -3122,6 +3171,11 @@ undici-types@~6.21.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -3246,7 +3300,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@^8.18.0: +ws@^8.18.0, ws@^8.18.3: version "8.18.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== From 8e5bbd255709c9df0b4dffbfd39c06765abdc724 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:11:58 +0000 Subject: [PATCH 2/2] Fix TypeScript type errors in CDP connection manager - Add type assertions for sendBrowserCommand return values - Cast attachResult to { sessionId: string } - Cast Target.getTargets result to { targetInfos: Protocol.Target.TargetInfo[] } - Cast Target.createTarget result to Protocol.Target.CreateTargetResponse - Build now passes successfully Co-Authored-By: devin@hyperbrowser.ai --- src/cdp/connection-manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cdp/connection-manager.ts b/src/cdp/connection-manager.ts index e8cb790..52035fc 100644 --- a/src/cdp/connection-manager.ts +++ b/src/cdp/connection-manager.ts @@ -248,7 +248,7 @@ export class CDPConnectionManager extends EventEmitter { const attachResult = await this.sendBrowserCommand("Target.attachToTarget", { targetId: actualTargetId, flatten: true - }); + }) as { sessionId: string }; const sessionId = attachResult.sessionId; @@ -348,7 +348,7 @@ export class CDPConnectionManager extends EventEmitter { console.log("[CDP] Listing targets..."); try { - const result = await this.sendBrowserCommand("Target.getTargets"); + const result = await this.sendBrowserCommand("Target.getTargets") as { targetInfos: Protocol.Target.TargetInfo[] }; const targets = result.targetInfos || []; console.log(`[CDP] Found ${targets.length} targets`); return targets; @@ -365,7 +365,7 @@ export class CDPConnectionManager extends EventEmitter { try { const result = await this.sendBrowserCommand("Target.createTarget", { url - }); + }) as Protocol.Target.CreateTargetResponse; console.log(`[CDP] Target created: ${result.targetId}`); return result; } catch (error) {