import { createRequire } from "node:module"; // ── Windows Node.js compatibility (auto-generated) ── import { fileURLToPath as _ftp } from "node:url"; import { dirname as _dn } from "node:path"; const __browseNodeSrcDir = _dn(_dn(_ftp(import.meta.url))) + "/src"; { const _r = createRequire(import.meta.url); _r("./bun-polyfill.cjs"); } // ── end compatibility ── var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __hasOwnProp = Object.prototype.hasOwnProperty; function __accessProp(key) { return this[key]; } var __toCommonJS = (from) => { var entry = (__moduleCache ??= new WeakMap).get(from), desc; if (entry) return entry; entry = __defProp({}, "__esModule", { value: true }); if (from && typeof from === "object" || typeof from === "function") { for (var key of __getOwnPropNames(from)) if (!__hasOwnProp.call(entry, key)) __defProp(entry, key, { get: __accessProp.bind(from, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } __moduleCache.set(from, entry); return entry; }; var __moduleCache; var __returnValue = (v) => v; function __exportSetter(name, newValue) { this[name] = __returnValue.bind(null, newValue); } var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: __exportSetter.bind(all, name) }); }; var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); var __require = /* @__PURE__ */ createRequire(import.meta.url); // browse/src/buffers.ts class CircularBuffer { buffer; head = 0; _size = 0; _totalAdded = 0; capacity; constructor(capacity) { this.capacity = capacity; this.buffer = new Array(capacity); } push(entry) { const index = (this.head + this._size) % this.capacity; this.buffer[index] = entry; if (this._size < this.capacity) { this._size++; } else { this.head = (this.head + 1) % this.capacity; } this._totalAdded++; } toArray() { const result = []; for (let i = 0;i < this._size; i++) { result.push(this.buffer[(this.head + i) % this.capacity]); } return result; } last(n) { const count = Math.min(n, this._size); const result = []; const start = (this.head + this._size - count) % this.capacity; for (let i = 0;i < count; i++) { result.push(this.buffer[(start + i) % this.capacity]); } return result; } get length() { return this._size; } get totalAdded() { return this._totalAdded; } clear() { this.head = 0; this._size = 0; } get(index) { if (index < 0 || index >= this._size) return; return this.buffer[(this.head + index) % this.capacity]; } set(index, entry) { if (index < 0 || index >= this._size) return; this.buffer[(this.head + index) % this.capacity] = entry; } } function addConsoleEntry(entry) { consoleBuffer.push(entry); } function addNetworkEntry(entry) { networkBuffer.push(entry); } function addDialogEntry(entry) { dialogBuffer.push(entry); } var HIGH_WATER_MARK = 50000, consoleBuffer, networkBuffer, dialogBuffer; var init_buffers = __esm(() => { consoleBuffer = new CircularBuffer(HIGH_WATER_MARK); networkBuffer = new CircularBuffer(HIGH_WATER_MARK); dialogBuffer = new CircularBuffer(HIGH_WATER_MARK); }); // browse/src/url-validation.ts function normalizeHostname(hostname) { let h = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname; if (h.endsWith(".")) h = h.slice(0, -1); return h; } function isMetadataIp(hostname) { try { const probe = new URL(`http://${hostname}`); const normalized = probe.hostname; if (BLOCKED_METADATA_HOSTS.has(normalized)) return true; if (normalized.endsWith(".") && BLOCKED_METADATA_HOSTS.has(normalized.slice(0, -1))) return true; } catch {} return false; } async function resolvesToBlockedIp(hostname) { try { const dns = await import("node:dns"); const { resolve4 } = dns.promises; const addresses = await resolve4(hostname); return addresses.some((addr) => BLOCKED_METADATA_HOSTS.has(addr)); } catch { return false; } } async function validateNavigationUrl(url) { let parsed; try { parsed = new URL(url); } catch { throw new Error(`Invalid URL: ${url}`); } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { throw new Error(`Blocked: scheme "${parsed.protocol}" is not allowed. Only http: and https: URLs are permitted.`); } const hostname = normalizeHostname(parsed.hostname.toLowerCase()); if (BLOCKED_METADATA_HOSTS.has(hostname) || isMetadataIp(hostname)) { throw new Error(`Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security.`); } const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; const isPrivateNet = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/.test(hostname); if (!isLoopback && !isPrivateNet && await resolvesToBlockedIp(hostname)) { throw new Error(`Blocked: ${parsed.hostname} resolves to a cloud metadata IP. Possible DNS rebinding attack.`); } } var BLOCKED_METADATA_HOSTS; var init_url_validation = __esm(() => { BLOCKED_METADATA_HOSTS = new Set([ "169.254.169.254", "fd00::", "metadata.google.internal", "metadata.azure.internal" ]); }); // browse/src/config.ts var exports_config = {}; __export(exports_config, { resolveConfig: () => resolveConfig, readVersionHash: () => readVersionHash, getRemoteSlug: () => getRemoteSlug, getGitRoot: () => getGitRoot, ensureStateDir: () => ensureStateDir }); import * as fs from "fs"; import * as path from "path"; function getGitRoot() { try { const proc = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "pipe", timeout: 2000 }); if (proc.exitCode !== 0) return null; return proc.stdout.toString().trim() || null; } catch { return null; } } function resolveConfig(env = process.env) { let stateFile; let stateDir; let projectDir; if (env.BROWSE_STATE_FILE) { stateFile = env.BROWSE_STATE_FILE; stateDir = path.dirname(stateFile); projectDir = path.dirname(stateDir); } else { projectDir = getGitRoot() || process.cwd(); stateDir = path.join(projectDir, ".gstack"); stateFile = path.join(stateDir, "browse.json"); } return { projectDir, stateDir, stateFile, consoleLog: path.join(stateDir, "browse-console.log"), networkLog: path.join(stateDir, "browse-network.log"), dialogLog: path.join(stateDir, "browse-dialog.log") }; } function ensureStateDir(config) { try { fs.mkdirSync(config.stateDir, { recursive: true }); } catch (err) { if (err.code === "EACCES") { throw new Error(`Cannot create state directory ${config.stateDir}: permission denied`); } if (err.code === "ENOTDIR") { throw new Error(`Cannot create state directory ${config.stateDir}: a file exists at that path`); } throw err; } const gitignorePath = path.join(config.projectDir, ".gitignore"); try { const content = fs.readFileSync(gitignorePath, "utf-8"); if (!content.match(/^\.gstack\/?$/m)) { const separator = content.endsWith(` `) ? "" : ` `; fs.appendFileSync(gitignorePath, `${separator}.gstack/ `); } } catch (err) { if (err.code !== "ENOENT") { const logPath = path.join(config.stateDir, "browse-server.log"); try { fs.appendFileSync(logPath, `[${new Date().toISOString()}] Warning: could not update .gitignore at ${gitignorePath}: ${err.message} `); } catch {} } } } function getRemoteSlug() { try { const proc = Bun.spawnSync(["git", "remote", "get-url", "origin"], { stdout: "pipe", stderr: "pipe", timeout: 2000 }); if (proc.exitCode !== 0) throw new Error("no remote"); const url = proc.stdout.toString().trim(); const match = url.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/); if (match) return `${match[1]}-${match[2]}`; throw new Error("unparseable"); } catch { const root = getGitRoot(); return path.basename(root || process.cwd()); } } function readVersionHash(execPath = process.execPath) { try { const versionFile = path.resolve(path.dirname(execPath), ".version"); return fs.readFileSync(versionFile, "utf-8").trim() || null; } catch { return null; } } var init_config = () => {}; // browse/src/platform.ts import * as os from "os"; import * as path2 from "path"; function isPathWithin(resolvedPath, dir) { return resolvedPath === dir || resolvedPath.startsWith(dir + path2.sep); } var IS_WINDOWS, TEMP_DIR; var init_platform = __esm(() => { IS_WINDOWS = process.platform === "win32"; TEMP_DIR = IS_WINDOWS ? os.tmpdir() : "/tmp"; }); // browse/src/cdp-inspector.ts async function getOrCreateSession(page) { let session = cdpSessions.get(page); if (session) { try { await session.send("DOM.getDocument", { depth: 0 }); return session; } catch { cdpSessions.delete(page); initializedPages.delete(page); } } session = await page.context().newCDPSession(page); cdpSessions.set(page, session); await session.send("DOM.enable"); await session.send("CSS.enable"); initializedPages.add(page); page.once("framenavigated", () => { try { session.detach().catch(() => {}); } catch {} cdpSessions.delete(page); initializedPages.delete(page); }); return session; } function computeSpecificity(selector) { let a = 0, b = 0, c = 0; let cleaned = selector; const ids = cleaned.match(/#[a-zA-Z_-][\w-]*/g); if (ids) a += ids.length; const classes = cleaned.match(/\.[a-zA-Z_-][\w-]*/g); if (classes) b += classes.length; const attrs = cleaned.match(/\[[^\]]+\]/g); if (attrs) b += attrs.length; const pseudoClasses = cleaned.match(/(?])([a-zA-Z][\w-]*)/g); if (types) c += types.length; const pseudoElements = cleaned.match(/::[a-zA-Z][\w-]*/g); if (pseudoElements) c += pseudoElements.length; return { a, b, c }; } function compareSpecificity(s1, s2) { if (s1.a !== s2.a) return s1.a - s2.a; if (s1.b !== s2.b) return s1.b - s2.b; return s1.c - s2.c; } async function inspectElement(page, selector, options) { const session = await getOrCreateSession(page); const { root } = await session.send("DOM.getDocument", { depth: 0 }); let nodeId; try { const result = await session.send("DOM.querySelector", { nodeId: root.nodeId, selector }); nodeId = result.nodeId; if (!nodeId) throw new Error(`Element not found: ${selector}`); } catch (err) { throw new Error(`Element not found: ${selector} — ${err.message}`); } const { node } = await session.send("DOM.describeNode", { nodeId, depth: 0 }); const tagName = (node.localName || node.nodeName || "").toLowerCase(); const attrPairs = node.attributes || []; const attributes = {}; for (let i = 0;i < attrPairs.length; i += 2) { attributes[attrPairs[i]] = attrPairs[i + 1]; } const id = attributes.id || null; const classes = attributes.class ? attributes.class.split(/\s+/).filter(Boolean) : []; let boxModel = { content: { x: 0, y: 0, width: 0, height: 0 }, padding: { top: 0, right: 0, bottom: 0, left: 0 }, border: { top: 0, right: 0, bottom: 0, left: 0 }, margin: { top: 0, right: 0, bottom: 0, left: 0 } }; try { const boxData = await session.send("DOM.getBoxModel", { nodeId }); const model = boxData.model; const content = model.content; const padding = model.padding; const border = model.border; const margin = model.margin; const contentX = content[0]; const contentY = content[1]; const contentWidth = content[2] - content[0]; const contentHeight = content[5] - content[1]; boxModel = { content: { x: contentX, y: contentY, width: contentWidth, height: contentHeight }, padding: { top: content[1] - padding[1], right: padding[2] - content[2], bottom: padding[5] - content[5], left: content[0] - padding[0] }, border: { top: padding[1] - border[1], right: border[2] - padding[2], bottom: border[5] - padding[5], left: padding[0] - border[0] }, margin: { top: border[1] - margin[1], right: margin[2] - border[2], bottom: margin[5] - border[5], left: border[0] - margin[0] } }; } catch {} const matchedData = await session.send("CSS.getMatchedStylesForNode", { nodeId }); const computedData = await session.send("CSS.getComputedStyleForNode", { nodeId }); const computedStyles = {}; for (const entry of computedData.computedStyle) { if (KEY_CSS_SET.has(entry.name)) { computedStyles[entry.name] = entry.value; } } const inlineData = await session.send("CSS.getInlineStylesForNode", { nodeId }); const inlineStyles = {}; if (inlineData.inlineStyle?.cssProperties) { for (const prop of inlineData.inlineStyle.cssProperties) { if (prop.name && prop.value && !prop.disabled) { inlineStyles[prop.name] = prop.value; } } } const matchedRules = []; const seenProperties = new Map; if (matchedData.matchedCSSRules) { for (const match of matchedData.matchedCSSRules) { const rule = match.rule; const isUA = rule.origin === "user-agent"; if (isUA && !options?.includeUA) continue; let selectorText = ""; if (rule.selectorList?.selectors) { const matchingIdx = match.matchingSelectors?.[0] ?? 0; selectorText = rule.selectorList.selectors[matchingIdx]?.text || rule.selectorList.text || ""; } let source = "inline"; let sourceLine = 0; let sourceColumn = 0; let styleSheetId; let range; if (rule.styleSheetId) { styleSheetId = rule.styleSheetId; try { source = rule.origin === "regular" ? rule.styleSheetId || "stylesheet" : rule.origin; } catch {} } if (rule.style?.range) { range = rule.style.range; sourceLine = rule.style.range.startLine || 0; sourceColumn = rule.style.range.startColumn || 0; } if (styleSheetId) { try { if (rule.style?.cssText) {} } catch {} } let media; if (match.rule?.media) { const mediaList = match.rule.media; if (Array.isArray(mediaList) && mediaList.length > 0) { media = mediaList.map((m) => m.text).filter(Boolean).join(", "); } } const specificity = computeSpecificity(selectorText); const properties = []; if (rule.style?.cssProperties) { for (const prop of rule.style.cssProperties) { if (!prop.name || prop.disabled) continue; if (prop.name.startsWith("-") && !KEY_CSS_SET.has(prop.name)) continue; properties.push({ name: prop.name, value: prop.value || "", important: prop.important || (prop.value?.includes("!important") ?? false), overridden: false }); } } matchedRules.push({ selector: selectorText, properties, source, sourceLine, sourceColumn, specificity, media, userAgent: isUA, styleSheetId, range }); } } matchedRules.sort((a, b) => -compareSpecificity(a.specificity, b.specificity)); for (let i = 0;i < matchedRules.length; i++) { for (const prop of matchedRules[i].properties) { const key = prop.name; if (!seenProperties.has(key)) { seenProperties.set(key, i); } else { const earlierIdx = seenProperties.get(key); const earlierRule = matchedRules[earlierIdx]; const earlierProp = earlierRule.properties.find((p) => p.name === key); if (prop.important && earlierProp && !earlierProp.important) { if (earlierProp) earlierProp.overridden = true; seenProperties.set(key, i); } else { prop.overridden = true; } } } } const pseudoElements = []; if (matchedData.pseudoElements) { for (const pseudo of matchedData.pseudoElements) { const pseudoType = pseudo.pseudoType || "unknown"; const rules = []; if (pseudo.matches) { for (const match of pseudo.matches) { const rule = match.rule; const sel = rule.selectorList?.text || ""; const props = (rule.style?.cssProperties || []).filter((p) => p.name && !p.disabled).map((p) => `${p.name}: ${p.value}`).join("; "); if (props) { rules.push({ selector: sel, properties: props }); } } } if (rules.length > 0) { pseudoElements.push({ pseudo: `::${pseudoType}`, rules }); } } } for (const rule of matchedRules) { if (rule.styleSheetId && rule.source !== "inline") { try { const sheetMeta = await session.send("CSS.getStyleSheetText", { styleSheetId: rule.styleSheetId }).catch(() => null); } catch {} } } return { selector, tagName, id, classes, attributes, boxModel, computedStyles, matchedRules, inlineStyles, pseudoElements }; } async function modifyStyle(page, selector, property, value) { if (!/^[a-zA-Z-]+$/.test(property)) { throw new Error(`Invalid CSS property name: ${property}. Only letters and hyphens allowed.`); } let oldValue = ""; let source = "inline"; let sourceLine = 0; let method = "inline"; try { const session = await getOrCreateSession(page); const result = await inspectElement(page, selector); oldValue = result.computedStyles[property] || ""; let targetRule = null; for (const rule of result.matchedRules) { if (rule.userAgent) continue; const hasProp = rule.properties.some((p) => p.name === property); if (hasProp && rule.styleSheetId && rule.range) { targetRule = rule; break; } } if (targetRule?.styleSheetId && targetRule.range) { const range = targetRule.range; const styleText = await session.send("CSS.getStyleSheetText", { styleSheetId: targetRule.styleSheetId }); const currentProps = targetRule.properties; const newPropsText = currentProps.map((p) => { if (p.name === property) { return `${p.name}: ${value}`; } return `${p.name}: ${p.value}`; }).join("; "); try { await session.send("CSS.setStyleTexts", { edits: [{ styleSheetId: targetRule.styleSheetId, range, text: newPropsText }] }); method = "setStyleTexts"; source = `${targetRule.source}:${targetRule.sourceLine}`; sourceLine = targetRule.sourceLine; } catch {} } if (method === "inline") { await page.evaluate(([sel, prop, val]) => { const el = document.querySelector(sel); if (!el) throw new Error(`Element not found: ${sel}`); el.style.setProperty(prop, val); }, [selector, property, value]); } } catch (err) { await page.evaluate(([sel, prop, val]) => { const el = document.querySelector(sel); if (!el) throw new Error(`Element not found: ${sel}`); el.style.setProperty(prop, val); }, [selector, property, value]); } const modification = { selector, property, oldValue, newValue: value, source, sourceLine, timestamp: Date.now(), method }; modificationHistory.push(modification); return modification; } async function undoModification(page, index) { const idx = index ?? modificationHistory.length - 1; if (idx < 0 || idx >= modificationHistory.length) { throw new Error(`No modification at index ${idx}. History has ${modificationHistory.length} entries.`); } const mod = modificationHistory[idx]; if (mod.method === "setStyleTexts") { try { await modifyStyle(page, mod.selector, mod.property, mod.oldValue); modificationHistory.pop(); } catch { await page.evaluate(([sel, prop, val]) => { const el = document.querySelector(sel); if (!el) return; if (val) { el.style.setProperty(prop, val); } else { el.style.removeProperty(prop); } }, [mod.selector, mod.property, mod.oldValue]); } } else { await page.evaluate(([sel, prop, val]) => { const el = document.querySelector(sel); if (!el) return; if (val) { el.style.setProperty(prop, val); } else { el.style.removeProperty(prop); } }, [mod.selector, mod.property, mod.oldValue]); } modificationHistory.splice(idx, 1); } function getModificationHistory() { return [...modificationHistory]; } async function resetModifications(page) { for (let i = modificationHistory.length - 1;i >= 0; i--) { const mod = modificationHistory[i]; try { await page.evaluate(([sel, prop, val]) => { const el = document.querySelector(sel); if (!el) return; if (val) { el.style.setProperty(prop, val); } else { el.style.removeProperty(prop); } }, [mod.selector, mod.property, mod.oldValue]); } catch {} } modificationHistory.length = 0; } function formatInspectorResult(result, options) { const lines = []; const classStr = result.classes.length > 0 ? ` class="${result.classes.join(" ")}"` : ""; const idStr = result.id ? ` id="${result.id}"` : ""; lines.push(`Element: <${result.tagName}${idStr}${classStr}>`); lines.push(`Selector: ${result.selector}`); const w = Math.round(result.boxModel.content.width + result.boxModel.padding.left + result.boxModel.padding.right); const h = Math.round(result.boxModel.content.height + result.boxModel.padding.top + result.boxModel.padding.bottom); lines.push(`Dimensions: ${w} x ${h}`); lines.push(""); lines.push("Box Model:"); const bm = result.boxModel; lines.push(` margin: ${Math.round(bm.margin.top)}px ${Math.round(bm.margin.right)}px ${Math.round(bm.margin.bottom)}px ${Math.round(bm.margin.left)}px`); lines.push(` padding: ${Math.round(bm.padding.top)}px ${Math.round(bm.padding.right)}px ${Math.round(bm.padding.bottom)}px ${Math.round(bm.padding.left)}px`); lines.push(` border: ${Math.round(bm.border.top)}px ${Math.round(bm.border.right)}px ${Math.round(bm.border.bottom)}px ${Math.round(bm.border.left)}px`); lines.push(` content: ${Math.round(bm.content.width)} x ${Math.round(bm.content.height)}`); lines.push(""); const displayRules = options?.includeUA ? result.matchedRules : result.matchedRules.filter((r) => !r.userAgent); lines.push(`Matched Rules (${displayRules.length}):`); if (displayRules.length === 0) { lines.push(" (none)"); } else { for (const rule of displayRules) { const propsStr = rule.properties.filter((p) => !p.overridden).map((p) => `${p.name}: ${p.value}${p.important ? " !important" : ""}`).join("; "); if (!propsStr) continue; const spec = `[${rule.specificity.a},${rule.specificity.b},${rule.specificity.c}]`; lines.push(` ${rule.selector} { ${propsStr} }`); lines.push(` -> ${rule.source}:${rule.sourceLine} ${spec}${rule.media ? ` @media ${rule.media}` : ""}`); } } lines.push(""); lines.push("Inline Styles:"); const inlineEntries = Object.entries(result.inlineStyles); if (inlineEntries.length === 0) { lines.push(" (none)"); } else { const inlineStr = inlineEntries.map(([k, v]) => `${k}: ${v}`).join("; "); lines.push(` ${inlineStr}`); } lines.push(""); lines.push("Computed (key):"); const cs = result.computedStyles; const computedPairs = []; for (const prop of KEY_CSS_PROPERTIES) { if (cs[prop] !== undefined) { computedPairs.push(`${prop}: ${cs[prop]}`); } } for (let i = 0;i < computedPairs.length; i += 3) { const chunk = computedPairs.slice(i, i + 3); lines.push(` ${chunk.join(" | ")}`); } if (result.pseudoElements.length > 0) { lines.push(""); lines.push("Pseudo-elements:"); for (const pseudo of result.pseudoElements) { for (const rule of pseudo.rules) { lines.push(` ${pseudo.pseudo} ${rule.selector} { ${rule.properties} }`); } } } return lines.join(` `); } function detachSession(page) { if (page) { const session = cdpSessions.get(page); if (session) { try { session.detach().catch(() => {}); } catch {} cdpSessions.delete(page); initializedPages.delete(page); } } } var KEY_CSS_PROPERTIES, KEY_CSS_SET, cdpSessions, initializedPages, modificationHistory; var init_cdp_inspector = __esm(() => { KEY_CSS_PROPERTIES = [ "display", "position", "top", "right", "bottom", "left", "float", "clear", "z-index", "overflow", "overflow-x", "overflow-y", "width", "height", "min-width", "max-width", "min-height", "max-height", "margin-top", "margin-right", "margin-bottom", "margin-left", "padding-top", "padding-right", "padding-bottom", "padding-left", "border-top-width", "border-right-width", "border-bottom-width", "border-left-width", "border-style", "border-color", "font-family", "font-size", "font-weight", "line-height", "color", "background-color", "background-image", "opacity", "box-shadow", "border-radius", "transform", "transition", "flex-direction", "flex-wrap", "justify-content", "align-items", "gap", "grid-template-columns", "grid-template-rows", "text-align", "text-decoration", "visibility", "cursor", "pointer-events" ]; KEY_CSS_SET = new Set(KEY_CSS_PROPERTIES); cdpSessions = new WeakMap; initializedPages = new WeakSet; modificationHistory = []; }); // browse/src/read-commands.ts var exports_read_commands = {}; __export(exports_read_commands, { validateReadPath: () => validateReadPath, handleReadCommand: () => handleReadCommand, getCleanText: () => getCleanText }); import * as fs2 from "fs"; import * as path3 from "path"; function hasAwait(code) { const stripped = code.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); return /\bawait\b/.test(stripped); } function needsBlockWrapper(code) { const trimmed = code.trim(); if (trimmed.split(` `).length > 1) return true; if (/\b(const|let|var|function|class|return|throw|if|for|while|switch|try)\b/.test(trimmed)) return true; if (trimmed.includes(";")) return true; return false; } function wrapForEvaluate(code) { if (!hasAwait(code)) return code; const trimmed = code.trim(); return needsBlockWrapper(trimmed) ? `(async()=>{ ${code} })()` : `(async()=>(${trimmed}))()`; } function validateReadPath(filePath) { const resolved = path3.resolve(filePath); let realPath; try { realPath = fs2.realpathSync(resolved); } catch (err) { if (err.code === "ENOENT") { try { const dir = fs2.realpathSync(path3.dirname(resolved)); realPath = path3.join(dir, path3.basename(resolved)); } catch { realPath = resolved; } } else { throw new Error(`Cannot resolve real path: ${filePath} (${err.code})`); } } const isSafe = SAFE_DIRECTORIES.some((dir) => isPathWithin(realPath, dir)); if (!isSafe) { throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(", ")}`); } } async function getCleanText(page) { return await page.evaluate(() => { const body = document.body; if (!body) return ""; const clone = body.cloneNode(true); clone.querySelectorAll("script, style, noscript, svg").forEach((el) => el.remove()); return clone.innerText.split(` `).map((line) => line.trim()).filter((line) => line.length > 0).join(` `); }); } async function handleReadCommand(command, args, bm) { const page = bm.getPage(); const target = bm.getActiveFrameOrPage(); switch (command) { case "text": { return await getCleanText(target); } case "html": { const selector = args[0]; if (selector) { const resolved = await bm.resolveRef(selector); if ("locator" in resolved) { return await resolved.locator.innerHTML({ timeout: 5000 }); } return await target.locator(resolved.selector).innerHTML({ timeout: 5000 }); } const doctype = await target.evaluate(() => { const dt = document.doctype; return dt ? `` : ""; }); const html = await target.evaluate(() => document.documentElement.outerHTML); return doctype ? `${doctype} ${html}` : html; } case "links": { const links = await target.evaluate(() => [...document.querySelectorAll("a[href]")].map((a) => ({ text: a.textContent?.trim().slice(0, 120) || "", href: a.href })).filter((l) => l.text && l.href)); return links.map((l) => `${l.text} → ${l.href}`).join(` `); } case "forms": { const forms = await target.evaluate(() => { return [...document.querySelectorAll("form")].map((form, i) => { const fields = [...form.querySelectorAll("input, select, textarea")].map((el) => { const input = el; return { tag: el.tagName.toLowerCase(), type: input.type || undefined, name: input.name || undefined, id: input.id || undefined, placeholder: input.placeholder || undefined, required: input.required || undefined, value: input.type === "password" ? "[redacted]" : input.value || undefined, options: el.tagName === "SELECT" ? [...el.options].map((o) => ({ value: o.value, text: o.text })) : undefined }; }); return { index: i, action: form.action || undefined, method: form.method || "get", id: form.id || undefined, fields }; }); }); return JSON.stringify(forms, null, 2); } case "accessibility": { const snapshot = await target.locator("body").ariaSnapshot(); return snapshot; } case "js": { const expr = args[0]; if (!expr) throw new Error("Usage: browse js "); const wrapped = wrapForEvaluate(expr); const result = await target.evaluate(wrapped); return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? ""); } case "eval": { const filePath = args[0]; if (!filePath) throw new Error("Usage: browse eval "); validateReadPath(filePath); if (!fs2.existsSync(filePath)) throw new Error(`File not found: ${filePath}`); const code = fs2.readFileSync(filePath, "utf-8"); const wrapped = wrapForEvaluate(code); const result = await target.evaluate(wrapped); return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? ""); } case "css": { const [selector, property] = args; if (!selector || !property) throw new Error("Usage: browse css "); const resolved = await bm.resolveRef(selector); if ("locator" in resolved) { const value2 = await resolved.locator.evaluate((el, prop) => getComputedStyle(el).getPropertyValue(prop), property); return value2; } const value = await target.evaluate(([sel, prop]) => { const el = document.querySelector(sel); if (!el) return `Element not found: ${sel}`; return getComputedStyle(el).getPropertyValue(prop); }, [resolved.selector, property]); return value; } case "attrs": { const selector = args[0]; if (!selector) throw new Error("Usage: browse attrs "); const resolved = await bm.resolveRef(selector); if ("locator" in resolved) { const attrs2 = await resolved.locator.evaluate((el) => { const result = {}; for (const attr of el.attributes) { result[attr.name] = attr.value; } return result; }); return JSON.stringify(attrs2, null, 2); } const attrs = await target.evaluate((sel) => { const el = document.querySelector(sel); if (!el) return `Element not found: ${sel}`; const result = {}; for (const attr of el.attributes) { result[attr.name] = attr.value; } return result; }, resolved.selector); return typeof attrs === "string" ? attrs : JSON.stringify(attrs, null, 2); } case "console": { if (args[0] === "--clear") { consoleBuffer.clear(); return "Console buffer cleared."; } const entries = args[0] === "--errors" ? consoleBuffer.toArray().filter((e) => e.level === "error" || e.level === "warning") : consoleBuffer.toArray(); if (entries.length === 0) return args[0] === "--errors" ? "(no console errors)" : "(no console messages)"; return entries.map((e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`).join(` `); } case "network": { if (args[0] === "--clear") { networkBuffer.clear(); return "Network buffer cleared."; } if (networkBuffer.length === 0) return "(no network requests)"; return networkBuffer.toArray().map((e) => `${e.method} ${e.url} → ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`).join(` `); } case "dialog": { if (args[0] === "--clear") { dialogBuffer.clear(); return "Dialog buffer cleared."; } if (dialogBuffer.length === 0) return "(no dialogs captured)"; return dialogBuffer.toArray().map((e) => `[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ""}`).join(` `); } case "is": { const property = args[0]; const selector = args[1]; if (!property || !selector) throw new Error(`Usage: browse is Properties: visible, hidden, enabled, disabled, checked, editable, focused`); const resolved = await bm.resolveRef(selector); let locator; if ("locator" in resolved) { locator = resolved.locator; } else { locator = target.locator(resolved.selector); } switch (property) { case "visible": return String(await locator.isVisible()); case "hidden": return String(await locator.isHidden()); case "enabled": return String(await locator.isEnabled()); case "disabled": return String(await locator.isDisabled()); case "checked": return String(await locator.isChecked()); case "editable": return String(await locator.isEditable()); case "focused": { const isFocused = await locator.evaluate((el) => el === document.activeElement); return String(isFocused); } default: throw new Error(`Unknown property: ${property}. Use: visible, hidden, enabled, disabled, checked, editable, focused`); } } case "cookies": { const cookies = await page.context().cookies(); return JSON.stringify(cookies, null, 2); } case "storage": { if (args[0] === "set" && args[1]) { const key = args[1]; const value = args[2] || ""; await target.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]); return `Set localStorage["${key}"]`; } const storage = await target.evaluate(() => ({ localStorage: { ...localStorage }, sessionStorage: { ...sessionStorage } })); const SENSITIVE_KEY = /(^|[_.-])(token|secret|key|password|credential|auth|jwt|session|csrf)($|[_.-])|api.?key/i; const SENSITIVE_VALUE = /^(eyJ|sk-|sk_live_|sk_test_|pk_live_|pk_test_|rk_live_|sk-ant-|ghp_|gho_|github_pat_|xox[bpsa]-|AKIA[A-Z0-9]{16}|AIza|SG\.|Bearer\s|sbp_)/; const redacted = JSON.parse(JSON.stringify(storage)); for (const storeType of ["localStorage", "sessionStorage"]) { const store = redacted[storeType]; if (!store) continue; for (const [key, value] of Object.entries(store)) { if (typeof value !== "string") continue; if (SENSITIVE_KEY.test(key) || SENSITIVE_VALUE.test(value)) { store[key] = `[REDACTED — ${value.length} chars]`; } } } return JSON.stringify(redacted, null, 2); } case "perf": { const timings = await page.evaluate(() => { const nav = performance.getEntriesByType("navigation")[0]; if (!nav) return "No navigation timing data available."; return { dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart), tcp: Math.round(nav.connectEnd - nav.connectStart), ssl: Math.round(nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0), ttfb: Math.round(nav.responseStart - nav.requestStart), download: Math.round(nav.responseEnd - nav.responseStart), domParse: Math.round(nav.domInteractive - nav.responseEnd), domReady: Math.round(nav.domContentLoadedEventEnd - nav.startTime), load: Math.round(nav.loadEventEnd - nav.startTime), total: Math.round(nav.loadEventEnd - nav.startTime) }; }); if (typeof timings === "string") return timings; return Object.entries(timings).map(([k, v]) => `${k.padEnd(12)} ${v}ms`).join(` `); } case "inspect": { let includeUA = false; let showHistory = false; let selector; for (const arg of args) { if (arg === "--all") { includeUA = true; } else if (arg === "--history") { showHistory = true; } else if (!selector) { selector = arg; } } if (showHistory) { const history = getModificationHistory(); if (history.length === 0) return "(no style modifications)"; return history.map((m, i) => `[${i}] ${m.selector} { ${m.property}: ${m.oldValue} → ${m.newValue} } (${m.source}, ${m.method})`).join(` `); } if (!selector) { const stored = bm._inspectorData; const storedTs = bm._inspectorTimestamp; if (stored) { const stale = storedTs && Date.now() - storedTs > 60000; let output = formatInspectorResult(stored, { includeUA }); if (stale) output = `⚠ Data may be stale (>60s old) ` + output; return output; } throw new Error(`Usage: browse inspect [selector] [--all] [--history] Or pick an element in the Chrome sidebar first.`); } const result = await inspectElement(page, selector, { includeUA }); bm._inspectorData = result; bm._inspectorTimestamp = Date.now(); return formatInspectorResult(result, { includeUA }); } default: throw new Error(`Unknown read command: ${command}`); } } var SAFE_DIRECTORIES; var init_read_commands = __esm(() => { init_buffers(); init_platform(); init_cdp_inspector(); SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()].map((d) => { try { return fs2.realpathSync(d); } catch { return d; } }); }); // browse/src/cookie-import-browser.ts const Database = null; // bun:sqlite stubbed on Node import * as crypto from "crypto"; import * as fs3 from "fs"; import * as path4 from "path"; import * as os2 from "os"; function findInstalledBrowsers() { return BROWSER_REGISTRY.filter((browser) => { if (findBrowserMatch(browser, "Default") !== null) return true; for (const platform of getSearchPlatforms()) { const dataDir = getDataDirForPlatform(browser, platform); if (!dataDir) continue; const browserDir = path4.join(getBaseDir(platform), dataDir); try { const entries = fs3.readdirSync(browserDir, { withFileTypes: true }); if (entries.some((e) => e.isDirectory() && e.name.startsWith("Profile ") && fs3.existsSync(path4.join(browserDir, e.name, "Cookies")))) return true; } catch {} } return false; }); } function listSupportedBrowserNames() { const hostPlatform = getHostPlatform(); return BROWSER_REGISTRY.filter((browser) => hostPlatform ? getDataDirForPlatform(browser, hostPlatform) !== null : true).map((browser) => browser.name); } function listProfiles(browserName) { const browser = resolveBrowser(browserName); const profiles = []; for (const platform of getSearchPlatforms()) { const dataDir = getDataDirForPlatform(browser, platform); if (!dataDir) continue; const browserDir = path4.join(getBaseDir(platform), dataDir); if (!fs3.existsSync(browserDir)) continue; let entries; try { entries = fs3.readdirSync(browserDir, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { if (!entry.isDirectory()) continue; if (entry.name !== "Default" && !entry.name.startsWith("Profile ")) continue; const cookiePath = path4.join(browserDir, entry.name, "Cookies"); if (!fs3.existsSync(cookiePath)) continue; if (profiles.some((p) => p.name === entry.name)) continue; let displayName = entry.name; try { const prefsPath = path4.join(browserDir, entry.name, "Preferences"); if (fs3.existsSync(prefsPath)) { const prefs = JSON.parse(fs3.readFileSync(prefsPath, "utf-8")); const email = prefs?.account_info?.[0]?.email; if (email && typeof email === "string") { displayName = email; } else { const profileName = prefs?.profile?.name; if (profileName && typeof profileName === "string") { displayName = profileName; } } } } catch {} profiles.push({ name: entry.name, displayName }); } if (profiles.length > 0) break; } return profiles; } function listDomains(browserName, profile = "Default") { const browser = resolveBrowser(browserName); const match = getBrowserMatch(browser, profile); const db = openDb(match.dbPath, browser.name); try { const now = chromiumNow(); const rows = db.query(`SELECT host_key AS domain, COUNT(*) AS count FROM cookies WHERE has_expires = 0 OR expires_utc > ? GROUP BY host_key ORDER BY count DESC`).all(now); return { domains: rows, browser: browser.name }; } finally { db.close(); } } async function importCookies(browserName, domains, profile = "Default") { if (domains.length === 0) return { cookies: [], count: 0, failed: 0, domainCounts: {} }; const browser = resolveBrowser(browserName); const match = getBrowserMatch(browser, profile); const derivedKeys = await getDerivedKeys(match); const db = openDb(match.dbPath, browser.name); try { const now = chromiumNow(); const placeholders = domains.map(() => "?").join(","); const rows = db.query(`SELECT host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, has_expires, samesite FROM cookies WHERE host_key IN (${placeholders}) AND (has_expires = 0 OR expires_utc > ?) ORDER BY host_key, name`).all(...domains, now); const cookies = []; let failed = 0; const domainCounts = {}; for (const row of rows) { try { const value = decryptCookieValue(row, derivedKeys); const cookie = toPlaywrightCookie(row, value); cookies.push(cookie); domainCounts[row.host_key] = (domainCounts[row.host_key] || 0) + 1; } catch { failed++; } } return { cookies, count: cookies.length, failed, domainCounts }; } finally { db.close(); } } function resolveBrowser(nameOrAlias) { const needle = nameOrAlias.toLowerCase().trim(); const found = BROWSER_REGISTRY.find((b) => b.aliases.includes(needle) || b.name.toLowerCase() === needle); if (!found) { const supported = BROWSER_REGISTRY.flatMap((b) => b.aliases).join(", "); throw new CookieImportError(`Unknown browser '${nameOrAlias}'. Supported: ${supported}`, "unknown_browser"); } return found; } function validateProfile(profile) { if (/[/\\]|\.\./.test(profile) || /[\x00-\x1f]/.test(profile)) { throw new CookieImportError(`Invalid profile name: '${profile}'`, "bad_request"); } } function getHostPlatform() { if (process.platform === "darwin" || process.platform === "linux") return process.platform; return null; } function getSearchPlatforms() { const current = getHostPlatform(); const order = []; if (current) order.push(current); for (const platform of ["darwin", "linux"]) { if (!order.includes(platform)) order.push(platform); } return order; } function getDataDirForPlatform(browser, platform) { return platform === "darwin" ? browser.dataDir : browser.linuxDataDir || null; } function getBaseDir(platform) { return platform === "darwin" ? path4.join(os2.homedir(), "Library", "Application Support") : path4.join(os2.homedir(), ".config"); } function findBrowserMatch(browser, profile) { validateProfile(profile); for (const platform of getSearchPlatforms()) { const dataDir = getDataDirForPlatform(browser, platform); if (!dataDir) continue; const dbPath = path4.join(getBaseDir(platform), dataDir, profile, "Cookies"); try { if (fs3.existsSync(dbPath)) { return { browser, platform, dbPath }; } } catch {} } return null; } function getBrowserMatch(browser, profile) { const match = findBrowserMatch(browser, profile); if (match) return match; const attempted = getSearchPlatforms().map((platform) => { const dataDir = getDataDirForPlatform(browser, platform); return dataDir ? path4.join(getBaseDir(platform), dataDir, profile, "Cookies") : null; }).filter((entry) => entry !== null); throw new CookieImportError(`${browser.name} is not installed (no cookie database at ${attempted.join(" or ")})`, "not_installed"); } function openDb(dbPath, browserName) { try { return new Database(dbPath, { readonly: true }); } catch (err) { if (err.message?.includes("SQLITE_BUSY") || err.message?.includes("database is locked")) { return openDbFromCopy(dbPath, browserName); } if (err.message?.includes("SQLITE_CORRUPT") || err.message?.includes("malformed")) { throw new CookieImportError(`Cookie database for ${browserName} is corrupt`, "db_corrupt"); } throw err; } } function openDbFromCopy(dbPath, browserName) { const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto.randomUUID()}.db`; try { fs3.copyFileSync(dbPath, tmpPath); const walPath = dbPath + "-wal"; const shmPath = dbPath + "-shm"; if (fs3.existsSync(walPath)) fs3.copyFileSync(walPath, tmpPath + "-wal"); if (fs3.existsSync(shmPath)) fs3.copyFileSync(shmPath, tmpPath + "-shm"); const db = new Database(tmpPath, { readonly: true }); const origClose = db.close.bind(db); db.close = () => { origClose(); try { fs3.unlinkSync(tmpPath); } catch {} try { fs3.unlinkSync(tmpPath + "-wal"); } catch {} try { fs3.unlinkSync(tmpPath + "-shm"); } catch {} }; return db; } catch { try { fs3.unlinkSync(tmpPath); } catch {} throw new CookieImportError(`Cookie database is locked (${browserName} may be running). Try closing ${browserName} first.`, "db_locked", "retry"); } } function deriveKey(password, iterations) { return crypto.pbkdf2Sync(password, "saltysalt", iterations, 16, "sha1"); } function getCachedDerivedKey(cacheKey, password, iterations) { const cached = keyCache.get(cacheKey); if (cached) return cached; const derived = deriveKey(password, iterations); keyCache.set(cacheKey, derived); return derived; } async function getDerivedKeys(match) { if (match.platform === "darwin") { const password = await getMacKeychainPassword(match.browser.keychainService); return new Map([ ["v10", getCachedDerivedKey(`darwin:${match.browser.keychainService}:v10`, password, 1003)] ]); } const keys = new Map; keys.set("v10", getCachedDerivedKey("linux:v10", "peanuts", 1)); const linuxPassword = await getLinuxSecretPassword(match.browser); if (linuxPassword) { keys.set("v11", getCachedDerivedKey(`linux:${match.browser.keychainService}:v11`, linuxPassword, 1)); } return keys; } async function getMacKeychainPassword(service) { const proc = Bun.spawn(["security", "find-generic-password", "-s", service, "-w"], { stdout: "pipe", stderr: "pipe" }); const timeout = new Promise((_, reject) => setTimeout(() => { proc.kill(); reject(new CookieImportError(`macOS is waiting for Keychain permission. Look for a dialog asking to allow access to "${service}".`, "keychain_timeout", "retry")); }, 1e4)); try { const exitCode = await Promise.race([proc.exited, timeout]); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); if (exitCode !== 0) { const errText = stderr.trim().toLowerCase(); if (errText.includes("user canceled") || errText.includes("denied") || errText.includes("interaction not allowed")) { throw new CookieImportError(`Keychain access denied. Click "Allow" in the macOS dialog for "${service}".`, "keychain_denied", "retry"); } if (errText.includes("could not be found") || errText.includes("not found")) { throw new CookieImportError(`No Keychain entry for "${service}". Is this a Chromium-based browser?`, "keychain_not_found"); } throw new CookieImportError(`Could not read Keychain: ${stderr.trim()}`, "keychain_error", "retry"); } return stdout.trim(); } catch (err) { if (err instanceof CookieImportError) throw err; throw new CookieImportError(`Could not read Keychain: ${err.message}`, "keychain_error", "retry"); } } async function getLinuxSecretPassword(browser) { const attempts = [ ["secret-tool", "lookup", "Title", browser.keychainService] ]; if (browser.linuxApplication) { attempts.push(["secret-tool", "lookup", "xdg:schema", "chrome_libsecret_os_crypt_password_v2", "application", browser.linuxApplication], ["secret-tool", "lookup", "xdg:schema", "chrome_libsecret_os_crypt_password", "application", browser.linuxApplication]); } for (const cmd of attempts) { const password = await runPasswordLookup(cmd, 3000); if (password) return password; } return null; } async function runPasswordLookup(cmd, timeoutMs) { try { const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); const timeout = new Promise((_, reject) => setTimeout(() => { proc.kill(); reject(new Error("timeout")); }, timeoutMs)); const exitCode = await Promise.race([proc.exited, timeout]); const stdout = await new Response(proc.stdout).text(); if (exitCode !== 0) return null; const password = stdout.trim(); return password.length > 0 ? password : null; } catch { return null; } } function decryptCookieValue(row, keys) { if (row.value && row.value.length > 0) return row.value; const ev = Buffer.from(row.encrypted_value); if (ev.length === 0) return ""; const prefix = ev.slice(0, 3).toString("utf-8"); const key = keys.get(prefix); if (!key) throw new Error(`No decryption key available for ${prefix} cookies`); const ciphertext = ev.slice(3); const iv = Buffer.alloc(16, 32); const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv); const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]); if (plaintext.length <= 32) return ""; return plaintext.slice(32).toString("utf-8"); } function toPlaywrightCookie(row, value) { return { name: row.name, value, domain: row.host_key, path: row.path || "/", expires: chromiumEpochToUnix(row.expires_utc, row.has_expires), secure: row.is_secure === 1, httpOnly: row.is_httponly === 1, sameSite: mapSameSite(row.samesite) }; } function chromiumNow() { return BigInt(Date.now()) * 1000n + CHROMIUM_EPOCH_OFFSET; } function chromiumEpochToUnix(epoch, hasExpires) { if (hasExpires === 0 || epoch === 0 || epoch === 0n) return -1; const epochBig = BigInt(epoch); const unixMicro = epochBig - CHROMIUM_EPOCH_OFFSET; return Number(unixMicro / 1000000n); } function mapSameSite(value) { switch (value) { case 0: return "None"; case 1: return "Lax"; case 2: return "Strict"; default: return "Lax"; } } var CookieImportError, BROWSER_REGISTRY, keyCache, CHROMIUM_EPOCH_OFFSET = 11644473600000000n; var init_cookie_import_browser = __esm(() => { CookieImportError = class CookieImportError extends Error { code; action; constructor(message, code, action) { super(message); this.code = code; this.action = action; this.name = "CookieImportError"; } }; BROWSER_REGISTRY = [ { name: "Comet", dataDir: "Comet/", keychainService: "Comet Safe Storage", aliases: ["comet", "perplexity"] }, { name: "Chrome", dataDir: "Google/Chrome/", keychainService: "Chrome Safe Storage", aliases: ["chrome", "google-chrome", "google-chrome-stable"], linuxDataDir: "google-chrome/", linuxApplication: "chrome" }, { name: "Chromium", dataDir: "chromium/", keychainService: "Chromium Safe Storage", aliases: ["chromium"], linuxDataDir: "chromium/", linuxApplication: "chromium" }, { name: "Arc", dataDir: "Arc/User Data/", keychainService: "Arc Safe Storage", aliases: ["arc"] }, { name: "Brave", dataDir: "BraveSoftware/Brave-Browser/", keychainService: "Brave Safe Storage", aliases: ["brave"], linuxDataDir: "BraveSoftware/Brave-Browser/", linuxApplication: "brave" }, { name: "Edge", dataDir: "Microsoft Edge/", keychainService: "Microsoft Edge Safe Storage", aliases: ["edge"], linuxDataDir: "microsoft-edge/", linuxApplication: "microsoft-edge" } ]; keyCache = new Map; }); // browse/src/write-commands.ts var exports_write_commands = {}; __export(exports_write_commands, { handleWriteCommand: () => handleWriteCommand }); import * as fs4 from "fs"; import * as path5 from "path"; function validateOutputPath(filePath) { const resolved = path5.resolve(filePath); const isSafe = SAFE_DIRECTORIES2.some((dir) => isPathWithin(resolved, dir)); if (!isSafe) { throw new Error(`Path must be within: ${SAFE_DIRECTORIES2.join(", ")}`); } } async function handleWriteCommand(command, args, bm) { const page = bm.getPage(); const target = bm.getActiveFrameOrPage(); const inFrame = bm.getFrame() !== null; switch (command) { case "goto": { if (inFrame) throw new Error("Cannot use goto inside a frame. Run 'frame main' first."); const url = args[0]; if (!url) throw new Error("Usage: browse goto "); await validateNavigationUrl(url); const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 }); const status = response?.status() || "unknown"; return `Navigated to ${url} (${status})`; } case "back": { if (inFrame) throw new Error("Cannot use back inside a frame. Run 'frame main' first."); await page.goBack({ waitUntil: "domcontentloaded", timeout: 15000 }); return `Back → ${page.url()}`; } case "forward": { if (inFrame) throw new Error("Cannot use forward inside a frame. Run 'frame main' first."); await page.goForward({ waitUntil: "domcontentloaded", timeout: 15000 }); return `Forward → ${page.url()}`; } case "reload": { if (inFrame) throw new Error("Cannot use reload inside a frame. Run 'frame main' first."); await page.reload({ waitUntil: "domcontentloaded", timeout: 15000 }); return `Reloaded ${page.url()}`; } case "click": { const selector = args[0]; if (!selector) throw new Error("Usage: browse click "); const role = bm.getRefRole(selector); if (role === "option") { const resolved2 = await bm.resolveRef(selector); if ("locator" in resolved2) { const optionInfo = await resolved2.locator.evaluate((el) => { if (el.tagName !== "OPTION") return null; const option = el; const select = option.closest("select"); if (!select) return null; return { value: option.value, text: option.text }; }); if (optionInfo) { await resolved2.locator.locator("xpath=ancestor::select").selectOption(optionInfo.value, { timeout: 5000 }); return `Selected "${optionInfo.text}" (auto-routed from click on