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/session.ts
1import { execFile } from "node:child_process";2import { promisify } from "node:util";3import { CdpClient, TargetSession, evaluateRuntime } from "./cdp-client";45interface NavigationResult {6errorText?: string;7}89const execFileAsync = promisify(execFile);10const MACOS_BROWSER_APP_IDS = [11"com.google.Chrome",12"org.chromium.Chromium",13"com.brave.Browser",14"com.microsoft.edgemac",15];1617function sleep(ms: number): Promise<void> {18return new Promise((resolve) => setTimeout(resolve, ms));19}2021async function activateBrowserApp(): Promise<void> {22if (process.platform !== "darwin") {23return;24}2526for (const appId of MACOS_BROWSER_APP_IDS) {27try {28await execFileAsync("osascript", ["-e", `tell application id "${appId}" to activate`]);29return;30} catch {31// Try the next installed browser bundle id.32}33}34}3536export class BrowserSession {37private constructor(38private readonly cdp: CdpClient,39public readonly targetSession: TargetSession,40public readonly interactive: boolean,41) {}4243static async open(44cdp: CdpClient,45options: {46initialUrl?: string;47interactive?: boolean;48} = {},49): Promise<BrowserSession> {50const targetSession = await cdp.createPageSession({51initialUrl: options.initialUrl,52visible: options.interactive,53});54const browser = new BrowserSession(cdp, targetSession, Boolean(options.interactive));55if (browser.interactive) {56await browser.bringToFront().catch(() => {});57}58return browser;59}6061async goto(url: string, timeoutMs = 30_000): Promise<void> {62const loadPromise = this.targetSession.waitForEvent("Page.loadEventFired", undefined, timeoutMs).catch(() => null);63const result = await this.targetSession.send<NavigationResult>("Page.navigate", { url });64if (result.errorText) {65throw new Error(`Navigation failed: ${result.errorText}`);66}67await loadPromise;68await this.waitForReadyState(timeoutMs);69}7071async waitForReadyState(timeoutMs = 30_000): Promise<void> {72const startedAt = Date.now();73while (Date.now() - startedAt < timeoutMs) {74const state = await this.evaluate<string>("document.readyState");75if (state === "interactive" || state === "complete") {76return;77}78await sleep(150);79}80throw new Error("Timed out waiting for document.readyState");81}8283async evaluate<T>(expression: string): Promise<T> {84return evaluateRuntime<T>(this.targetSession, expression);85}8687async getHTML(): Promise<string> {88return this.evaluate<string>("document.documentElement.outerHTML");89}9091async getTitle(): Promise<string> {92return this.evaluate<string>("document.title");93}9495async getURL(): Promise<string> {96return this.evaluate<string>("window.location.href");97}9899async bringToFront(): Promise<void> {100await this.targetSession.send("Page.bringToFront").catch(async () => {101await this.cdp.sendBrowserCommand("Target.activateTarget", {102targetId: this.targetSession.targetId,103});104});105if (this.interactive) {106await activateBrowserApp().catch(() => {});107}108}109110async click(selector: string): Promise<void> {111const result = await this.evaluate<{ ok: boolean; error?: string }>(`112(() => {113const element = document.querySelector(${JSON.stringify(selector)});114if (!element) {115return { ok: false, error: "Element not found" };116}117element.scrollIntoView({ block: "center", inline: "center" });118if (element instanceof HTMLElement) {119element.click();120return { ok: true };121}122return { ok: false, error: "Element is not clickable" };123})()124`);125126if (!result.ok) {127throw new Error(result.error ?? `Failed to click ${selector}`);128}129}130131async scrollToEnd(options: { stepPx?: number; delayMs?: number; maxSteps?: number } = {}): Promise<void> {132const stepPx = options.stepPx ?? 1_400;133const delayMs = options.delayMs ?? 250;134const maxSteps = options.maxSteps ?? 6;135136for (let step = 0; step < maxSteps; step += 1) {137const done = await this.evaluate<boolean>(`138(() => {139const before = window.scrollY;140window.scrollBy(0, ${stepPx});141const atBottom = window.innerHeight + window.scrollY >= document.body.scrollHeight - 4;142return atBottom || window.scrollY === before;143})()144`);145if (done) {146break;147}148await sleep(delayMs);149}150}151152async close(): Promise<void> {153await this.cdp.closeTarget(this.targetSession.targetId);154}155}156