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/chrome-launcher.ts
1import { launch, type LaunchedChrome } from "chrome-launcher";2import WebSocket from "ws";3import type { Logger } from "../utils/logger";4import {5cleanChromeLockArtifacts,6ensureChromeProfileDir,7findChromeProcessUsingProfile,8findExistingChromeDebugPort,9hasChromeLockArtifacts,10listChromeProfileEntries,11resolveChromeProfileDir,12shouldRetryChromeLaunchRecovery,13} from "./profile";1415interface ChromeVersionResponse {16webSocketDebuggerUrl: string;17}1819export interface ChromeConnectOptions {20cdpUrl?: string;21browserPath?: string;22headless?: boolean;23logger?: Logger;24profileDir?: string;25}2627export interface ChromeConnection {28browserWsUrl: string;29origin?: string;30port?: number;31profileDir?: string;32launched: boolean;33close(): Promise<void>;34}3536async function fetchJson<T>(url: string): Promise<T> {37const response = await fetch(url);38if (!response.ok) {39throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);40}41return (await response.json()) as T;42}4344async function connectToHttpEndpoint(origin: string): Promise<ChromeConnection> {45const normalizedOrigin = origin.replace(/\/$/, "");46const version = await fetchJson<ChromeVersionResponse>(`${normalizedOrigin}/json/version`);47return {48browserWsUrl: version.webSocketDebuggerUrl,49origin: normalizedOrigin,50port: Number(new URL(normalizedOrigin).port || 80),51launched: false,52async close() {53// Reused external Chrome, nothing to close here.54},55};56}5758async function tryReuseChrome(profileDir: string, logger?: Logger): Promise<ChromeConnection | null> {59const port = await findExistingChromeDebugPort({ profileDir });60if (!port) {61return null;62}6364const origin = `http://127.0.0.1:${port}`;65try {66const connection = await connectToHttpEndpoint(origin);67logger?.info(`Reusing Chrome debugger at ${origin} for profile ${profileDir}`);68return {69...connection,70profileDir,71};72} catch {73// Debugger disappeared between detection and connect.74}75return null;76}7778async function launchFreshChrome(79profileDir: string,80options: Pick<ChromeConnectOptions, "browserPath" | "headless">,81): Promise<ChromeConnection> {82let launchedChrome: LaunchedChrome | null = null;83try {84launchedChrome = await launch({85chromePath: options.browserPath,86userDataDir: profileDir,87chromeFlags: [88"--disable-background-networking",89"--disable-default-apps",90"--disable-popup-blocking",91"--disable-sync",92"--no-first-run",93"--no-default-browser-check",94"--remote-allow-origins=*",95...(!options.headless ? ["--no-startup-window"] : []),96...(options.headless ? ["--headless=new"] : []),97],98});99100const origin = `http://127.0.0.1:${launchedChrome.port}`;101const version = await fetchJson<ChromeVersionResponse>(`${origin}/json/version`);102103const chrome = launchedChrome;104return {105browserWsUrl: version.webSocketDebuggerUrl,106origin,107port: launchedChrome.port,108profileDir,109launched: true,110async close() {111if (!chrome) return;112await gracefulCloseChrome(chrome, origin);113},114};115} catch (error) {116launchedChrome?.kill();117throw error;118}119}120121async function gracefulCloseChrome(chrome: LaunchedChrome, origin: string): Promise<void> {122try {123const resp = await fetch(`${origin}/json/version`);124const { webSocketDebuggerUrl } = (await resp.json()) as ChromeVersionResponse;125if (webSocketDebuggerUrl) {126const ws = await new Promise<WebSocket>((resolve, reject) => {127const socket = new WebSocket(webSocketDebuggerUrl);128socket.once("open", () => resolve(socket));129socket.once("error", reject);130});131const id = 1;132ws.send(JSON.stringify({ id, method: "Browser.close" }));133await new Promise<void>((resolve) => {134const timer = setTimeout(() => { ws.close(); resolve(); }, 5_000);135ws.once("close", () => { clearTimeout(timer); resolve(); });136});137const exited = await new Promise<boolean>((resolve) => {138if (chrome.pid && !isProcessAlive(chrome.pid)) { resolve(true); return; }139const timer = setTimeout(() => resolve(false), 3_000);140chrome.process.once("exit", () => { clearTimeout(timer); resolve(true); });141});142if (exited) return;143}144} catch {}145chrome.kill();146}147148function isProcessAlive(pid: number): boolean {149try { process.kill(pid, 0); return true; } catch { return false; }150}151152export async function connectChrome(options: ChromeConnectOptions): Promise<ChromeConnection> {153if (options.cdpUrl) {154if (options.cdpUrl.startsWith("ws://") || options.cdpUrl.startsWith("wss://")) {155return {156browserWsUrl: options.cdpUrl,157launched: false,158async close() {},159};160}161return connectToHttpEndpoint(options.cdpUrl);162}163164const profileDir = ensureChromeProfileDir(resolveChromeProfileDir(options.profileDir));165const reused = await tryReuseChrome(profileDir, options.logger);166if (reused) {167return reused;168}169170options.logger?.warn(`No running Chrome debugger found for profile ${profileDir}. Launching Chrome with that profile.`);171try {172return await launchFreshChrome(profileDir, options);173} catch (error) {174const entries = await listChromeProfileEntries(profileDir);175const shouldRetry = shouldRetryChromeLaunchRecovery({176hasLockArtifacts: hasChromeLockArtifacts(entries),177hasLiveOwner: findChromeProcessUsingProfile(profileDir),178});179if (!shouldRetry) {180throw error;181}182183options.logger?.warn(`Chrome launch failed with stale profile locks. Cleaning ${profileDir} and retrying once.`);184cleanChromeLockArtifacts(profileDir);185return await launchFreshChrome(profileDir, options);186}187}188