browse
Version:
Unified Browserbase CLI for browser automation and cloud APIs.
253 lines (252 loc) • 8.26 kB
JavaScript
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 "";
}
}