lobster-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/browser/dom/flat-tree.ts","../../src/browser/dom/snapshot.ts","../../src/browser/dom/semantic-tree.ts","../../src/browser/dom/markdown.ts","../../src/browser/dom/form-state.ts","../../src/browser/interceptor.ts","../../src/browser/page-adapter.ts"],"sourcesContent":["/**\n * Script that runs inside the browser to extract a flat DOM tree\n * with indexed interactive elements — the format the AI agent uses.\n *\n * Based on Page Agent's DOM extraction approach.\n */\nexport const FLAT_TREE_SCRIPT = `\n(() => {\n const INTERACTIVE_TAGS = new Set([\n 'a', 'button', 'input', 'select', 'textarea', 'details', 'summary',\n 'label', 'option', 'fieldset', 'legend',\n ]);\n\n const INTERACTIVE_ROLES = new Set([\n 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox',\n 'listbox', 'menu', 'menuitem', 'tab', 'switch', 'slider',\n 'searchbox', 'spinbutton', 'option', 'menuitemcheckbox', 'menuitemradio',\n ]);\n\n const ATTR_WHITELIST = [\n 'type', 'role', 'aria-label', 'aria-expanded', 'aria-selected',\n 'aria-checked', 'aria-disabled', 'placeholder', 'title', 'href',\n 'value', 'name', 'alt', 'src',\n ];\n\n let highlightIndex = 0;\n const nodes = {};\n const selectorMap = {};\n\n function isVisible(el) {\n if (el.offsetWidth === 0 && el.offsetHeight === 0) return false;\n const style = getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;\n return true;\n }\n\n function isInteractive(el) {\n const tag = el.tagName.toLowerCase();\n if (INTERACTIVE_TAGS.has(tag)) return true;\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) return true;\n if (el.getAttribute('contenteditable') === 'true') return true;\n if (el.getAttribute('tabindex') !== null && parseInt(el.getAttribute('tabindex')) >= 0) return true;\n if (el.onclick || el.getAttribute('onclick')) return true;\n return false;\n }\n\n function getAttributes(el) {\n const attrs = {};\n for (const attr of ATTR_WHITELIST) {\n const val = el.getAttribute(attr);\n if (val !== null && val !== '') attrs[attr] = val;\n }\n return attrs;\n }\n\n function getScrollable(el) {\n const style = getComputedStyle(el);\n const overflowY = style.overflowY;\n const overflowX = style.overflowX;\n const isScrollableY = (overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight;\n const isScrollableX = (overflowX === 'auto' || overflowX === 'scroll') && el.scrollWidth > el.clientWidth;\n if (!isScrollableY && !isScrollableX) return null;\n return {\n left: el.scrollLeft,\n top: el.scrollTop,\n right: el.scrollWidth - el.clientWidth - el.scrollLeft,\n bottom: el.scrollHeight - el.clientHeight - el.scrollTop,\n };\n }\n\n function walk(el, parentId) {\n if (!el || el.nodeType === 8) return; // skip comments\n\n if (el.nodeType === 3) { // text node\n const text = el.textContent.trim();\n if (!text) return;\n const id = 'text_' + Math.random().toString(36).slice(2, 8);\n nodes[id] = { id, tagName: '#text', text, parentId };\n if (parentId && nodes[parentId]) {\n nodes[parentId].children = nodes[parentId].children || [];\n nodes[parentId].children.push(id);\n }\n return;\n }\n\n if (el.nodeType !== 1) return; // only elements\n\n const tag = el.tagName.toLowerCase();\n if (['script', 'style', 'noscript', 'svg', 'path'].includes(tag)) return;\n if (!isVisible(el)) return;\n\n const id = tag + '_' + Math.random().toString(36).slice(2, 8);\n const interactive = isInteractive(el);\n const node = {\n id,\n tagName: tag,\n attributes: getAttributes(el),\n parentId,\n children: [],\n isInteractive: interactive,\n };\n\n if (interactive) {\n node.highlightIndex = highlightIndex;\n selectorMap[highlightIndex] = id;\n highlightIndex++;\n }\n\n const scrollable = getScrollable(el);\n if (scrollable) node.scrollable = scrollable;\n\n const text = [];\n for (const child of el.childNodes) {\n if (child.nodeType === 3 && child.textContent.trim()) {\n text.push(child.textContent.trim());\n }\n }\n if (text.length > 0) node.text = text.join(' ').slice(0, 200);\n\n nodes[id] = node;\n\n if (parentId && nodes[parentId]) {\n nodes[parentId].children.push(id);\n }\n\n for (const child of el.children) {\n walk(child, id);\n }\n }\n\n const rootId = 'root';\n nodes[rootId] = { id: rootId, tagName: 'body', children: [], attributes: {} };\n for (const child of document.body.children) {\n walk(child, rootId);\n }\n\n return { rootId, map: nodes, selectorMap };\n})()\n`;\n\n/**\n * Convert a FlatDomTree into the indexed text format that the LLM agent reads.\n * Example output:\n * [0]<button type=submit>Search</>\n * [1]<input type=text placeholder=\"Enter query\" />\n */\nexport function flatTreeToString(tree: { rootId: string; map: Record<string, any> }): string {\n const lines: string[] = [];\n\n function walk(nodeId: string, depth: number) {\n const node = tree.map[nodeId];\n if (!node) return;\n\n const indent = '\\t'.repeat(depth);\n\n if (node.tagName === '#text') {\n if (node.text) lines.push(`${indent}${node.text}`);\n return;\n }\n\n const attrs = node.attributes || {};\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => (v === '' ? k : `${k}=\"${v}\"`))\n .join(' ');\n\n const prefix = node.highlightIndex !== undefined ? `[${node.highlightIndex}]` : '';\n const scrollInfo = node.scrollable\n ? ` |scroll: ${Math.round(node.scrollable.top)}px up, ${Math.round(node.scrollable.bottom)}px down|`\n : '';\n\n const text = node.text || '';\n const tag = node.tagName;\n\n if (prefix || text || node.children?.length > 0) {\n const opening = `${indent}${prefix}<${tag}${attrStr ? ' ' + attrStr : ''}${scrollInfo}>`;\n\n if (!node.children?.length || (node.children.length === 0 && text)) {\n lines.push(`${opening}${text}</>`);\n } else {\n lines.push(`${opening}${text}`);\n for (const childId of node.children || []) {\n walk(childId, depth + 1);\n }\n }\n } else {\n for (const childId of node.children || []) {\n walk(childId, depth);\n }\n }\n }\n\n walk(tree.rootId, 0);\n return lines.join('\\n');\n}\n","/**\n * Advanced DOM snapshot script — runs inside the browser.\n * Multi-stage pruning pipeline producing LLM-optimized output.\n *\n * Stages:\n * 1. Walk DOM, collect visibility + layout + interactivity signals\n * 2. Prune invisible, zero-area, non-content elements\n * 3. SVG & decoration collapse\n * 4. Shadow DOM traversal\n * 5. Same-origin iframe extraction\n * 6. Bounding-box parent-child dedup (link/button wrapping)\n * 7. Paint-order occlusion detection (overlay/modal coverage)\n * 8. Attribute whitelist filtering\n * 9. Ad/tracker filtering\n * 10. Scroll position info\n * 11. data-ref annotation for targeting\n * 12. Token-efficient serialization with interactive indices\n */\n/**\n * Build snapshot script with optional previous hashes for diff marking.\n * Elements new since last snapshot get a `*` prefix on their index.\n */\nexport function buildSnapshotScript(previousHashes?: string[]): string {\n return SNAPSHOT_SCRIPT_FN(previousHashes || []);\n}\n\nfunction SNAPSHOT_SCRIPT_FN(prevHashes: string[]): string {\n return `\n(() => {\n let idx = 0;\n const __prevHashes = new Set(${JSON.stringify(prevHashes)});\n const __currentHashes = [];\n`;\n}\n\nexport const SNAPSHOT_SCRIPT = `\n(() => {\n let idx = 0;\n const __prevHashes = (window.__lobster_prev_hashes) ? new Set(window.__lobster_prev_hashes) : null;\n const __currentHashes = [];\n\n const SKIP_TAGS = new Set([\n 'script','style','noscript','svg','path','meta','link','head',\n 'template','slot','colgroup','col',\n ]);\n\n const INTERACTIVE_TAGS = new Set([\n 'a','button','input','select','textarea','details','summary','label',\n ]);\n\n const INTERACTIVE_ROLES = new Set([\n 'button','link','textbox','checkbox','radio','combobox','listbox',\n 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',\n 'option','menuitemcheckbox','menuitemradio','treeitem',\n ]);\n\n const ATTR_WHITELIST = [\n 'type','role','aria-label','aria-expanded','aria-selected','aria-checked',\n 'aria-disabled','aria-haspopup','aria-pressed','placeholder','title',\n 'href','value','name','alt','src','action','method','for',\n 'data-testid','data-id','contenteditable','tabindex',\n ];\n\n const AD_PATTERNS = /ad[-_]?banner|ad[-_]?container|google[-_]?ad|doubleclick|adsbygoogle|sponsored|^ad$/i;\n\n // ── Stage 1: Visibility check ──\n function isVisible(el) {\n if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;\n const s = getComputedStyle(el);\n if (s.display === 'none') return false;\n if (s.visibility === 'hidden' || s.visibility === 'collapse') return false;\n if (s.opacity === '0') return false;\n if (s.clipPath === 'inset(100%)') return false;\n // Check for offscreen positioning\n const rect = el.getBoundingClientRect();\n if (rect.right < 0 || rect.bottom < 0) return false;\n return true;\n }\n\n // ── Stage 2: Interactive detection ──\n function isInteractive(el) {\n const tag = el.tagName.toLowerCase();\n if (INTERACTIVE_TAGS.has(tag)) {\n // Skip disabled elements\n if (el.disabled) return false;\n // Skip hidden inputs\n if (tag === 'input' && el.type === 'hidden') return false;\n return true;\n }\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) return true;\n if (el.contentEditable === 'true') return true;\n if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;\n if (el.onclick) return true;\n return false;\n }\n\n // ── Stage 8: Attribute filtering ──\n function getAttrs(el) {\n const parts = [];\n for (const name of ATTR_WHITELIST) {\n let v = el.getAttribute(name);\n if (v === null || v === '') continue;\n // Truncate long values\n if (v.length > 80) v = v.slice(0, 77) + '...';\n // Skip href=\"javascript:...\"\n if (name === 'href' && v.startsWith('javascript:')) continue;\n parts.push(name + '=' + v);\n }\n return parts.length ? ' ' + parts.join(' ') : '';\n }\n\n // ── Stage 9: Ad filtering ──\n function isAd(el) {\n const id = el.id || '';\n const cls = el.className || '';\n if (typeof cls === 'string' && AD_PATTERNS.test(cls)) return true;\n if (AD_PATTERNS.test(id)) return true;\n if (el.tagName === 'IFRAME' && AD_PATTERNS.test(el.src || '')) return true;\n return false;\n }\n\n // ── Stage 10: Scroll info ──\n function getScrollInfo(el) {\n const s = getComputedStyle(el);\n const overflowY = s.overflowY;\n const overflowX = s.overflowX;\n const scrollableY = (overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight;\n const scrollableX = (overflowX === 'auto' || overflowX === 'scroll') && el.scrollWidth > el.clientWidth;\n if (!scrollableY && !scrollableX) return '';\n\n const parts = [];\n if (scrollableY) {\n const up = Math.round(el.scrollTop);\n const down = Math.round(el.scrollHeight - el.clientHeight - el.scrollTop);\n if (up > 0) parts.push(up + 'px up');\n if (down > 0) parts.push(down + 'px down');\n }\n if (scrollableX) {\n const left = Math.round(el.scrollLeft);\n const right = Math.round(el.scrollWidth - el.clientWidth - el.scrollLeft);\n if (left > 0) parts.push(left + 'px left');\n if (right > 0) parts.push(right + 'px right');\n }\n return parts.length ? ' |scroll: ' + parts.join(', ') + '|' : '';\n }\n\n // ── Stage 6: Bounding-box dedup ──\n // If a parent and child are both interactive and have ~same bounding box,\n // skip the parent (e.g., <a><button>Click</button></a>)\n function isWrappingInteractive(el) {\n if (!isInteractive(el)) return false;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return false;\n for (const child of el.children) {\n if (!isInteractive(child)) continue;\n const cr = child.getBoundingClientRect();\n const overlapX = Math.min(rect.right, cr.right) - Math.max(rect.left, cr.left);\n const overlapY = Math.min(rect.bottom, cr.bottom) - Math.max(rect.top, cr.top);\n const overlapArea = Math.max(0, overlapX) * Math.max(0, overlapY);\n const parentArea = rect.width * rect.height;\n if (parentArea > 0 && overlapArea / parentArea > 0.85) return true;\n }\n return false;\n }\n\n // ── Stage 7: Occlusion detection ──\n function isOccluded(el) {\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return false;\n const cx = rect.left + rect.width / 2;\n const cy = rect.top + rect.height / 2;\n const topEl = document.elementFromPoint(cx, cy);\n if (!topEl) return false;\n if (topEl === el || el.contains(topEl) || topEl.contains(el)) return false;\n // Check z-index — if top element is a modal/overlay, mark as occluded\n const topZ = parseInt(getComputedStyle(topEl).zIndex) || 0;\n const elZ = parseInt(getComputedStyle(el).zIndex) || 0;\n return topZ > elZ + 10;\n }\n\n // ── Stage 5: Iframe content extraction ──\n function getIframeContent(iframe, depth, maxDepth) {\n try {\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) return '';\n return '\\\\n' + walkNode(doc.body, depth, maxDepth);\n } catch { return ''; }\n }\n\n // ── Stage 4: Shadow DOM traversal ──\n function getShadowContent(el, depth, maxDepth) {\n if (!el.shadowRoot) return '';\n let out = '';\n for (const child of el.shadowRoot.childNodes) {\n out += walkNode(child, depth, maxDepth);\n }\n return out;\n }\n\n // ── Input value hint ──\n function getInputHint(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === 'input') {\n const type = el.type || 'text';\n const val = el.value || '';\n const checked = el.checked;\n if (type === 'checkbox' || type === 'radio') {\n return checked ? ' [checked]' : ' [unchecked]';\n }\n if (val) return ' value=\"' + val.slice(0, 50) + '\"';\n }\n if (tag === 'textarea' && el.value) {\n return ' value=\"' + el.value.slice(0, 50) + '\"';\n }\n if (tag === 'select' && el.selectedOptions?.length) {\n return ' selected=\"' + el.selectedOptions[0].text.slice(0, 40) + '\"';\n }\n return '';\n }\n\n const MAX_DEPTH = 25;\n const MAX_TEXT = 150;\n\n function walkNode(node, depth, maxDepth) {\n if (depth > maxDepth) return '';\n if (!node) return '';\n\n // Text node\n if (node.nodeType === 3) {\n const t = node.textContent.trim();\n if (!t) return '';\n const text = t.length > MAX_TEXT ? t.slice(0, MAX_TEXT) + '...' : t;\n return ' '.repeat(depth) + text + '\\\\n';\n }\n\n // Comment node — skip\n if (node.nodeType === 8) return '';\n\n // Only element nodes from here\n if (node.nodeType !== 1) return '';\n\n const el = node;\n const tag = el.tagName.toLowerCase();\n\n // ── Stage 3: Skip tags ──\n if (SKIP_TAGS.has(tag)) return '';\n\n // ── Stage 2: Visibility ──\n if (!isVisible(el)) return '';\n\n // ── Stage 9: Ad filtering ──\n if (isAd(el)) return '';\n\n // ── Stage 6: Bbox dedup — skip wrapping interactive parent ──\n const skipSelf = isWrappingInteractive(el);\n\n const indent = ' '.repeat(depth);\n const inter = !skipSelf && isInteractive(el);\n let prefix = '';\n if (inter) {\n const thisIdx = idx++;\n // Hash: tag + text + key attributes for diff tracking\n const hashText = tag + ':' + (el.textContent || '').trim().slice(0, 40) + ':' + (el.getAttribute('href') || '') + ':' + (el.getAttribute('aria-label') || '');\n __currentHashes.push(hashText);\n const isNew = __prevHashes && __prevHashes.size > 0 && !__prevHashes.has(hashText);\n prefix = isNew ? '*[' + thisIdx + ']' : '[' + thisIdx + ']';\n }\n\n // ── Stage 11: Annotate with data-ref ──\n if (inter) {\n try { el.dataset.ref = String(idx - 1); } catch {}\n }\n\n // ── Stage 7: Occlusion check for interactive elements ──\n if (inter && isOccluded(el)) {\n // Still include but mark as occluded\n // (agent needs to know element exists but may need to scroll/close modal)\n }\n\n const a = getAttrs(el);\n const scrollInfo = getScrollInfo(el);\n const inputHint = inter ? getInputHint(el) : '';\n\n // Leaf text extraction\n let leafText = '';\n if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {\n const t = el.childNodes[0].textContent.trim();\n if (t) leafText = t.length > MAX_TEXT ? t.slice(0, MAX_TEXT) + '...' : t;\n }\n\n // ── Stage 5: Iframe ──\n if (tag === 'iframe') {\n const iframeContent = getIframeContent(el, depth + 1, maxDepth);\n if (iframeContent) {\n return indent + prefix + '<iframe' + a + '>\\\\n' + iframeContent;\n }\n return '';\n }\n\n // Build output\n let out = '';\n\n if (skipSelf) {\n // Skip self but render children\n for (const c of el.childNodes) out += walkNode(c, depth, maxDepth);\n out += getShadowContent(el, depth, maxDepth);\n return out;\n }\n\n if (inter || leafText || el.children.length === 0) {\n if (leafText) {\n out = indent + prefix + '<' + tag + a + scrollInfo + inputHint + '>' + leafText + '</' + tag + '>\\\\n';\n } else {\n out = indent + prefix + '<' + tag + a + scrollInfo + inputHint + '>\\\\n';\n for (const c of el.childNodes) out += walkNode(c, depth + 1, maxDepth);\n out += getShadowContent(el, depth + 1, maxDepth);\n }\n } else {\n // Non-interactive container — flatten depth if no useful info\n if (scrollInfo) {\n out = indent + '<' + tag + scrollInfo + '>\\\\n';\n for (const c of el.childNodes) out += walkNode(c, depth + 1, maxDepth);\n out += getShadowContent(el, depth + 1, maxDepth);\n } else {\n for (const c of el.childNodes) out += walkNode(c, depth, maxDepth);\n out += getShadowContent(el, depth, maxDepth);\n }\n }\n\n return out;\n }\n\n // ── Page-level scroll info header ──\n const scrollY = window.scrollY;\n const scrollMax = document.documentElement.scrollHeight - window.innerHeight;\n const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const pageH = document.documentElement.scrollHeight;\n\n let header = '';\n header += 'viewport: ' + vpW + 'x' + vpH + ' | page_height: ' + pageH + 'px';\n header += ' | scroll: ' + scrollPct + '%';\n if (scrollY > 50) header += ' (' + Math.round(scrollY) + 'px from top)';\n if (scrollMax - scrollY > 50) header += ' (' + Math.round(scrollMax - scrollY) + 'px more below)';\n header += '\\\\n---\\\\n';\n\n // Store current hashes for next diff comparison\n window.__lobster_prev_hashes = __currentHashes;\n\n return header + walkNode(document.body, 0, MAX_DEPTH);\n})()\n`;\n","/**\n * Semantic tree — W3C accessible name algorithm, XPath, listener detection.\n *\n * Based on Lightpanda's SemanticTree.zig approach:\n * - Accessible name: aria-labelledby → aria-label → alt → title → placeholder → text content\n * - XPath generation for element location\n * - Interactive classification: native, aria, contenteditable, listener, focusable\n * - Disabled state with fieldset inheritance\n * - Input value, option, checked state extraction\n */\nexport const SEMANTIC_TREE_SCRIPT = `\n(() => {\n const SKIP = new Set(['script','style','noscript','svg','head','meta','link','template']);\n\n const ROLE_MAP = {\n a: 'link', button: 'button', input: 'textbox', select: 'combobox',\n textarea: 'textbox', h1: 'heading', h2: 'heading', h3: 'heading',\n h4: 'heading', h5: 'heading', h6: 'heading', nav: 'navigation',\n main: 'main', header: 'banner', footer: 'contentinfo', aside: 'complementary',\n form: 'form', table: 'table', img: 'img', ul: 'list', ol: 'list', li: 'listitem',\n section: 'region', article: 'article', dialog: 'dialog', details: 'group',\n summary: 'button', progress: 'progressbar', meter: 'meter', output: 'status',\n label: 'label', legend: 'legend', fieldset: 'group', option: 'option',\n tr: 'row', td: 'cell', th: 'columnheader', caption: 'caption',\n };\n\n const INTERACTIVE_ROLES = new Set([\n 'button','link','textbox','checkbox','radio','combobox','listbox',\n 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',\n 'option','menuitemcheckbox','menuitemradio','treeitem',\n ]);\n\n // ── W3C Accessible Name Algorithm (simplified) ──\n function getAccessibleName(el) {\n // 1. aria-labelledby (highest priority)\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const ids = labelledBy.split(/\\\\s+/);\n const parts = ids.map(id => {\n const ref = document.getElementById(id);\n return ref ? ref.textContent.trim() : '';\n }).filter(Boolean);\n if (parts.length > 0) return parts.join(' ').slice(0, 120);\n }\n\n // 2. aria-label\n const ariaLabel = el.getAttribute('aria-label');\n if (ariaLabel) return ariaLabel.slice(0, 120);\n\n // 3. alt (for images)\n const alt = el.getAttribute('alt');\n if (alt) return alt.slice(0, 120);\n\n // 4. title\n const title = el.getAttribute('title');\n if (title) return title.slice(0, 120);\n\n // 5. placeholder (for inputs)\n const placeholder = el.getAttribute('placeholder');\n if (placeholder) return placeholder.slice(0, 120);\n\n // 6. value (for buttons)\n if (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button')) {\n const val = el.getAttribute('value');\n if (val) return val.slice(0, 120);\n }\n\n // 7. Associated label\n if (el.id) {\n const label = document.querySelector('label[for=\"' + el.id + '\"]');\n if (label) return label.textContent.trim().slice(0, 120);\n }\n\n // 8. Direct text content (only for leaf-ish elements)\n if (el.children.length <= 2) {\n const text = el.textContent.trim();\n if (text && text.length < 120) return text;\n }\n\n return '';\n }\n\n // ── XPath generation ──\n function getXPath(el) {\n const parts = [];\n let current = el;\n while (current && current.nodeType === 1) {\n let index = 1;\n let sibling = current.previousElementSibling;\n while (sibling) {\n if (sibling.tagName === current.tagName) index++;\n sibling = sibling.previousElementSibling;\n }\n const tag = current.tagName.toLowerCase();\n parts.unshift(tag + '[' + index + ']');\n current = current.parentElement;\n }\n return '/' + parts.join('/');\n }\n\n // ── Interactivity classification ──\n function classifyInteractivity(el) {\n const types = [];\n const tag = el.tagName.toLowerCase();\n\n // Native\n if (['a','button','input','select','textarea','details','summary'].includes(tag)) {\n if (tag === 'a' && !el.href) {} // anchor without href is not interactive\n else if (tag === 'input' && el.type === 'hidden') {} // hidden inputs\n else types.push('native');\n }\n\n // ARIA role\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) types.push('aria');\n\n // Contenteditable\n if (el.contentEditable === 'true') types.push('contenteditable');\n\n // Focusable\n if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) types.push('focusable');\n\n // Event listeners (check onclick and common inline handlers)\n if (el.onclick || el.onmousedown || el.onkeydown || el.onkeypress ||\n el.getAttribute('onclick') || el.getAttribute('onmousedown')) {\n types.push('listener');\n }\n\n return types;\n }\n\n // ── Disabled state with fieldset inheritance ──\n function isDisabled(el) {\n if (el.disabled) return true;\n // Check fieldset disabled inheritance\n let parent = el.parentElement;\n while (parent) {\n if (parent.tagName === 'FIELDSET' && parent.disabled) {\n // Exception: elements inside the first legend child are NOT disabled\n const firstLegend = parent.querySelector(':scope > legend');\n if (firstLegend && firstLegend.contains(el)) return false;\n return true;\n }\n parent = parent.parentElement;\n }\n return false;\n }\n\n // ── Walk the DOM ──\n function walk(el, depth, maxDepth) {\n if (!el || depth > maxDepth) return '';\n\n if (el.nodeType === 3) {\n const t = el.textContent.trim();\n return t ? ' '.repeat(depth) + 'text \"' + t.slice(0, 100) + '\"\\\\n' : '';\n }\n\n if (el.nodeType !== 1) return '';\n const tag = el.tagName.toLowerCase();\n if (SKIP.has(tag)) return '';\n\n const style = getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden') return '';\n\n const indent = ' '.repeat(depth);\n const role = el.getAttribute('role') || ROLE_MAP[tag] || '';\n const name = getAccessibleName(el);\n const interTypes = classifyInteractivity(el);\n const interactive = interTypes.length > 0;\n const disabled = interactive && isDisabled(el);\n\n let line = indent;\n line += role || tag;\n\n if (name) line += ' \"' + name.slice(0, 80) + '\"';\n\n if (interactive) {\n line += ' [' + interTypes.join(',') + ']';\n if (disabled) line += ' {disabled}';\n line += ' xpath=' + getXPath(el);\n }\n\n // Input state\n if (tag === 'input') {\n const type = el.type || 'text';\n line += ' type=' + type;\n if (type === 'checkbox' || type === 'radio') {\n line += el.checked ? ' [checked]' : ' [unchecked]';\n } else if (el.value) {\n line += ' value=\"' + el.value.slice(0, 50) + '\"';\n }\n }\n if (tag === 'textarea' && el.value) {\n line += ' value=\"' + el.value.slice(0, 50) + '\"';\n }\n if (tag === 'select') {\n const opts = Array.from(el.options || []).map(o => ({\n text: o.text.slice(0, 30),\n value: o.value,\n selected: o.selected,\n }));\n const selected = opts.find(o => o.selected);\n if (selected) line += ' selected=\"' + selected.text + '\"';\n if (opts.length <= 10) {\n line += ' options=[' + opts.map(o => o.text).join('|') + ']';\n }\n }\n\n line += '\\\\n';\n\n let out = line;\n for (const c of el.childNodes) {\n out += walk(c, depth + 1, maxDepth);\n }\n\n // Shadow DOM\n if (el.shadowRoot) {\n for (const c of el.shadowRoot.childNodes) {\n out += walk(c, depth + 1, maxDepth);\n }\n }\n\n return out;\n }\n\n return walk(document.body, 0, 20);\n})()\n`;\n","/**\n * DOM-to-Markdown converter — runs inside the browser.\n *\n * Full-featured conversion based on Lightpanda's markdown.zig:\n * - Table support with header separator rows\n * - URL resolution (relative → absolute)\n * - Nested ordered/unordered lists with proper indentation\n * - Character escaping for Markdown special chars\n * - Strikethrough, code blocks, blockquotes\n * - Smart anchor handling (inline vs block)\n * - Whitespace collapsing\n */\nexport const MARKDOWN_SCRIPT = `\n(() => {\n const SKIP = new Set(['script','style','noscript','svg','head','template']);\n const baseUrl = location.href;\n\n // Resolve relative URLs to absolute\n function resolveUrl(href) {\n if (!href || href.startsWith('javascript:') || href.startsWith('#')) return href;\n try { return new URL(href, baseUrl).href; } catch { return href; }\n }\n\n // Escape Markdown special chars in text\n function escapeText(text) {\n return text\n .replace(/\\\\\\\\/g, '\\\\\\\\\\\\\\\\')\n .replace(/([*_~\\`\\\\[\\\\]|])/g, '\\\\\\\\$1');\n }\n\n // State tracking\n let listDepth = 0;\n let orderedCounters = [];\n let inPre = false;\n let inTable = false;\n\n function listIndent() { return ' '.repeat(listDepth); }\n\n function walk(el) {\n if (!el) return '';\n\n // Text node\n if (el.nodeType === 3) {\n const text = el.textContent || '';\n if (inPre) return text;\n // Collapse whitespace\n const collapsed = text.replace(/\\\\s+/g, ' ');\n return collapsed === ' ' && !el.previousSibling && !el.nextSibling ? '' : collapsed;\n }\n\n if (el.nodeType !== 1) return '';\n const tag = el.tagName.toLowerCase();\n if (SKIP.has(tag)) return '';\n\n // Visibility check\n try {\n const s = getComputedStyle(el);\n if (s.display === 'none' || s.visibility === 'hidden') return '';\n } catch {}\n\n // Get children content\n function childContent() {\n let out = '';\n for (const c of el.childNodes) out += walk(c);\n return out;\n }\n\n switch (tag) {\n // ── Headings ──\n case 'h1': return '\\\\n\\\\n# ' + childContent().trim() + '\\\\n\\\\n';\n case 'h2': return '\\\\n\\\\n## ' + childContent().trim() + '\\\\n\\\\n';\n case 'h3': return '\\\\n\\\\n### ' + childContent().trim() + '\\\\n\\\\n';\n case 'h4': return '\\\\n\\\\n#### ' + childContent().trim() + '\\\\n\\\\n';\n case 'h5': return '\\\\n\\\\n##### ' + childContent().trim() + '\\\\n\\\\n';\n case 'h6': return '\\\\n\\\\n###### ' + childContent().trim() + '\\\\n\\\\n';\n\n // ── Block elements ──\n case 'p': return '\\\\n\\\\n' + childContent().trim() + '\\\\n\\\\n';\n case 'br': return '\\\\n';\n case 'hr': return '\\\\n\\\\n---\\\\n\\\\n';\n\n // ── Inline formatting ──\n case 'strong': case 'b': {\n const inner = childContent().trim();\n return inner ? '**' + inner + '**' : '';\n }\n case 'em': case 'i': {\n const inner = childContent().trim();\n return inner ? '*' + inner + '*' : '';\n }\n case 's': case 'del': case 'strike': {\n const inner = childContent().trim();\n return inner ? '~~' + inner + '~~' : '';\n }\n case 'code': {\n if (inPre) return childContent();\n const inner = childContent();\n return inner ? '\\\\x60' + inner + '\\\\x60' : '';\n }\n\n // ── Code blocks ──\n case 'pre': {\n inPre = true;\n const inner = childContent();\n inPre = false;\n const lang = el.querySelector('code')?.className?.match(/language-(\\\\w+)/)?.[1] || '';\n return '\\\\n\\\\n\\\\x60\\\\x60\\\\x60' + lang + '\\\\n' + inner.trim() + '\\\\n\\\\x60\\\\x60\\\\x60\\\\n\\\\n';\n }\n\n // ── Links ──\n case 'a': {\n const href = resolveUrl(el.getAttribute('href') || '');\n const inner = childContent().trim();\n const name = inner || el.getAttribute('aria-label') || el.getAttribute('title') || '';\n if (!name) return '';\n if (!href || href === '#' || href.startsWith('javascript:')) return name;\n return '[' + name + '](' + href + ')';\n }\n\n // ── Images ──\n case 'img': {\n const alt = el.getAttribute('alt') || '';\n const src = resolveUrl(el.getAttribute('src') || '');\n return src ? '![' + alt + '](' + src + ')' : '';\n }\n\n // ── Lists ──\n case 'ul': {\n listDepth++;\n orderedCounters.push(0);\n const inner = childContent();\n listDepth--;\n orderedCounters.pop();\n return '\\\\n' + inner;\n }\n case 'ol': {\n listDepth++;\n orderedCounters.push(0);\n const inner = childContent();\n listDepth--;\n orderedCounters.pop();\n return '\\\\n' + inner;\n }\n case 'li': {\n const parent = el.parentElement?.tagName?.toLowerCase();\n const isOrdered = parent === 'ol';\n const inner = childContent().trim();\n if (!inner) return '';\n if (isOrdered) {\n const counter = orderedCounters.length > 0\n ? ++orderedCounters[orderedCounters.length - 1] : 1;\n return listIndent() + counter + '. ' + inner + '\\\\n';\n }\n return listIndent() + '- ' + inner + '\\\\n';\n }\n\n // ── Blockquote ──\n case 'blockquote': {\n const inner = childContent().trim();\n if (!inner) return '';\n return '\\\\n\\\\n' + inner.split('\\\\n').map(line => '> ' + line).join('\\\\n') + '\\\\n\\\\n';\n }\n\n // ── Tables ──\n case 'table': {\n inTable = true;\n let out = '\\\\n\\\\n';\n const rows = el.querySelectorAll('tr');\n let headerDone = false;\n\n for (let i = 0; i < rows.length; i++) {\n const cells = rows[i].querySelectorAll('th, td');\n const isHeader = rows[i].querySelector('th') !== null;\n const cellTexts = [];\n for (const cell of cells) {\n let cellText = '';\n for (const c of cell.childNodes) cellText += walk(c);\n cellTexts.push(cellText.trim().replace(/\\\\|/g, '\\\\\\\\|').replace(/\\\\n/g, ' '));\n }\n\n out += '| ' + cellTexts.join(' | ') + ' |\\\\n';\n\n if (isHeader && !headerDone) {\n out += '| ' + cellTexts.map(() => '---').join(' | ') + ' |\\\\n';\n headerDone = true;\n }\n\n // First data row without headers — synthesize separator\n if (i === 0 && !isHeader && !headerDone) {\n out += '| ' + cellTexts.map(() => '---').join(' | ') + ' |\\\\n';\n headerDone = true;\n }\n }\n\n inTable = false;\n return out + '\\\\n';\n }\n case 'thead': case 'tbody': case 'tfoot':\n return childContent();\n case 'tr': case 'td': case 'th':\n // Handled by table walker above; fallback for orphaned elements\n return childContent();\n\n // ── Definition lists ──\n case 'dl': return '\\\\n\\\\n' + childContent() + '\\\\n\\\\n';\n case 'dt': return '\\\\n**' + childContent().trim() + '**\\\\n';\n case 'dd': return ': ' + childContent().trim() + '\\\\n';\n\n // ── Figure ──\n case 'figure': return '\\\\n\\\\n' + childContent().trim() + '\\\\n\\\\n';\n case 'figcaption': return '\\\\n*' + childContent().trim() + '*\\\\n';\n\n // ── Details/Summary ──\n case 'details': return '\\\\n\\\\n' + childContent() + '\\\\n\\\\n';\n case 'summary': return '**' + childContent().trim() + '**\\\\n\\\\n';\n\n // ── Generic blocks ──\n case 'div': case 'section': case 'article': case 'main': case 'aside':\n case 'header': case 'footer': case 'nav':\n return '\\\\n' + childContent() + '\\\\n';\n\n case 'span': case 'small': case 'sub': case 'sup': case 'abbr':\n case 'time': case 'mark': case 'cite': case 'q':\n return childContent();\n\n default:\n return childContent();\n }\n }\n\n const raw = walk(document.body);\n // Clean up: collapse 3+ newlines to 2, trim\n return raw.replace(/\\\\n{3,}/g, '\\\\n\\\\n').replace(/^\\\\n+|\\\\n+$/g, '').trim();\n})()\n`;\n","/**\n * Form state extraction — runs inside the browser.\n *\n * Extracts all form fields (including orphan fields not in <form> tags),\n * their types, labels, values, required/disabled state.\n *\n * Based on OpenCLI's getFormStateJs() pattern.\n */\nexport const FORM_STATE_SCRIPT = `\n(() => {\n function extractField(el) {\n const tag = el.tagName.toLowerCase();\n const type = (el.getAttribute('type') || tag).toLowerCase();\n\n // Skip non-user-facing inputs\n if (['hidden', 'submit', 'button', 'reset', 'image'].includes(type)) return null;\n\n const name = el.name || el.id || '';\n\n // Find label via multiple strategies\n const label =\n el.getAttribute('aria-label') ||\n (el.id ? document.querySelector('label[for=\"' + el.id + '\"]')?.textContent?.trim() : null) ||\n el.closest('label')?.textContent?.trim() ||\n el.placeholder ||\n '';\n\n // Extract value based on type\n let value;\n if (tag === 'select') {\n const selected = el.options[el.selectedIndex];\n value = selected ? selected.textContent.trim() : '';\n } else if (type === 'checkbox' || type === 'radio') {\n value = el.checked;\n } else if (type === 'password') {\n value = el.value ? '••••' : '';\n } else if (el.isContentEditable) {\n value = el.textContent?.trim()?.slice(0, 200) || '';\n } else {\n value = el.value || '';\n }\n\n return {\n tag,\n type,\n name,\n label: label.slice(0, 80),\n value: typeof value === 'string' ? value.slice(0, 200) : value,\n required: !!el.required,\n disabled: !!el.disabled,\n ref: el.dataset?.ref || null,\n };\n }\n\n const result = { forms: [], orphanFields: [] };\n\n // Collect forms\n for (const form of document.forms) {\n const fields = [];\n for (const el of form.elements) {\n const field = extractField(el);\n if (field) fields.push(field);\n }\n result.forms.push({\n id: form.id || '',\n name: form.name || '',\n action: form.action || '',\n method: (form.method || 'get').toUpperCase(),\n fields,\n });\n }\n\n // Collect orphan fields (not in a <form>)\n const allInputs = document.querySelectorAll(\n 'input, textarea, select, [contenteditable=\"true\"]'\n );\n for (const el of allInputs) {\n if (!el.form) {\n const field = extractField(el);\n if (field) result.orphanFields.push(field);\n }\n }\n\n return result;\n})()\n`;\n","/**\n * Network interceptor script — patches fetch and XHR to capture responses.\n * Based on OpenCLI's interception approach.\n */\nexport function buildInterceptorScript(pattern: string): string {\n return `\n(() => {\n if (window.__lobster_interceptor__) return;\n window.__lobster_interceptor__ = { requests: [] };\n const store = window.__lobster_interceptor__;\n const pattern = ${JSON.stringify(pattern)};\n\n // Patch fetch\n const origFetch = window.fetch;\n window.fetch = async function(...args) {\n const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';\n const resp = await origFetch.apply(this, args);\n if (url.includes(pattern)) {\n const clone = resp.clone();\n try {\n const body = await clone.json();\n store.requests.push({ url, method: 'GET', status: resp.status, body, timestamp: Date.now() });\n } catch {}\n }\n return resp;\n };\n\n // Patch XHR\n const origOpen = XMLHttpRequest.prototype.open;\n const origSend = XMLHttpRequest.prototype.send;\n XMLHttpRequest.prototype.open = function(method, url, ...rest) {\n this.__url = url;\n this.__method = method;\n return origOpen.call(this, method, url, ...rest);\n };\n XMLHttpRequest.prototype.send = function(...args) {\n this.addEventListener('load', function() {\n if (this.__url && this.__url.includes(pattern)) {\n try {\n const body = JSON.parse(this.responseText);\n store.requests.push({ url: this.__url, method: this.__method, status: this.status, body, timestamp: Date.now() });\n } catch {}\n }\n });\n return origSend.apply(this, args);\n };\n})()\n`;\n}\n\nexport const GET_INTERCEPTED_SCRIPT = `\n(() => {\n const store = window.__lobster_interceptor__;\n if (!store) return [];\n const reqs = [...store.requests];\n store.requests = [];\n return reqs;\n})()\n`;\n","import type { Page } from 'puppeteer-core';\nimport type {\n IPage, WaitCondition, Cookie, NetworkEntry, TabInfo,\n SnapshotOptions, SemanticTreeOptions, FlatDomTree, BrowserState, FormState,\n} from '../types/page.js';\nimport { FLAT_TREE_SCRIPT, flatTreeToString } from './dom/flat-tree.js';\nimport { SNAPSHOT_SCRIPT } from './dom/snapshot.js';\nimport { SEMANTIC_TREE_SCRIPT } from './dom/semantic-tree.js';\nimport { MARKDOWN_SCRIPT } from './dom/markdown.js';\nimport { FORM_STATE_SCRIPT } from './dom/form-state.js';\nimport { buildInterceptorScript, GET_INTERCEPTED_SCRIPT } from './interceptor.js';\n\nexport class PuppeteerPage implements IPage {\n private page: Page;\n\n constructor(page: Page) {\n this.page = page;\n }\n\n get raw(): Page { return this.page; }\n\n async goto(url: string, options?: { waitUntil?: WaitCondition; timeout?: number }): Promise<void> {\n await this.page.goto(url, {\n waitUntil: (options?.waitUntil as any) || 'networkidle2',\n timeout: options?.timeout || 30000,\n });\n }\n\n async goBack(): Promise<void> {\n await this.page.goBack({ waitUntil: 'networkidle2' });\n }\n\n async url(): Promise<string> {\n return this.page.url();\n }\n\n async title(): Promise<string> {\n return this.page.title();\n }\n\n async evaluate<T = unknown>(js: string): Promise<T> {\n return this.page.evaluate(js) as Promise<T>;\n }\n\n async snapshot(_opts?: SnapshotOptions): Promise<string> {\n return this.page.evaluate(SNAPSHOT_SCRIPT) as Promise<string>;\n }\n\n async semanticTree(_opts?: SemanticTreeOptions): Promise<string> {\n return this.page.evaluate(SEMANTIC_TREE_SCRIPT) as Promise<string>;\n }\n\n async flatTree(): Promise<FlatDomTree> {\n const raw = await this.page.evaluate(FLAT_TREE_SCRIPT);\n return raw as FlatDomTree;\n }\n\n async markdown(): Promise<string> {\n return this.page.evaluate(MARKDOWN_SCRIPT) as Promise<string>;\n }\n\n async browserState(): Promise<BrowserState> {\n const state = await this.page.evaluate(`\n (() => {\n const scrollY = window.scrollY;\n const scrollX = window.scrollX;\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const pageW = document.documentElement.scrollWidth;\n const pageH = document.documentElement.scrollHeight;\n const maxScrollY = pageH - vpH;\n return {\n url: location.href,\n title: document.title,\n viewportWidth: vpW,\n viewportHeight: vpH,\n pageWidth: pageW,\n pageHeight: pageH,\n scrollX: scrollX,\n scrollY: scrollY,\n scrollPercent: maxScrollY > 0 ? Math.round((scrollY / maxScrollY) * 100) : 0,\n pixelsAbove: Math.round(scrollY),\n pixelsBelow: Math.round(Math.max(0, maxScrollY - scrollY)),\n };\n })()\n `) as BrowserState;\n return state;\n }\n\n async formState(): Promise<FormState> {\n return this.page.evaluate(FORM_STATE_SCRIPT) as Promise<FormState>;\n }\n\n async click(ref: string | number): Promise<void> {\n if (typeof ref === 'number') {\n await this.page.evaluate((idx) => {\n const el = document.querySelector('[data-ref=\"' + idx + '\"]') as HTMLElement;\n if (!el) throw new Error('Element with index ' + idx + ' not found');\n\n // Blur previously focused element\n const prev = document.activeElement as HTMLElement | null;\n if (prev && prev !== el && prev !== document.body) {\n prev.blur();\n prev.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, cancelable: true }));\n prev.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false, cancelable: true }));\n }\n\n // Scroll into view\n if (typeof (el as any).scrollIntoViewIfNeeded === 'function') {\n (el as any).scrollIntoViewIfNeeded();\n } else {\n el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });\n }\n\n // Full mouse event sequence — required for React, analytics, custom handlers\n el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true }));\n el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));\n el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));\n el.focus();\n el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));\n el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));\n }, ref);\n // Wait for click processing (animations, state updates)\n await new Promise((r) => setTimeout(r, 200));\n } else {\n await this.page.click(ref);\n }\n }\n\n async typeText(ref: string | number, text: string): Promise<void> {\n if (typeof ref === 'number') {\n // First click the element (triggers full event sequence + focus)\n await this.click(ref);\n\n await this.page.evaluate((idx, txt) => {\n const el = document.querySelector('[data-ref=\"' + idx + '\"]') as HTMLElement;\n if (!el) throw new Error('Element with index ' + idx + ' not found');\n\n const isInput = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';\n const isContentEditable = el.isContentEditable;\n\n if (isContentEditable) {\n // ── Contenteditable: Plan A — synthetic InputEvents ──\n // Works for: React contenteditable, Quill\n // Clear existing content\n if (el.dispatchEvent(new InputEvent('beforeinput', {\n bubbles: true, cancelable: true, inputType: 'deleteContent',\n }))) {\n el.innerText = '';\n el.dispatchEvent(new InputEvent('input', {\n bubbles: true, inputType: 'deleteContent',\n }));\n }\n\n // Insert new text\n if (el.dispatchEvent(new InputEvent('beforeinput', {\n bubbles: true, cancelable: true, inputType: 'insertText', data: txt,\n }))) {\n el.innerText = txt;\n el.dispatchEvent(new InputEvent('input', {\n bubbles: true, inputType: 'insertText', data: txt,\n }));\n }\n\n // Verify Plan A worked\n const planAOk = el.innerText.trim() === txt.trim();\n\n if (!planAOk) {\n // ── Plan B — execCommand fallback ──\n // Works for: Slate.js, some rich-text editors\n el.focus();\n const doc = el.ownerDocument;\n const sel = (doc.defaultView || window).getSelection();\n const range = doc.createRange();\n range.selectNodeContents(el);\n sel?.removeAllRanges();\n sel?.addRange(range);\n doc.execCommand('delete', false);\n doc.execCommand('insertText', false, txt);\n }\n\n el.dispatchEvent(new Event('change', { bubbles: true }));\n el.blur();\n\n } else if (isInput) {\n // ── Input/Textarea: use native value setter to bypass React/Vue ──\n const inputEl = el as HTMLInputElement | HTMLTextAreaElement;\n const proto = Object.getPrototypeOf(inputEl);\n const descriptor =\n Object.getOwnPropertyDescriptor(proto, 'value') ||\n Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value') ||\n Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');\n\n if (descriptor?.set) {\n descriptor.set.call(inputEl, txt);\n } else {\n inputEl.value = txt;\n }\n\n inputEl.dispatchEvent(new Event('input', { bubbles: true }));\n inputEl.dispatchEvent(new Event('change', { bubbles: true }));\n } else {\n // Fallback: try setting value anyway\n (el as any).value = txt;\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n }\n }, ref, text);\n } else {\n // CSS selector path — click to focus, then use keyboard\n await this.page.click(ref, { count: 3 });\n await this.page.keyboard.type(text);\n }\n }\n\n async pressKey(key: string): Promise<void> {\n await this.page.keyboard.press(key as any);\n }\n\n async selectOption(ref: string | number, value: string): Promise<void> {\n const selector = typeof ref === 'number' ? '[data-ref=\"' + ref + '\"]' : ref;\n await this.page.select(selector, value);\n }\n\n async scroll(direction: 'up' | 'down' | 'left' | 'right', amount?: number): Promise<void> {\n const distance = amount || 500;\n const isVertical = direction === 'up' || direction === 'down';\n const positive = direction === 'down' || direction === 'right';\n const delta = positive ? distance : -distance;\n\n await this.page.evaluate((dy, dx, isVert) => {\n // Helper: check if element is a valid scroll container\n const canScroll = (el) => {\n if (!el) return false;\n const s = getComputedStyle(el);\n if (isVert) {\n return /(auto|scroll|overlay)/.test(s.overflowY) &&\n el.scrollHeight > el.clientHeight &&\n el.clientHeight >= window.innerHeight * 0.3;\n } else {\n return /(auto|scroll|overlay)/.test(s.overflowX) &&\n el.scrollWidth > el.clientWidth &&\n el.clientWidth >= window.innerWidth * 0.3;\n }\n };\n\n // Walk from active element up to find a scrollable container\n let el = document.activeElement;\n while (el && !canScroll(el) && el !== document.body) {\n el = el.parentElement;\n }\n\n // If no scrollable ancestor, search the DOM\n if (!canScroll(el)) {\n el = Array.from(document.querySelectorAll('*')).find(canScroll) || null;\n }\n\n const isPageLevel = !el || el === document.body ||\n el === document.documentElement || el === document.scrollingElement;\n\n if (isPageLevel) {\n // Page-level scroll\n if (isVert) {\n window.scrollBy(0, dy);\n } else {\n window.scrollBy(dx, 0);\n }\n } else {\n // Container scroll\n if (isVert) {\n el.scrollBy({ top: dy, behavior: 'smooth' });\n } else {\n el.scrollBy({ left: dx, behavior: 'smooth' });\n }\n }\n }, isVertical ? delta : 0, isVertical ? 0 : delta, isVertical);\n\n // Wait for smooth scroll to settle\n await new Promise((r) => setTimeout(r, 150));\n }\n\n async scrollToElement(ref: string | number): Promise<void> {\n const selector = typeof ref === 'number' ? '[data-ref=\"' + ref + '\"]' : ref;\n await this.page.evaluate((sel) => {\n const el = document.querySelector(sel);\n if (!el) return;\n if (typeof (el as any).scrollIntoViewIfNeeded === 'function') {\n (el as any).scrollIntoViewIfNeeded();\n } else {\n el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });\n }\n }, selector);\n }\n\n async getCookies(opts?: { domain?: string }): Promise<Cookie[]> {\n const cookies = await this.page.cookies();\n const filtered = opts?.domain\n ? cookies.filter((c) => c.domain.includes(opts.domain!))\n : cookies;\n return filtered.map((c) => ({\n name: c.name,\n value: c.value,\n domain: c.domain,\n path: c.path,\n expires: c.expires,\n httpOnly: c.httpOnly,\n secure: c.secure,\n sameSite: c.sameSite as Cookie['sameSite'],\n }));\n }\n\n async wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void> {\n if (typeof options === 'number') {\n await new Promise((r) => setTimeout(r, options * 1000));\n return;\n }\n if (options.time) {\n await new Promise((r) => setTimeout(r, options.time! * 1000));\n }\n if (options.text) {\n await this.page.waitForFunction(\n (t) => document.body.innerText.includes(t),\n { timeout: options.timeout || 30000 },\n options.text\n );\n }\n }\n\n async networkRequests(includeStatic?: boolean): Promise<NetworkEntry[]> {\n // Use Performance API to extract network requests from the browser\n const entries = await this.page.evaluate(`\n (() => {\n const entries = performance.getEntriesByType('resource');\n const staticTypes = new Set(['img', 'font', 'css', 'script', 'link']);\n const includeStatic = ${!!includeStatic};\n\n return entries\n .filter(e => includeStatic || !staticTypes.has(e.initiatorType))\n .map(e => ({\n url: e.name,\n method: 'GET',\n status: 200,\n type: e.initiatorType || 'other',\n size: e.transferSize || e.encodedBodySize || 0,\n duration: Math.round(e.duration),\n }));\n })()\n `) as NetworkEntry[];\n return entries || [];\n }\n\n async installInterceptor(pattern: string): Promise<void> {\n await this.page.evaluate(buildInterceptorScript(pattern));\n }\n\n async getInterceptedRequests(): Promise<unknown[]> {\n return this.page.evaluate(GET_INTERCEPTED_SCRIPT) as Promise<unknown[]>;\n }\n\n async screenshot(opts?: { format?: 'png' | 'jpeg'; fullPage?: boolean }): Promise<Buffer> {\n const result = await this.page.screenshot({\n type: opts?.format || 'png',\n fullPage: opts?.fullPage ?? false,\n });\n return Buffer.from(result);\n }\n\n async tabs(): Promise<TabInfo[]> {\n const browser = this.page.browser();\n const pages = await browser.pages();\n return pages.map((p, i) => ({\n id: i,\n url: p.url(),\n title: '',\n active: p === this.page,\n }));\n }\n\n async close(): Promise<void> {\n await this.page.close();\n }\n}\n"],"mappings":";AAMO,IAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC6BzB,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACzBxB,IAAM,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACE7B,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACJxB,IAAM,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":["el"]}
1
+ {"version":3,"sources":["../../src/browser/dom/flat-tree.ts","../../src/browser/dom/snapshot.ts","../../src/browser/dom/compact-snapshot.ts","../../src/browser/dom/semantic-tree.ts","../../src/browser/dom/markdown.ts","../../src/browser/dom/form-state.ts","../../src/browser/dom/interactive.ts","../../src/browser/interceptor.ts","../../src/browser/semantic-find.ts","../../src/browser/page-adapter.ts"],"sourcesContent":["/**\n * Script that runs inside the browser to extract a flat DOM tree\n * with indexed interactive elements — the format the AI agent uses.\n *\n * Based on Page Agent's DOM extraction approach.\n */\nexport const FLAT_TREE_SCRIPT = `\n(() => {\n const INTERACTIVE_TAGS = new Set([\n 'a', 'button', 'input', 'select', 'textarea', 'details', 'summary',\n 'label', 'option', 'fieldset', 'legend',\n ]);\n\n const INTERACTIVE_ROLES = new Set([\n 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox',\n 'listbox', 'menu', 'menuitem', 'tab', 'switch', 'slider',\n 'searchbox', 'spinbutton', 'option', 'menuitemcheckbox', 'menuitemradio',\n ]);\n\n const ATTR_WHITELIST = [\n 'type', 'role', 'aria-label', 'aria-expanded', 'aria-selected',\n 'aria-checked', 'aria-disabled', 'placeholder', 'title', 'href',\n 'value', 'name', 'alt', 'src',\n ];\n\n let highlightIndex = 0;\n const nodes = {};\n const selectorMap = {};\n\n function isVisible(el) {\n if (el.offsetWidth === 0 && el.offsetHeight === 0) return false;\n const style = getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;\n return true;\n }\n\n function isInteractive(el) {\n const tag = el.tagName.toLowerCase();\n if (INTERACTIVE_TAGS.has(tag)) return true;\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) return true;\n if (el.getAttribute('contenteditable') === 'true') return true;\n if (el.getAttribute('tabindex') !== null && parseInt(el.getAttribute('tabindex')) >= 0) return true;\n if (el.onclick || el.getAttribute('onclick')) return true;\n return false;\n }\n\n function getAttributes(el) {\n const attrs = {};\n for (const attr of ATTR_WHITELIST) {\n const val = el.getAttribute(attr);\n if (val !== null && val !== '') attrs[attr] = val;\n }\n return attrs;\n }\n\n function getScrollable(el) {\n const style = getComputedStyle(el);\n const overflowY = style.overflowY;\n const overflowX = style.overflowX;\n const isScrollableY = (overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight;\n const isScrollableX = (overflowX === 'auto' || overflowX === 'scroll') && el.scrollWidth > el.clientWidth;\n if (!isScrollableY && !isScrollableX) return null;\n return {\n left: el.scrollLeft,\n top: el.scrollTop,\n right: el.scrollWidth - el.clientWidth - el.scrollLeft,\n bottom: el.scrollHeight - el.clientHeight - el.scrollTop,\n };\n }\n\n function walk(el, parentId) {\n if (!el || el.nodeType === 8) return; // skip comments\n\n if (el.nodeType === 3) { // text node\n const text = el.textContent.trim();\n if (!text) return;\n const id = 'text_' + Math.random().toString(36).slice(2, 8);\n nodes[id] = { id, tagName: '#text', text, parentId };\n if (parentId && nodes[parentId]) {\n nodes[parentId].children = nodes[parentId].children || [];\n nodes[parentId].children.push(id);\n }\n return;\n }\n\n if (el.nodeType !== 1) return; // only elements\n\n const tag = el.tagName.toLowerCase();\n if (['script', 'style', 'noscript', 'svg', 'path'].includes(tag)) return;\n if (!isVisible(el)) return;\n\n const id = tag + '_' + Math.random().toString(36).slice(2, 8);\n const interactive = isInteractive(el);\n const node = {\n id,\n tagName: tag,\n attributes: getAttributes(el),\n parentId,\n children: [],\n isInteractive: interactive,\n };\n\n if (interactive) {\n node.highlightIndex = highlightIndex;\n selectorMap[highlightIndex] = id;\n highlightIndex++;\n }\n\n const scrollable = getScrollable(el);\n if (scrollable) node.scrollable = scrollable;\n\n const text = [];\n for (const child of el.childNodes) {\n if (child.nodeType === 3 && child.textContent.trim()) {\n text.push(child.textContent.trim());\n }\n }\n if (text.length > 0) node.text = text.join(' ').slice(0, 200);\n\n nodes[id] = node;\n\n if (parentId && nodes[parentId]) {\n nodes[parentId].children.push(id);\n }\n\n for (const child of el.children) {\n walk(child, id);\n }\n }\n\n const rootId = 'root';\n nodes[rootId] = { id: rootId, tagName: 'body', children: [], attributes: {} };\n for (const child of document.body.children) {\n walk(child, rootId);\n }\n\n return { rootId, map: nodes, selectorMap };\n})()\n`;\n\n/**\n * Convert a FlatDomTree into the indexed text format that the LLM agent reads.\n * Example output:\n * [0]<button type=submit>Search</>\n * [1]<input type=text placeholder=\"Enter query\" />\n */\nexport function flatTreeToString(tree: { rootId: string; map: Record<string, any> }): string {\n const lines: string[] = [];\n\n function walk(nodeId: string, depth: number) {\n const node = tree.map[nodeId];\n if (!node) return;\n\n const indent = '\\t'.repeat(depth);\n\n if (node.tagName === '#text') {\n if (node.text) lines.push(`${indent}${node.text}`);\n return;\n }\n\n const attrs = node.attributes || {};\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => (v === '' ? k : `${k}=\"${v}\"`))\n .join(' ');\n\n const prefix = node.highlightIndex !== undefined ? `[${node.highlightIndex}]` : '';\n const scrollInfo = node.scrollable\n ? ` |scroll: ${Math.round(node.scrollable.top)}px up, ${Math.round(node.scrollable.bottom)}px down|`\n : '';\n\n const text = node.text || '';\n const tag = node.tagName;\n\n if (prefix || text || node.children?.length > 0) {\n const opening = `${indent}${prefix}<${tag}${attrStr ? ' ' + attrStr : ''}${scrollInfo}>`;\n\n if (!node.children?.length || (node.children.length === 0 && text)) {\n lines.push(`${opening}${text}</>`);\n } else {\n lines.push(`${opening}${text}`);\n for (const childId of node.children || []) {\n walk(childId, depth + 1);\n }\n }\n } else {\n for (const childId of node.children || []) {\n walk(childId, depth);\n }\n }\n }\n\n walk(tree.rootId, 0);\n return lines.join('\\n');\n}\n","/**\n * Advanced DOM snapshot script — runs inside the browser.\n * Multi-stage pruning pipeline producing LLM-optimized output.\n *\n * Stages:\n * 1. Walk DOM, collect visibility + layout + interactivity signals\n * 2. Prune invisible, zero-area, non-content elements\n * 3. SVG & decoration collapse\n * 4. Shadow DOM traversal\n * 5. Same-origin iframe extraction\n * 6. Bounding-box parent-child dedup (link/button wrapping)\n * 7. Paint-order occlusion detection (overlay/modal coverage)\n * 8. Attribute whitelist filtering\n * 9. Ad/tracker filtering\n * 10. Scroll position info\n * 11. data-ref annotation for targeting\n * 12. Token-efficient serialization with interactive indices\n */\n/**\n * Build snapshot script with optional previous hashes for diff marking.\n * Elements new since last snapshot get a `*` prefix on their index.\n */\nexport function buildSnapshotScript(previousHashes?: string[]): string {\n return SNAPSHOT_SCRIPT_FN(previousHashes || []);\n}\n\nfunction SNAPSHOT_SCRIPT_FN(prevHashes: string[]): string {\n return `\n(() => {\n let idx = 0;\n const __prevHashes = new Set(${JSON.stringify(prevHashes)});\n const __currentHashes = [];\n`;\n}\n\nexport const SNAPSHOT_SCRIPT = `\n(() => {\n let idx = 0;\n const __prevHashes = (window.__lobster_prev_hashes) ? new Set(window.__lobster_prev_hashes) : null;\n const __currentHashes = [];\n\n const SKIP_TAGS = new Set([\n 'script','style','noscript','svg','path','meta','link','head',\n 'template','slot','colgroup','col',\n ]);\n\n const INTERACTIVE_TAGS = new Set([\n 'a','button','input','select','textarea','details','summary','label',\n ]);\n\n const INTERACTIVE_ROLES = new Set([\n 'button','link','textbox','checkbox','radio','combobox','listbox',\n 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',\n 'option','menuitemcheckbox','menuitemradio','treeitem',\n ]);\n\n const ATTR_WHITELIST = [\n 'type','role','aria-label','aria-expanded','aria-selected','aria-checked',\n 'aria-disabled','aria-haspopup','aria-pressed','placeholder','title',\n 'href','value','name','alt','src','action','method','for',\n 'data-testid','data-id','contenteditable','tabindex',\n ];\n\n const AD_PATTERNS = /ad[-_]?banner|ad[-_]?container|google[-_]?ad|doubleclick|adsbygoogle|sponsored|^ad$/i;\n\n // ── Stage 1: Visibility check ──\n function isVisible(el) {\n if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;\n const s = getComputedStyle(el);\n if (s.display === 'none') return false;\n if (s.visibility === 'hidden' || s.visibility === 'collapse') return false;\n if (s.opacity === '0') return false;\n if (s.clipPath === 'inset(100%)') return false;\n // Check for offscreen positioning\n const rect = el.getBoundingClientRect();\n if (rect.right < 0 || rect.bottom < 0) return false;\n return true;\n }\n\n // ── Stage 2: Interactive detection ──\n function isInteractive(el) {\n const tag = el.tagName.toLowerCase();\n if (INTERACTIVE_TAGS.has(tag)) {\n // Skip disabled elements\n if (el.disabled) return false;\n // Skip hidden inputs\n if (tag === 'input' && el.type === 'hidden') return false;\n return true;\n }\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) return true;\n if (el.contentEditable === 'true') return true;\n if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;\n if (el.onclick) return true;\n return false;\n }\n\n // ── Stage 8: Attribute filtering ──\n function getAttrs(el) {\n const parts = [];\n for (const name of ATTR_WHITELIST) {\n let v = el.getAttribute(name);\n if (v === null || v === '') continue;\n // Truncate long values\n if (v.length > 80) v = v.slice(0, 77) + '...';\n // Skip href=\"javascript:...\"\n if (name === 'href' && v.startsWith('javascript:')) continue;\n parts.push(name + '=' + v);\n }\n return parts.length ? ' ' + parts.join(' ') : '';\n }\n\n // ── Stage 9: Ad filtering ──\n function isAd(el) {\n const id = el.id || '';\n const cls = el.className || '';\n if (typeof cls === 'string' && AD_PATTERNS.test(cls)) return true;\n if (AD_PATTERNS.test(id)) return true;\n if (el.tagName === 'IFRAME' && AD_PATTERNS.test(el.src || '')) return true;\n return false;\n }\n\n // ── Stage 10: Scroll info ──\n function getScrollInfo(el) {\n const s = getComputedStyle(el);\n const overflowY = s.overflowY;\n const overflowX = s.overflowX;\n const scrollableY = (overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight;\n const scrollableX = (overflowX === 'auto' || overflowX === 'scroll') && el.scrollWidth > el.clientWidth;\n if (!scrollableY && !scrollableX) return '';\n\n const parts = [];\n if (scrollableY) {\n const up = Math.round(el.scrollTop);\n const down = Math.round(el.scrollHeight - el.clientHeight - el.scrollTop);\n if (up > 0) parts.push(up + 'px up');\n if (down > 0) parts.push(down + 'px down');\n }\n if (scrollableX) {\n const left = Math.round(el.scrollLeft);\n const right = Math.round(el.scrollWidth - el.clientWidth - el.scrollLeft);\n if (left > 0) parts.push(left + 'px left');\n if (right > 0) parts.push(right + 'px right');\n }\n return parts.length ? ' |scroll: ' + parts.join(', ') + '|' : '';\n }\n\n // ── Stage 6: Bounding-box dedup ──\n // If a parent and child are both interactive and have ~same bounding box,\n // skip the parent (e.g., <a><button>Click</button></a>)\n function isWrappingInteractive(el) {\n if (!isInteractive(el)) return false;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return false;\n for (const child of el.children) {\n if (!isInteractive(child)) continue;\n const cr = child.getBoundingClientRect();\n const overlapX = Math.min(rect.right, cr.right) - Math.max(rect.left, cr.left);\n const overlapY = Math.min(rect.bottom, cr.bottom) - Math.max(rect.top, cr.top);\n const overlapArea = Math.max(0, overlapX) * Math.max(0, overlapY);\n const parentArea = rect.width * rect.height;\n if (parentArea > 0 && overlapArea / parentArea > 0.85) return true;\n }\n return false;\n }\n\n // ── Stage 7: Occlusion detection ──\n function isOccluded(el) {\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return false;\n const cx = rect.left + rect.width / 2;\n const cy = rect.top + rect.height / 2;\n const topEl = document.elementFromPoint(cx, cy);\n if (!topEl) return false;\n if (topEl === el || el.contains(topEl) || topEl.contains(el)) return false;\n // Check z-index — if top element is a modal/overlay, mark as occluded\n const topZ = parseInt(getComputedStyle(topEl).zIndex) || 0;\n const elZ = parseInt(getComputedStyle(el).zIndex) || 0;\n return topZ > elZ + 10;\n }\n\n // ── Stage 5: Iframe content extraction ──\n function getIframeContent(iframe, depth, maxDepth) {\n try {\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) return '';\n return '\\\\n' + walkNode(doc.body, depth, maxDepth);\n } catch { return ''; }\n }\n\n // ── Stage 4: Shadow DOM traversal ──\n function getShadowContent(el, depth, maxDepth) {\n if (!el.shadowRoot) return '';\n let out = '';\n for (const child of el.shadowRoot.childNodes) {\n out += walkNode(child, depth, maxDepth);\n }\n return out;\n }\n\n // ── Input value hint ──\n function getInputHint(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === 'input') {\n const type = el.type || 'text';\n const val = el.value || '';\n const checked = el.checked;\n if (type === 'checkbox' || type === 'radio') {\n return checked ? ' [checked]' : ' [unchecked]';\n }\n if (val) return ' value=\"' + val.slice(0, 50) + '\"';\n }\n if (tag === 'textarea' && el.value) {\n return ' value=\"' + el.value.slice(0, 50) + '\"';\n }\n if (tag === 'select' && el.selectedOptions?.length) {\n return ' selected=\"' + el.selectedOptions[0].text.slice(0, 40) + '\"';\n }\n return '';\n }\n\n const MAX_DEPTH = 25;\n const MAX_TEXT = 150;\n\n function walkNode(node, depth, maxDepth) {\n if (depth > maxDepth) return '';\n if (!node) return '';\n\n // Text node\n if (node.nodeType === 3) {\n const t = node.textContent.trim();\n if (!t) return '';\n const text = t.length > MAX_TEXT ? t.slice(0, MAX_TEXT) + '...' : t;\n return ' '.repeat(depth) + text + '\\\\n';\n }\n\n // Comment node — skip\n if (node.nodeType === 8) return '';\n\n // Only element nodes from here\n if (node.nodeType !== 1) return '';\n\n const el = node;\n const tag = el.tagName.toLowerCase();\n\n // ── Stage 3: Skip tags ──\n if (SKIP_TAGS.has(tag)) return '';\n\n // ── Stage 2: Visibility ──\n if (!isVisible(el)) return '';\n\n // ── Stage 9: Ad filtering ──\n if (isAd(el)) return '';\n\n // ── Stage 6: Bbox dedup — skip wrapping interactive parent ──\n const skipSelf = isWrappingInteractive(el);\n\n const indent = ' '.repeat(depth);\n const inter = !skipSelf && isInteractive(el);\n let prefix = '';\n if (inter) {\n const thisIdx = idx++;\n // Hash: tag + text + key attributes for diff tracking\n const hashText = tag + ':' + (el.textContent || '').trim().slice(0, 40) + ':' + (el.getAttribute('href') || '') + ':' + (el.getAttribute('aria-label') || '');\n __currentHashes.push(hashText);\n const isNew = __prevHashes && __prevHashes.size > 0 && !__prevHashes.has(hashText);\n prefix = isNew ? '*[' + thisIdx + ']' : '[' + thisIdx + ']';\n }\n\n // ── Stage 11: Annotate with data-ref ──\n if (inter) {\n try { el.dataset.ref = String(idx - 1); } catch {}\n }\n\n // ── Stage 7: Occlusion check for interactive elements ──\n if (inter && isOccluded(el)) {\n // Still include but mark as occluded\n // (agent needs to know element exists but may need to scroll/close modal)\n }\n\n const a = getAttrs(el);\n const scrollInfo = getScrollInfo(el);\n const inputHint = inter ? getInputHint(el) : '';\n\n // Leaf text extraction\n let leafText = '';\n if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {\n const t = el.childNodes[0].textContent.trim();\n if (t) leafText = t.length > MAX_TEXT ? t.slice(0, MAX_TEXT) + '...' : t;\n }\n\n // ── Stage 5: Iframe ──\n if (tag === 'iframe') {\n const iframeContent = getIframeContent(el, depth + 1, maxDepth);\n if (iframeContent) {\n return indent + prefix + '<iframe' + a + '>\\\\n' + iframeContent;\n }\n return '';\n }\n\n // Build output\n let out = '';\n\n if (skipSelf) {\n // Skip self but render children\n for (const c of el.childNodes) out += walkNode(c, depth, maxDepth);\n out += getShadowContent(el, depth, maxDepth);\n return out;\n }\n\n if (inter || leafText || el.children.length === 0) {\n if (leafText) {\n out = indent + prefix + '<' + tag + a + scrollInfo + inputHint + '>' + leafText + '</' + tag + '>\\\\n';\n } else {\n out = indent + prefix + '<' + tag + a + scrollInfo + inputHint + '>\\\\n';\n for (const c of el.childNodes) out += walkNode(c, depth + 1, maxDepth);\n out += getShadowContent(el, depth + 1, maxDepth);\n }\n } else {\n // Non-interactive container — flatten depth if no useful info\n if (scrollInfo) {\n out = indent + '<' + tag + scrollInfo + '>\\\\n';\n for (const c of el.childNodes) out += walkNode(c, depth + 1, maxDepth);\n out += getShadowContent(el, depth + 1, maxDepth);\n } else {\n for (const c of el.childNodes) out += walkNode(c, depth, maxDepth);\n out += getShadowContent(el, depth, maxDepth);\n }\n }\n\n return out;\n }\n\n // ── Page-level scroll info header ──\n const scrollY = window.scrollY;\n const scrollMax = document.documentElement.scrollHeight - window.innerHeight;\n const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const pageH = document.documentElement.scrollHeight;\n\n let header = '';\n header += 'viewport: ' + vpW + 'x' + vpH + ' | page_height: ' + pageH + 'px';\n header += ' | scroll: ' + scrollPct + '%';\n if (scrollY > 50) header += ' (' + Math.round(scrollY) + 'px from top)';\n if (scrollMax - scrollY > 50) header += ' (' + Math.round(scrollMax - scrollY) + 'px more below)';\n header += '\\\\n---\\\\n';\n\n // Store current hashes for next diff comparison\n window.__lobster_prev_hashes = __currentHashes;\n\n return header + walkNode(document.body, 0, MAX_DEPTH);\n})()\n`;\n","/**\n * Compact Snapshot — token-efficient DOM snapshot (~800 tokens).\n *\n * Only emits interactive elements + landmark section headers.\n * Format: [0] button \"Sign In\" (one line per element)\n *\n * Inspired by PinchTab's token-counting approach, built from scratch.\n */\n\nexport const COMPACT_SNAPSHOT_SCRIPT = `\n(() => {\n const TOKEN_BUDGET = 800;\n const CHARS_PER_TOKEN = 4;\n\n const INTERACTIVE_TAGS = new Set([\n 'a','button','input','select','textarea','details','summary','label',\n ]);\n const INTERACTIVE_ROLES = new Set([\n 'button','link','textbox','checkbox','radio','combobox','listbox',\n 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',\n 'option','menuitemcheckbox','menuitemradio','treeitem',\n ]);\n const LANDMARK_TAGS = new Map([\n ['nav', 'Navigation'],\n ['main', 'Main Content'],\n ['header', 'Header'],\n ['footer', 'Footer'],\n ['aside', 'Sidebar'],\n ['form', 'Form'],\n ]);\n const LANDMARK_ROLES = new Map([\n ['navigation', 'Navigation'],\n ['main', 'Main Content'],\n ['banner', 'Header'],\n ['contentinfo', 'Footer'],\n ['complementary', 'Sidebar'],\n ['search', 'Search'],\n ['dialog', 'Dialog'],\n ]);\n\n function isVisible(el) {\n if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;\n const s = getComputedStyle(el);\n return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';\n }\n\n function isInteractive(el) {\n const tag = el.tagName.toLowerCase();\n if (INTERACTIVE_TAGS.has(tag)) {\n if (el.disabled) return false;\n if (tag === 'input' && el.type === 'hidden') return false;\n return true;\n }\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) return true;\n if (el.contentEditable === 'true') return true;\n if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;\n return false;\n }\n\n function getRole(el) {\n const role = el.getAttribute('role');\n if (role) return role;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a') return 'link';\n if (tag === 'button' || tag === 'summary') return 'button';\n if (tag === 'input') return el.type || 'text';\n if (tag === 'select') return 'select';\n if (tag === 'textarea') return 'textarea';\n if (tag === 'label') return 'label';\n return tag;\n }\n\n function getName(el) {\n return (\n el.getAttribute('aria-label') ||\n el.getAttribute('alt') ||\n el.getAttribute('title') ||\n el.getAttribute('placeholder') ||\n (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button') ? el.value : '') ||\n (el.id ? document.querySelector('label[for=\"' + el.id + '\"]')?.textContent?.trim() : '') ||\n (el.children.length <= 2 ? el.textContent?.trim() : '') ||\n ''\n ).slice(0, 60);\n }\n\n function getValue(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === 'input') {\n const type = el.type || 'text';\n if (type === 'checkbox' || type === 'radio') return el.checked ? 'checked' : 'unchecked';\n if (type === 'password') return el.value ? '****' : '';\n return el.value ? el.value.slice(0, 30) : '';\n }\n if (tag === 'textarea') return el.value ? el.value.slice(0, 30) : '';\n if (tag === 'select' && el.selectedOptions?.length) return el.selectedOptions[0].text.slice(0, 30);\n return '';\n }\n\n // Collect elements\n let idx = 0;\n let charsUsed = 0;\n const lines = [];\n let lastLandmark = '';\n\n // Page header\n const scrollY = window.scrollY;\n const scrollMax = document.documentElement.scrollHeight - window.innerHeight;\n const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;\n const header = 'url: ' + location.href + ' | scroll: ' + scrollPct + '%';\n lines.push(header);\n charsUsed += header.length;\n\n // Walk DOM\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);\n let node;\n while ((node = walker.nextNode())) {\n if (!isVisible(node)) continue;\n\n const tag = node.tagName.toLowerCase();\n if (['script','style','noscript','svg','path','meta','link','head','template'].includes(tag)) continue;\n\n // Check for landmark\n const role = node.getAttribute('role');\n const landmark = LANDMARK_TAGS.get(tag) || (role ? LANDMARK_ROLES.get(role) : null);\n if (landmark && landmark !== lastLandmark) {\n const sectionLine = '--- ' + landmark + ' ---';\n if (charsUsed + sectionLine.length > TOKEN_BUDGET * CHARS_PER_TOKEN) break;\n lines.push(sectionLine);\n charsUsed += sectionLine.length;\n lastLandmark = landmark;\n }\n\n // Only emit interactive elements\n if (!isInteractive(node)) continue;\n\n const elRole = getRole(node);\n const name = getName(node);\n const value = getValue(node);\n\n // Build compact line\n let line = '[' + idx + '] ' + elRole;\n if (name) line += ' \"' + name.replace(/\"/g, \"'\") + '\"';\n if (value) line += ' val=\"' + value.replace(/\"/g, \"'\") + '\"';\n\n // Check token budget\n if (charsUsed + line.length > TOKEN_BUDGET * CHARS_PER_TOKEN) {\n lines.push('... (' + (document.querySelectorAll('a,button,input,select,textarea,[role]').length - idx) + ' more elements)');\n break;\n }\n\n // Annotate element with ref for clicking\n try { node.dataset.ref = String(idx); } catch {}\n\n lines.push(line);\n charsUsed += line.length;\n idx++;\n }\n\n return lines.join('\\\\n');\n})()\n`;\n\n/**\n * Build compact snapshot script with custom token budget.\n */\nexport function buildCompactSnapshotScript(tokenBudget: number = 800): string {\n return COMPACT_SNAPSHOT_SCRIPT.replace('const TOKEN_BUDGET = 800;', `const TOKEN_BUDGET = ${tokenBudget};`);\n}\n","/**\n * Semantic tree — W3C accessible name algorithm, XPath, listener detection.\n *\n * Based on Lightpanda's SemanticTree.zig approach:\n * - Accessible name: aria-labelledby → aria-label → alt → title → placeholder → text content\n * - XPath generation for element location\n * - Interactive classification: native, aria, contenteditable, listener, focusable\n * - Disabled state with fieldset inheritance\n * - Input value, option, checked state extraction\n */\nexport const SEMANTIC_TREE_SCRIPT = `\n(() => {\n const SKIP = new Set(['script','style','noscript','svg','head','meta','link','template']);\n\n const ROLE_MAP = {\n a: 'link', button: 'button', input: 'textbox', select: 'combobox',\n textarea: 'textbox', h1: 'heading', h2: 'heading', h3: 'heading',\n h4: 'heading', h5: 'heading', h6: 'heading', nav: 'navigation',\n main: 'main', header: 'banner', footer: 'contentinfo', aside: 'complementary',\n form: 'form', table: 'table', img: 'img', ul: 'list', ol: 'list', li: 'listitem',\n section: 'region', article: 'article', dialog: 'dialog', details: 'group',\n summary: 'button', progress: 'progressbar', meter: 'meter', output: 'status',\n label: 'label', legend: 'legend', fieldset: 'group', option: 'option',\n tr: 'row', td: 'cell', th: 'columnheader', caption: 'caption',\n };\n\n const INTERACTIVE_ROLES = new Set([\n 'button','link','textbox','checkbox','radio','combobox','listbox',\n 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',\n 'option','menuitemcheckbox','menuitemradio','treeitem',\n ]);\n\n // ── W3C Accessible Name Algorithm (simplified) ──\n function getAccessibleName(el) {\n // 1. aria-labelledby (highest priority)\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const ids = labelledBy.split(/\\\\s+/);\n const parts = ids.map(id => {\n const ref = document.getElementById(id);\n return ref ? ref.textContent.trim() : '';\n }).filter(Boolean);\n if (parts.length > 0) return parts.join(' ').slice(0, 120);\n }\n\n // 2. aria-label\n const ariaLabel = el.getAttribute('aria-label');\n if (ariaLabel) return ariaLabel.slice(0, 120);\n\n // 3. alt (for images)\n const alt = el.getAttribute('alt');\n if (alt) return alt.slice(0, 120);\n\n // 4. title\n const title = el.getAttribute('title');\n if (title) return title.slice(0, 120);\n\n // 5. placeholder (for inputs)\n const placeholder = el.getAttribute('placeholder');\n if (placeholder) return placeholder.slice(0, 120);\n\n // 6. value (for buttons)\n if (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button')) {\n const val = el.getAttribute('value');\n if (val) return val.slice(0, 120);\n }\n\n // 7. Associated label\n if (el.id) {\n const label = document.querySelector('label[for=\"' + el.id + '\"]');\n if (label) return label.textContent.trim().slice(0, 120);\n }\n\n // 8. Direct text content (only for leaf-ish elements)\n if (el.children.length <= 2) {\n const text = el.textContent.trim();\n if (text && text.length < 120) return text;\n }\n\n return '';\n }\n\n // ── XPath generation ──\n function getXPath(el) {\n const parts = [];\n let current = el;\n while (current && current.nodeType === 1) {\n let index = 1;\n let sibling = current.previousElementSibling;\n while (sibling) {\n if (sibling.tagName === current.tagName) index++;\n sibling = sibling.previousElementSibling;\n }\n const tag = current.tagName.toLowerCase();\n parts.unshift(tag + '[' + index + ']');\n current = current.parentElement;\n }\n return '/' + parts.join('/');\n }\n\n // ── Interactivity classification ──\n function classifyInteractivity(el) {\n const types = [];\n const tag = el.tagName.toLowerCase();\n\n // Native\n if (['a','button','input','select','textarea','details','summary'].includes(tag)) {\n if (tag === 'a' && !el.href) {} // anchor without href is not interactive\n else if (tag === 'input' && el.type === 'hidden') {} // hidden inputs\n else types.push('native');\n }\n\n // ARIA role\n const role = el.getAttribute('role');\n if (role && INTERACTIVE_ROLES.has(role)) types.push('aria');\n\n // Contenteditable\n if (el.contentEditable === 'true') types.push('contenteditable');\n\n // Focusable\n if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) types.push('focusable');\n\n // Event listeners (check onclick and common inline handlers)\n if (el.onclick || el.onmousedown || el.onkeydown || el.onkeypress ||\n el.getAttribute('onclick') || el.getAttribute('onmousedown')) {\n types.push('listener');\n }\n\n return types;\n }\n\n // ── Disabled state with fieldset inheritance ──\n function isDisabled(el) {\n if (el.disabled) return true;\n // Check fieldset disabled inheritance\n let parent = el.parentElement;\n while (parent) {\n if (parent.tagName === 'FIELDSET' && parent.disabled) {\n // Exception: elements inside the first legend child are NOT disabled\n const firstLegend = parent.querySelector(':scope > legend');\n if (firstLegend && firstLegend.contains(el)) return false;\n return true;\n }\n parent = parent.parentElement;\n }\n return false;\n }\n\n // ── Walk the DOM ──\n function walk(el, depth, maxDepth) {\n if (!el || depth > maxDepth) return '';\n\n if (el.nodeType === 3) {\n const t = el.textContent.trim();\n return t ? ' '.repeat(depth) + 'text \"' + t.slice(0, 100) + '\"\\\\n' : '';\n }\n\n if (el.nodeType !== 1) return '';\n const tag = el.tagName.toLowerCase();\n if (SKIP.has(tag)) return '';\n\n const style = getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden') return '';\n\n const indent = ' '.repeat(depth);\n const role = el.getAttribute('role') || ROLE_MAP[tag] || '';\n const name = getAccessibleName(el);\n const interTypes = classifyInteractivity(el);\n const interactive = interTypes.length > 0;\n const disabled = interactive && isDisabled(el);\n\n let line = indent;\n line += role || tag;\n\n if (name) line += ' \"' + name.slice(0, 80) + '\"';\n\n if (interactive) {\n line += ' [' + interTypes.join(',') + ']';\n if (disabled) line += ' {disabled}';\n line += ' xpath=' + getXPath(el);\n }\n\n // Input state\n if (tag === 'input') {\n const type = el.type || 'text';\n line += ' type=' + type;\n if (type === 'checkbox' || type === 'radio') {\n line += el.checked ? ' [checked]' : ' [unchecked]';\n } else if (el.value) {\n line += ' value=\"' + el.value.slice(0, 50) + '\"';\n }\n }\n if (tag === 'textarea' && el.value) {\n line += ' value=\"' + el.value.slice(0, 50) + '\"';\n }\n if (tag === 'select') {\n const opts = Array.from(el.options || []).map(o => ({\n text: o.text.slice(0, 30),\n value: o.value,\n selected: o.selected,\n }));\n const selected = opts.find(o => o.selected);\n if (selected) line += ' selected=\"' + selected.text + '\"';\n if (opts.length <= 10) {\n line += ' options=[' + opts.map(o => o.text).join('|') + ']';\n }\n }\n\n line += '\\\\n';\n\n let out = line;\n for (const c of el.childNodes) {\n out += walk(c, depth + 1, maxDepth);\n }\n\n // Shadow DOM\n if (el.shadowRoot) {\n for (const c of el.shadowRoot.childNodes) {\n out += walk(c, depth + 1, maxDepth);\n }\n }\n\n return out;\n }\n\n return walk(document.body, 0, 20);\n})()\n`;\n","/**\n * DOM-to-Markdown converter — runs inside the browser.\n *\n * Full-featured conversion based on Lightpanda's markdown.zig:\n * - Table support with header separator rows\n * - URL resolution (relative → absolute)\n * - Nested ordered/unordered lists with proper indentation\n * - Character escaping for Markdown special chars\n * - Strikethrough, code blocks, blockquotes\n * - Smart anchor handling (inline vs block)\n * - Whitespace collapsing\n */\nexport const MARKDOWN_SCRIPT = `\n(() => {\n const SKIP = new Set(['script','style','noscript','svg','head','template']);\n const baseUrl = location.href;\n\n // Resolve relative URLs to absolute\n function resolveUrl(href) {\n if (!href || href.startsWith('javascript:') || href.startsWith('#')) return href;\n try { return new URL(href, baseUrl).href; } catch { return href; }\n }\n\n // Escape Markdown special chars in text\n function escapeText(text) {\n return text\n .replace(/\\\\\\\\/g, '\\\\\\\\\\\\\\\\')\n .replace(/([*_~\\`\\\\[\\\\]|])/g, '\\\\\\\\$1');\n }\n\n // State tracking\n let listDepth = 0;\n let orderedCounters = [];\n let inPre = false;\n let inTable = false;\n\n function listIndent() { return ' '.repeat(listDepth); }\n\n function walk(el) {\n if (!el) return '';\n\n // Text node\n if (el.nodeType === 3) {\n const text = el.textContent || '';\n if (inPre) return text;\n // Collapse whitespace\n const collapsed = text.replace(/\\\\s+/g, ' ');\n return collapsed === ' ' && !el.previousSibling && !el.nextSibling ? '' : collapsed;\n }\n\n if (el.nodeType !== 1) return '';\n const tag = el.tagName.toLowerCase();\n if (SKIP.has(tag)) return '';\n\n // Visibility check\n try {\n const s = getComputedStyle(el);\n if (s.display === 'none' || s.visibility === 'hidden') return '';\n } catch {}\n\n // Get children content\n function childContent() {\n let out = '';\n for (const c of el.childNodes) out += walk(c);\n return out;\n }\n\n switch (tag) {\n // ── Headings ──\n case 'h1': return '\\\\n\\\\n# ' + childContent().trim() + '\\\\n\\\\n';\n case 'h2': return '\\\\n\\\\n## ' + childContent().trim() + '\\\\n\\\\n';\n case 'h3': return '\\\\n\\\\n### ' + childContent().trim() + '\\\\n\\\\n';\n case 'h4': return '\\\\n\\\\n#### ' + childContent().trim() + '\\\\n\\\\n';\n case 'h5': return '\\\\n\\\\n##### ' + childContent().trim() + '\\\\n\\\\n';\n case 'h6': return '\\\\n\\\\n###### ' + childContent().trim() + '\\\\n\\\\n';\n\n // ── Block elements ──\n case 'p': return '\\\\n\\\\n' + childContent().trim() + '\\\\n\\\\n';\n case 'br': return '\\\\n';\n case 'hr': return '\\\\n\\\\n---\\\\n\\\\n';\n\n // ── Inline formatting ──\n case 'strong': case 'b': {\n const inner = childContent().trim();\n return inner ? '**' + inner + '**' : '';\n }\n case 'em': case 'i': {\n const inner = childContent().trim();\n return inner ? '*' + inner + '*' : '';\n }\n case 's': case 'del': case 'strike': {\n const inner = childContent().trim();\n return inner ? '~~' + inner + '~~' : '';\n }\n case 'code': {\n if (inPre) return childContent();\n const inner = childContent();\n return inner ? '\\\\x60' + inner + '\\\\x60' : '';\n }\n\n // ── Code blocks ──\n case 'pre': {\n inPre = true;\n const inner = childContent();\n inPre = false;\n const lang = el.querySelector('code')?.className?.match(/language-(\\\\w+)/)?.[1] || '';\n return '\\\\n\\\\n\\\\x60\\\\x60\\\\x60' + lang + '\\\\n' + inner.trim() + '\\\\n\\\\x60\\\\x60\\\\x60\\\\n\\\\n';\n }\n\n // ── Links ──\n case 'a': {\n const href = resolveUrl(el.getAttribute('href') || '');\n const inner = childContent().trim();\n const name = inner || el.getAttribute('aria-label') || el.getAttribute('title') || '';\n if (!name) return '';\n if (!href || href === '#' || href.startsWith('javascript:')) return name;\n return '[' + name + '](' + href + ')';\n }\n\n // ── Images ──\n case 'img': {\n const alt = el.getAttribute('alt') || '';\n const src = resolveUrl(el.getAttribute('src') || '');\n return src ? '![' + alt + '](' + src + ')' : '';\n }\n\n // ── Lists ──\n case 'ul': {\n listDepth++;\n orderedCounters.push(0);\n const inner = childContent();\n listDepth--;\n orderedCounters.pop();\n return '\\\\n' + inner;\n }\n case 'ol': {\n listDepth++;\n orderedCounters.push(0);\n const inner = childContent();\n listDepth--;\n orderedCounters.pop();\n return '\\\\n' + inner;\n }\n case 'li': {\n const parent = el.parentElement?.tagName?.toLowerCase();\n const isOrdered = parent === 'ol';\n const inner = childContent().trim();\n if (!inner) return '';\n if (isOrdered) {\n const counter = orderedCounters.length > 0\n ? ++orderedCounters[orderedCounters.length - 1] : 1;\n return listIndent() + counter + '. ' + inner + '\\\\n';\n }\n return listIndent() + '- ' + inner + '\\\\n';\n }\n\n // ── Blockquote ──\n case 'blockquote': {\n const inner = childContent().trim();\n if (!inner) return '';\n return '\\\\n\\\\n' + inner.split('\\\\n').map(line => '> ' + line).join('\\\\n') + '\\\\n\\\\n';\n }\n\n // ── Tables ──\n case 'table': {\n inTable = true;\n let out = '\\\\n\\\\n';\n const rows = el.querySelectorAll('tr');\n let headerDone = false;\n\n for (let i = 0; i < rows.length; i++) {\n const cells = rows[i].querySelectorAll('th, td');\n const isHeader = rows[i].querySelector('th') !== null;\n const cellTexts = [];\n for (const cell of cells) {\n let cellText = '';\n for (const c of cell.childNodes) cellText += walk(c);\n cellTexts.push(cellText.trim().replace(/\\\\|/g, '\\\\\\\\|').replace(/\\\\n/g, ' '));\n }\n\n out += '| ' + cellTexts.join(' | ') + ' |\\\\n';\n\n if (isHeader && !headerDone) {\n out += '| ' + cellTexts.map(() => '---').join(' | ') + ' |\\\\n';\n headerDone = true;\n }\n\n // First data row without headers — synthesize separator\n if (i === 0 && !isHeader && !headerDone) {\n out += '| ' + cellTexts.map(() => '---').join(' | ') + ' |\\\\n';\n headerDone = true;\n }\n }\n\n inTable = false;\n return out + '\\\\n';\n }\n case 'thead': case 'tbody': case 'tfoot':\n return childContent();\n case 'tr': case 'td': case 'th':\n // Handled by table walker above; fallback for orphaned elements\n return childContent();\n\n // ── Definition lists ──\n case 'dl': return '\\\\n\\\\n' + childContent() + '\\\\n\\\\n';\n case 'dt': return '\\\\n**' + childContent().trim() + '**\\\\n';\n case 'dd': return ': ' + childContent().trim() + '\\\\n';\n\n // ── Figure ──\n case 'figure': return '\\\\n\\\\n' + childContent().trim() + '\\\\n\\\\n';\n case 'figcaption': return '\\\\n*' + childContent().trim() + '*\\\\n';\n\n // ── Details/Summary ──\n case 'details': return '\\\\n\\\\n' + childContent() + '\\\\n\\\\n';\n case 'summary': return '**' + childContent().trim() + '**\\\\n\\\\n';\n\n // ── Generic blocks ──\n case 'div': case 'section': case 'article': case 'main': case 'aside':\n case 'header': case 'footer': case 'nav':\n return '\\\\n' + childContent() + '\\\\n';\n\n case 'span': case 'small': case 'sub': case 'sup': case 'abbr':\n case 'time': case 'mark': case 'cite': case 'q':\n return childContent();\n\n default:\n return childContent();\n }\n }\n\n const raw = walk(document.body);\n // Clean up: collapse 3+ newlines to 2, trim\n return raw.replace(/\\\\n{3,}/g, '\\\\n\\\\n').replace(/^\\\\n+|\\\\n+$/g, '').trim();\n})()\n`;\n","/**\n * Form state extraction — runs inside the browser.\n *\n * Extracts all form fields (including orphan fields not in <form> tags),\n * their types, labels, values, required/disabled state.\n *\n * Based on OpenCLI's getFormStateJs() pattern.\n */\nexport const FORM_STATE_SCRIPT = `\n(() => {\n function extractField(el) {\n const tag = el.tagName.toLowerCase();\n const type = (el.getAttribute('type') || tag).toLowerCase();\n\n // Skip non-user-facing inputs\n if (['hidden', 'submit', 'button', 'reset', 'image'].includes(type)) return null;\n\n const name = el.name || el.id || '';\n\n // Find label via multiple strategies\n const label =\n el.getAttribute('aria-label') ||\n (el.id ? document.querySelector('label[for=\"' + el.id + '\"]')?.textContent?.trim() : null) ||\n el.closest('label')?.textContent?.trim() ||\n el.placeholder ||\n '';\n\n // Extract value based on type\n let value;\n if (tag === 'select') {\n const selected = el.options[el.selectedIndex];\n value = selected ? selected.textContent.trim() : '';\n } else if (type === 'checkbox' || type === 'radio') {\n value = el.checked;\n } else if (type === 'password') {\n value = el.value ? '••••' : '';\n } else if (el.isContentEditable) {\n value = el.textContent?.trim()?.slice(0, 200) || '';\n } else {\n value = el.value || '';\n }\n\n return {\n tag,\n type,\n name,\n label: label.slice(0, 80),\n value: typeof value === 'string' ? value.slice(0, 200) : value,\n required: !!el.required,\n disabled: !!el.disabled,\n ref: el.dataset?.ref || null,\n };\n }\n\n const result = { forms: [], orphanFields: [] };\n\n // Collect forms\n for (const form of document.forms) {\n const fields = [];\n for (const el of form.elements) {\n const field = extractField(el);\n if (field) fields.push(field);\n }\n result.forms.push({\n id: form.id || '',\n name: form.name || '',\n action: form.action || '',\n method: (form.method || 'get').toUpperCase(),\n fields,\n });\n }\n\n // Collect orphan fields (not in a <form>)\n const allInputs = document.querySelectorAll(\n 'input, textarea, select, [contenteditable=\"true\"]'\n );\n for (const el of allInputs) {\n if (!el.form) {\n const field = extractField(el);\n if (field) result.orphanFields.push(field);\n }\n }\n\n return result;\n})()\n`;\n","/**\n * Interactive element classification — identifies what's clickable/editable.\n * Based on Lightpanda's InteractivityType approach.\n */\nexport const INTERACTIVE_ELEMENTS_SCRIPT = `\n(() => {\n const results = [];\n\n function classify(el) {\n const tag = el.tagName.toLowerCase();\n const role = el.getAttribute('role');\n const types = [];\n\n // Native interactive\n if (['a', 'button', 'input', 'select', 'textarea', 'details', 'summary'].includes(tag)) {\n types.push('native');\n }\n\n // ARIA role interactive\n if (role && ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'tab', 'switch', 'menuitem', 'slider'].includes(role)) {\n types.push('aria');\n }\n\n // Contenteditable\n if (el.contentEditable === 'true') types.push('contenteditable');\n\n // Focusable\n if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) types.push('focusable');\n\n // Has click listener (approximate)\n if (el.onclick) types.push('listener');\n\n return types;\n }\n\n let idx = 0;\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);\n let node;\n while (node = walker.nextNode()) {\n const types = classify(node);\n if (types.length === 0) continue;\n\n const style = getComputedStyle(node);\n if (style.display === 'none' || style.visibility === 'hidden') continue;\n\n const rect = node.getBoundingClientRect();\n results.push({\n index: idx++,\n tag: node.tagName.toLowerCase(),\n role: node.getAttribute('role') || '',\n text: (node.textContent || '').trim().slice(0, 100),\n types,\n ariaLabel: node.getAttribute('aria-label') || '',\n rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },\n });\n }\n\n return results;\n})()\n`;\n","/**\n * Network interceptor script — patches fetch and XHR to capture responses.\n * Based on OpenCLI's interception approach.\n */\nexport function buildInterceptorScript(pattern: string): string {\n return `\n(() => {\n if (window.__lobster_interceptor__) return;\n window.__lobster_interceptor__ = { requests: [] };\n const store = window.__lobster_interceptor__;\n const pattern = ${JSON.stringify(pattern)};\n\n // Patch fetch\n const origFetch = window.fetch;\n window.fetch = async function(...args) {\n const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';\n const resp = await origFetch.apply(this, args);\n if (url.includes(pattern)) {\n const clone = resp.clone();\n try {\n const body = await clone.json();\n store.requests.push({ url, method: 'GET', status: resp.status, body, timestamp: Date.now() });\n } catch {}\n }\n return resp;\n };\n\n // Patch XHR\n const origOpen = XMLHttpRequest.prototype.open;\n const origSend = XMLHttpRequest.prototype.send;\n XMLHttpRequest.prototype.open = function(method, url, ...rest) {\n this.__url = url;\n this.__method = method;\n return origOpen.call(this, method, url, ...rest);\n };\n XMLHttpRequest.prototype.send = function(...args) {\n this.addEventListener('load', function() {\n if (this.__url && this.__url.includes(pattern)) {\n try {\n const body = JSON.parse(this.responseText);\n store.requests.push({ url: this.__url, method: this.__method, status: this.status, body, timestamp: Date.now() });\n } catch {}\n }\n });\n return origSend.apply(this, args);\n };\n})()\n`;\n}\n\nexport const GET_INTERCEPTED_SCRIPT = `\n(() => {\n const store = window.__lobster_interceptor__;\n if (!store) return [];\n const reqs = [...store.requests];\n store.requests = [];\n return reqs;\n})()\n`;\n","/**\n * Semantic Element Finding — match elements by natural language.\n *\n * Uses Jaccard similarity with synonym expansion, role boost,\n * and prefix matching. Zero external dependencies — runs in Node.\n *\n * Inspired by PinchTab's hybrid lexical+embedding matcher, built from scratch.\n */\n\nexport interface FindMatch {\n ref: number;\n score: number;\n text: string;\n role: string;\n tag: string;\n}\n\nexport interface FindOptions {\n maxResults?: number; // default 5\n minScore?: number; // default 0.3\n}\n\ninterface InteractiveElement {\n index: number;\n tag: string;\n role: string;\n text: string;\n types: string[];\n ariaLabel: string;\n}\n\n// ── Synonym table ──\nconst SYNONYMS: Record<string, string[]> = {\n btn: ['button'],\n button: ['btn', 'submit', 'click'],\n submit: ['go', 'send', 'ok', 'confirm', 'done', 'button'],\n search: ['find', 'lookup', 'query', 'filter'],\n login: ['signin', 'sign-in', 'log-in', 'authenticate'],\n signup: ['register', 'create-account', 'sign-up', 'join'],\n logout: ['signout', 'sign-out', 'log-out'],\n close: ['dismiss', 'x', 'cancel', 'exit'],\n menu: ['nav', 'navigation', 'hamburger', 'sidebar'],\n nav: ['navigation', 'menu', 'navbar'],\n input: ['field', 'textbox', 'text', 'entry'],\n email: ['mail', 'e-mail'],\n password: ['pass', 'pwd', 'secret'],\n next: ['continue', 'forward', 'proceed'],\n back: ['previous', 'return', 'go-back'],\n save: ['store', 'keep', 'persist'],\n delete: ['remove', 'trash', 'discard', 'destroy'],\n edit: ['modify', 'change', 'update'],\n add: ['create', 'new', 'plus', 'insert'],\n settings: ['preferences', 'config', 'options', 'gear'],\n profile: ['account', 'user', 'avatar'],\n home: ['main', 'dashboard', 'start'],\n link: ['anchor', 'href', 'url'],\n select: ['dropdown', 'combo', 'picker', 'choose'],\n checkbox: ['check', 'toggle', 'tick'],\n upload: ['attach', 'file', 'browse'],\n download: ['save', 'export'],\n};\n\n// ── Role keywords that boost score ──\nconst ROLE_KEYWORDS = new Set([\n 'button', 'link', 'input', 'textbox', 'checkbox', 'radio',\n 'select', 'dropdown', 'tab', 'menu', 'menuitem', 'switch',\n 'slider', 'combobox', 'searchbox', 'option',\n]);\n\n/**\n * Tokenize a string into lowercase words.\n */\nfunction tokenize(text: string): string[] {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, ' ')\n .split(/[\\s-]+/)\n .filter((t) => t.length > 0);\n}\n\n/**\n * Expand tokens with synonyms.\n */\nfunction expandSynonyms(tokens: string[]): Set<string> {\n const expanded = new Set(tokens);\n for (const token of tokens) {\n const syns = SYNONYMS[token];\n if (syns) {\n for (const syn of syns) expanded.add(syn);\n }\n }\n return expanded;\n}\n\n/**\n * Build frequency map.\n */\nfunction freqMap(tokens: string[]): Map<string, number> {\n const map = new Map<string, number>();\n for (const t of tokens) {\n map.set(t, (map.get(t) || 0) + 1);\n }\n return map;\n}\n\n/**\n * Jaccard similarity with frequency weighting.\n */\nfunction jaccardScore(queryTokens: string[], descTokens: string[]): number {\n const qFreq = freqMap(queryTokens);\n const dFreq = freqMap(descTokens);\n\n let intersection = 0;\n let union = 0;\n\n const allTokens = new Set([...qFreq.keys(), ...dFreq.keys()]);\n for (const token of allTokens) {\n const qCount = qFreq.get(token) || 0;\n const dCount = dFreq.get(token) || 0;\n intersection += Math.min(qCount, dCount);\n union += Math.max(qCount, dCount);\n }\n\n return union === 0 ? 0 : intersection / union;\n}\n\n/**\n * Prefix matching score — handles abbreviations.\n * \"btn\" matches \"button\" partially.\n */\nfunction prefixScore(queryTokens: string[], descTokens: string[]): number {\n if (queryTokens.length === 0 || descTokens.length === 0) return 0;\n\n let matches = 0;\n for (const qt of queryTokens) {\n if (qt.length < 3) continue;\n for (const dt of descTokens) {\n if (dt.startsWith(qt) || qt.startsWith(dt)) {\n matches += 0.5;\n break;\n }\n }\n }\n\n return Math.min(matches / queryTokens.length, 0.3);\n}\n\n/**\n * Role keyword boost — if query mentions a role and element matches.\n */\nfunction roleBoost(queryTokens: string[], elementRole: string): number {\n const roleLower = elementRole.toLowerCase();\n for (const qt of queryTokens) {\n if (ROLE_KEYWORDS.has(qt) && roleLower.includes(qt)) {\n return 0.2;\n }\n }\n return 0;\n}\n\n/**\n * Score a single element against the query.\n */\nfunction scoreElement(\n queryTokens: string[],\n queryExpanded: Set<string>,\n element: InteractiveElement,\n): number {\n // Build description from all element text sources\n const descParts = [\n element.text,\n element.role,\n element.tag,\n element.ariaLabel,\n ].filter(Boolean);\n const descText = descParts.join(' ');\n const descTokens = tokenize(descText);\n\n if (descTokens.length === 0) return 0;\n\n // Expand description tokens too\n const descExpanded = expandSynonyms(descTokens);\n\n // 1. Jaccard similarity on expanded token sets\n const expandedQueryTokens = [...queryExpanded];\n const expandedDescTokens = [...descExpanded];\n const jaccard = jaccardScore(expandedQueryTokens, expandedDescTokens);\n\n // 2. Prefix matching\n const prefix = prefixScore(queryTokens, descTokens);\n\n // 3. Role keyword boost\n const role = roleBoost(queryTokens, element.role || element.tag);\n\n // 4. Exact substring match bonus\n const queryStr = queryTokens.join(' ');\n const descStr = descTokens.join(' ');\n const exactBonus = descStr.includes(queryStr) ? 0.3 : 0;\n\n return Math.min(jaccard + prefix + role + exactBonus, 1.0);\n}\n\n/**\n * Find elements matching a natural language query.\n *\n * @param elements - Interactive elements from INTERACTIVE_ELEMENTS_SCRIPT\n * @param query - Natural language description (e.g., \"login button\")\n * @param options - maxResults (default 5), minScore (default 0.3)\n */\nexport function semanticFind(\n elements: InteractiveElement[],\n query: string,\n options?: FindOptions,\n): FindMatch[] {\n const maxResults = options?.maxResults ?? 5;\n const minScore = options?.minScore ?? 0.3;\n\n const queryTokens = tokenize(query);\n if (queryTokens.length === 0) return [];\n\n const queryExpanded = expandSynonyms(queryTokens);\n\n const scored: FindMatch[] = [];\n\n for (const el of elements) {\n const score = scoreElement(queryTokens, queryExpanded, el);\n if (score >= minScore) {\n scored.push({\n ref: el.index,\n score: Math.round(score * 100) / 100,\n text: (el.text || el.ariaLabel || '').slice(0, 60),\n role: el.role || el.tag,\n tag: el.tag,\n });\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n return scored.slice(0, maxResults);\n}\n","import type { Page } from 'puppeteer-core';\nimport type {\n IPage, WaitCondition, Cookie, NetworkEntry, TabInfo,\n SnapshotOptions, SemanticTreeOptions, FlatDomTree, BrowserState, FormState,\n FindMatch, FindOptions,\n} from '../types/page.js';\nimport { FLAT_TREE_SCRIPT, flatTreeToString } from './dom/flat-tree.js';\nimport { SNAPSHOT_SCRIPT } from './dom/snapshot.js';\nimport { COMPACT_SNAPSHOT_SCRIPT } from './dom/compact-snapshot.js';\nimport { SEMANTIC_TREE_SCRIPT } from './dom/semantic-tree.js';\nimport { MARKDOWN_SCRIPT } from './dom/markdown.js';\nimport { FORM_STATE_SCRIPT } from './dom/form-state.js';\nimport { INTERACTIVE_ELEMENTS_SCRIPT } from './dom/interactive.js';\nimport { buildInterceptorScript, GET_INTERCEPTED_SCRIPT } from './interceptor.js';\nimport { semanticFind } from './semantic-find.js';\n\nexport class PuppeteerPage implements IPage {\n private page: Page;\n\n constructor(page: Page) {\n this.page = page;\n }\n\n get raw(): Page { return this.page; }\n\n async goto(url: string, options?: { waitUntil?: WaitCondition; timeout?: number }): Promise<void> {\n await this.page.goto(url, {\n waitUntil: (options?.waitUntil as any) || 'networkidle2',\n timeout: options?.timeout || 30000,\n });\n }\n\n async goBack(): Promise<void> {\n await this.page.goBack({ waitUntil: 'networkidle2' });\n }\n\n async url(): Promise<string> {\n return this.page.url();\n }\n\n async title(): Promise<string> {\n return this.page.title();\n }\n\n async evaluate<T = unknown>(js: string): Promise<T> {\n return this.page.evaluate(js) as Promise<T>;\n }\n\n async snapshot(opts?: SnapshotOptions): Promise<string> {\n if (opts?.compact) {\n return this.page.evaluate(COMPACT_SNAPSHOT_SCRIPT) as Promise<string>;\n }\n return this.page.evaluate(SNAPSHOT_SCRIPT) as Promise<string>;\n }\n\n async semanticTree(_opts?: SemanticTreeOptions): Promise<string> {\n return this.page.evaluate(SEMANTIC_TREE_SCRIPT) as Promise<string>;\n }\n\n async flatTree(): Promise<FlatDomTree> {\n const raw = await this.page.evaluate(FLAT_TREE_SCRIPT);\n return raw as FlatDomTree;\n }\n\n async markdown(): Promise<string> {\n return this.page.evaluate(MARKDOWN_SCRIPT) as Promise<string>;\n }\n\n async browserState(): Promise<BrowserState> {\n const state = await this.page.evaluate(`\n (() => {\n const scrollY = window.scrollY;\n const scrollX = window.scrollX;\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const pageW = document.documentElement.scrollWidth;\n const pageH = document.documentElement.scrollHeight;\n const maxScrollY = pageH - vpH;\n return {\n url: location.href,\n title: document.title,\n viewportWidth: vpW,\n viewportHeight: vpH,\n pageWidth: pageW,\n pageHeight: pageH,\n scrollX: scrollX,\n scrollY: scrollY,\n scrollPercent: maxScrollY > 0 ? Math.round((scrollY / maxScrollY) * 100) : 0,\n pixelsAbove: Math.round(scrollY),\n pixelsBelow: Math.round(Math.max(0, maxScrollY - scrollY)),\n };\n })()\n `) as BrowserState;\n return state;\n }\n\n async formState(): Promise<FormState> {\n return this.page.evaluate(FORM_STATE_SCRIPT) as Promise<FormState>;\n }\n\n async click(ref: string | number): Promise<void> {\n if (typeof ref === 'number') {\n await this.page.evaluate((idx) => {\n const el = document.querySelector('[data-ref=\"' + idx + '\"]') as HTMLElement;\n if (!el) throw new Error('Element with index ' + idx + ' not found');\n\n // Blur previously focused element\n const prev = document.activeElement as HTMLElement | null;\n if (prev && prev !== el && prev !== document.body) {\n prev.blur();\n prev.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, cancelable: true }));\n prev.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false, cancelable: true }));\n }\n\n // Scroll into view\n if (typeof (el as any).scrollIntoViewIfNeeded === 'function') {\n (el as any).scrollIntoViewIfNeeded();\n } else {\n el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });\n }\n\n // Full mouse event sequence — required for React, analytics, custom handlers\n el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true }));\n el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));\n el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));\n el.focus();\n el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));\n el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));\n }, ref);\n // Wait for click processing (animations, state updates)\n await new Promise((r) => setTimeout(r, 200));\n } else {\n await this.page.click(ref);\n }\n }\n\n async typeText(ref: string | number, text: string): Promise<void> {\n if (typeof ref === 'number') {\n // First click the element (triggers full event sequence + focus)\n await this.click(ref);\n\n await this.page.evaluate((idx, txt) => {\n const el = document.querySelector('[data-ref=\"' + idx + '\"]') as HTMLElement;\n if (!el) throw new Error('Element with index ' + idx + ' not found');\n\n const isInput = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';\n const isContentEditable = el.isContentEditable;\n\n if (isContentEditable) {\n // ── Contenteditable: Plan A — synthetic InputEvents ──\n // Works for: React contenteditable, Quill\n // Clear existing content\n if (el.dispatchEvent(new InputEvent('beforeinput', {\n bubbles: true, cancelable: true, inputType: 'deleteContent',\n }))) {\n el.innerText = '';\n el.dispatchEvent(new InputEvent('input', {\n bubbles: true, inputType: 'deleteContent',\n }));\n }\n\n // Insert new text\n if (el.dispatchEvent(new InputEvent('beforeinput', {\n bubbles: true, cancelable: true, inputType: 'insertText', data: txt,\n }))) {\n el.innerText = txt;\n el.dispatchEvent(new InputEvent('input', {\n bubbles: true, inputType: 'insertText', data: txt,\n }));\n }\n\n // Verify Plan A worked\n const planAOk = el.innerText.trim() === txt.trim();\n\n if (!planAOk) {\n // ── Plan B — execCommand fallback ──\n // Works for: Slate.js, some rich-text editors\n el.focus();\n const doc = el.ownerDocument;\n const sel = (doc.defaultView || window).getSelection();\n const range = doc.createRange();\n range.selectNodeContents(el);\n sel?.removeAllRanges();\n sel?.addRange(range);\n doc.execCommand('delete', false);\n doc.execCommand('insertText', false, txt);\n }\n\n el.dispatchEvent(new Event('change', { bubbles: true }));\n el.blur();\n\n } else if (isInput) {\n // ── Input/Textarea: use native value setter to bypass React/Vue ──\n const inputEl = el as HTMLInputElement | HTMLTextAreaElement;\n const proto = Object.getPrototypeOf(inputEl);\n const descriptor =\n Object.getOwnPropertyDescriptor(proto, 'value') ||\n Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value') ||\n Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');\n\n if (descriptor?.set) {\n descriptor.set.call(inputEl, txt);\n } else {\n inputEl.value = txt;\n }\n\n inputEl.dispatchEvent(new Event('input', { bubbles: true }));\n inputEl.dispatchEvent(new Event('change', { bubbles: true }));\n } else {\n // Fallback: try setting value anyway\n (el as any).value = txt;\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n }\n }, ref, text);\n } else {\n // CSS selector path — click to focus, then use keyboard\n await this.page.click(ref, { count: 3 });\n await this.page.keyboard.type(text);\n }\n }\n\n async pressKey(key: string): Promise<void> {\n await this.page.keyboard.press(key as any);\n }\n\n async selectOption(ref: string | number, value: string): Promise<void> {\n const selector = typeof ref === 'number' ? '[data-ref=\"' + ref + '\"]' : ref;\n await this.page.select(selector, value);\n }\n\n async scroll(direction: 'up' | 'down' | 'left' | 'right', amount?: number): Promise<void> {\n const distance = amount || 500;\n const isVertical = direction === 'up' || direction === 'down';\n const positive = direction === 'down' || direction === 'right';\n const delta = positive ? distance : -distance;\n\n await this.page.evaluate((dy, dx, isVert) => {\n // Helper: check if element is a valid scroll container\n const canScroll = (el) => {\n if (!el) return false;\n const s = getComputedStyle(el);\n if (isVert) {\n return /(auto|scroll|overlay)/.test(s.overflowY) &&\n el.scrollHeight > el.clientHeight &&\n el.clientHeight >= window.innerHeight * 0.3;\n } else {\n return /(auto|scroll|overlay)/.test(s.overflowX) &&\n el.scrollWidth > el.clientWidth &&\n el.clientWidth >= window.innerWidth * 0.3;\n }\n };\n\n // Walk from active element up to find a scrollable container\n let el = document.activeElement;\n while (el && !canScroll(el) && el !== document.body) {\n el = el.parentElement;\n }\n\n // If no scrollable ancestor, search the DOM\n if (!canScroll(el)) {\n el = Array.from(document.querySelectorAll('*')).find(canScroll) || null;\n }\n\n const isPageLevel = !el || el === document.body ||\n el === document.documentElement || el === document.scrollingElement;\n\n if (isPageLevel) {\n // Page-level scroll\n if (isVert) {\n window.scrollBy(0, dy);\n } else {\n window.scrollBy(dx, 0);\n }\n } else {\n // Container scroll\n if (isVert) {\n el.scrollBy({ top: dy, behavior: 'smooth' });\n } else {\n el.scrollBy({ left: dx, behavior: 'smooth' });\n }\n }\n }, isVertical ? delta : 0, isVertical ? 0 : delta, isVertical);\n\n // Wait for smooth scroll to settle\n await new Promise((r) => setTimeout(r, 150));\n }\n\n async scrollToElement(ref: string | number): Promise<void> {\n const selector = typeof ref === 'number' ? '[data-ref=\"' + ref + '\"]' : ref;\n await this.page.evaluate((sel) => {\n const el = document.querySelector(sel);\n if (!el) return;\n if (typeof (el as any).scrollIntoViewIfNeeded === 'function') {\n (el as any).scrollIntoViewIfNeeded();\n } else {\n el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });\n }\n }, selector);\n }\n\n async getCookies(opts?: { domain?: string }): Promise<Cookie[]> {\n const cookies = await this.page.cookies();\n const filtered = opts?.domain\n ? cookies.filter((c) => c.domain.includes(opts.domain!))\n : cookies;\n return filtered.map((c) => ({\n name: c.name,\n value: c.value,\n domain: c.domain,\n path: c.path,\n expires: c.expires,\n httpOnly: c.httpOnly,\n secure: c.secure,\n sameSite: c.sameSite as Cookie['sameSite'],\n }));\n }\n\n async wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void> {\n if (typeof options === 'number') {\n await new Promise((r) => setTimeout(r, options * 1000));\n return;\n }\n if (options.time) {\n await new Promise((r) => setTimeout(r, options.time! * 1000));\n }\n if (options.text) {\n await this.page.waitForFunction(\n (t) => document.body.innerText.includes(t),\n { timeout: options.timeout || 30000 },\n options.text\n );\n }\n }\n\n async networkRequests(includeStatic?: boolean): Promise<NetworkEntry[]> {\n // Use Performance API to extract network requests from the browser\n const entries = await this.page.evaluate(`\n (() => {\n const entries = performance.getEntriesByType('resource');\n const staticTypes = new Set(['img', 'font', 'css', 'script', 'link']);\n const includeStatic = ${!!includeStatic};\n\n return entries\n .filter(e => includeStatic || !staticTypes.has(e.initiatorType))\n .map(e => ({\n url: e.name,\n method: 'GET',\n status: 200,\n type: e.initiatorType || 'other',\n size: e.transferSize || e.encodedBodySize || 0,\n duration: Math.round(e.duration),\n }));\n })()\n `) as NetworkEntry[];\n return entries || [];\n }\n\n async installInterceptor(pattern: string): Promise<void> {\n await this.page.evaluate(buildInterceptorScript(pattern));\n }\n\n async getInterceptedRequests(): Promise<unknown[]> {\n return this.page.evaluate(GET_INTERCEPTED_SCRIPT) as Promise<unknown[]>;\n }\n\n async screenshot(opts?: { format?: 'png' | 'jpeg'; fullPage?: boolean }): Promise<Buffer> {\n const result = await this.page.screenshot({\n type: opts?.format || 'png',\n fullPage: opts?.fullPage ?? false,\n });\n return Buffer.from(result);\n }\n\n async tabs(): Promise<TabInfo[]> {\n const browser = this.page.browser();\n const pages = await browser.pages();\n return pages.map((p, i) => ({\n id: i,\n url: p.url(),\n title: '',\n active: p === this.page,\n }));\n }\n\n async find(query: string, options?: FindOptions): Promise<FindMatch[]> {\n const elements = await this.page.evaluate(INTERACTIVE_ELEMENTS_SCRIPT) as any[];\n return semanticFind(elements, query, options);\n }\n\n async close(): Promise<void> {\n await this.page.close();\n }\n}\n"],"mappings":";AAMO,IAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC6BzB,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC1BxB,IAAM,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACChC,IAAM,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACE7B,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACJxB,IAAM,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACJ1B,IAAM,8BAA8B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":["el"]}