UNPKG

browse

Version:

Unified Browserbase CLI for browser automation and cloud APIs.

253 lines (252 loc) 8.26 kB
import { Stagehand } from "@browserbasehq/stagehand"; import { emptyRefMaps, resolveSelector as resolveCachedSelector, } from "./commands/selectors.js"; import { executeDriverCommand } from "./commands/registry.js"; import { discoverLocalCdp } from "./local-cdp-discovery.js"; import { NetworkCapture } from "./network-capture.js"; const INIT_FAILURE_RETRY_MS = 5_000; export class DriverSessionManager { session; target; network; context = null; initFailure = null; initPromise = null; refMaps = emptyRefMaps(); selectedTargetId; stagehand = null; constructor(session, target) { this.session = session; this.target = target; this.network = new NetworkCapture(session); } async open(url) { return (await this.execute("open", { url })); } async execute(command, params) { return executeDriverCommand(this, command, params); } async activePage() { return this.ensurePage(); } async pageForOpen() { return this.ensurePage({ createIfMissing: true }); } async browserContext() { await this.ensureInitialized(); if (!this.context) { throw new Error("Driver context failed to initialize."); } return this.context; } async stagehandInstance() { await this.ensureInitialized(); if (!this.stagehand) { throw new Error("Stagehand instance failed to initialize."); } return this.stagehand; } async status() { if (!this.stagehand || !this.context) { return { browserConnected: false, initialized: false, mode: this.target.kind, pages: [], pid: process.pid, selectedTargetId: this.selectedTargetId, session: this.session, target: this.target, }; } const page = this.activePageIfPresent(); const pages = await this.pageSummaries(); return { browserConnected: true, initialized: true, mode: this.target.kind, pages, pid: process.pid, selectedTargetId: page?.targetId() ?? this.selectedTargetId, session: this.session, target: this.target, title: page ? await safeTitle(page) : undefined, url: page?.url(), }; } async close() { const stagehand = this.stagehand; this.stagehand = null; this.context = null; this.initFailure = null; await this.network.disable().catch(() => undefined); if (stagehand) { await stagehand.close().catch(() => undefined); } } resolveSelector(selector) { return resolveCachedSelector(selector, this.refMaps); } setRefMaps(refMaps) { this.refMaps = refMaps; } getRefMaps() { return this.refMaps; } async openResult(page) { return { mode: this.target.kind, pages: await this.pageSummaries(), selectedTargetId: page.targetId(), session: this.session, title: await this.safeTitle(page), url: page.url(), }; } async pageSummaries() { const pages = this.context?.pages() ?? []; return Promise.all(pages.map(async (page, index) => ({ index, targetId: page.targetId(), title: await this.safeTitle(page), url: page.url(), }))); } async safeTitle(page) { return safeTitle(page); } async ensurePage(options = {}) { await this.ensureInitialized(); if (!this.context) { throw new Error("Driver context failed to initialize."); } const target = this.target; if (target.kind === "cdp" && target.targetId) { const page = this.context .pages() .find((candidate) => candidate.targetId() === target.targetId); if (!page) { throw new Error(`Target ${target.targetId} was not found in the attached browser.`); } this.context.setActivePage(page); this.selectedTargetId = page.targetId(); return page; } const existingPage = this.activePageIfPresent() ?? this.context.pages()[0]; if (existingPage) { this.context.setActivePage(existingPage); this.selectedTargetId = existingPage.targetId(); return existingPage; } if (options.createIfMissing) { const page = await this.context.newPage(); this.context.setActivePage(page); this.selectedTargetId = page.targetId(); return page; } throw new Error(`No active page in session "${this.session}". Run browse open <url> --session ${this.session} or browse tab new <url> --session ${this.session}.`); } activePageIfPresent() { try { return this.context?.activePage() ?? undefined; } catch { return undefined; } } async ensureInitialized() { if (this.stagehand && this.context) return; if (this.initPromise) return this.initPromise; if (this.initFailure) { if (Date.now() < this.initFailure.retryAt) { throw this.initFailure.error; } this.initFailure = null; } this.initPromise = this.initialize() .then(() => { this.initFailure = null; }) .catch((error) => { this.initFailure = { error, retryAt: Date.now() + INIT_FAILURE_RETRY_MS, }; throw error; }) .finally(() => { this.initPromise = null; }); return this.initPromise; } async initialize() { const resolvedTarget = await this.resolveTarget(); const options = this.stagehandOptions(resolvedTarget); const stagehand = new Stagehand(options); try { await stagehand.init(); } catch (error) { await stagehand.close().catch(() => undefined); throw error; } this.stagehand = stagehand; this.context = stagehand.context; } async resolveTarget() { if (this.target.kind !== "auto-connect") return this.target; const discovered = await discoverLocalCdp(); if (!discovered) { throw new Error("No debuggable local browser found. Start Chrome with --remote-debugging-port=9222 or pass --cdp <url|port>."); } return { kind: "cdp", endpoint: discovered.wsUrl }; } stagehandOptions(target) { if (target.kind === "remote") { if (!process.env.BROWSERBASE_API_KEY) { throw new Error("Missing BROWSERBASE_API_KEY. Set it or pass --local for a managed local browser."); } return { apiKey: process.env.BROWSERBASE_API_KEY, browserbaseSessionCreateParams: { userMetadata: { browse_cli: "true" }, }, disableAPI: true, disablePino: true, env: "BROWSERBASE", verbose: 0, }; } if (target.kind === "managed-local") { return { disablePino: true, env: "LOCAL", localBrowserLaunchOptions: { headless: target.headless, }, verbose: 0, }; } if (target.kind === "cdp") { return { disablePino: true, env: "LOCAL", localBrowserLaunchOptions: { cdpUrl: target.endpoint, }, verbose: 0, }; } throw new Error(`Unsupported target kind: ${target.kind}`); } } async function safeTitle(page) { try { return await page.title(); } catch { return ""; } }