/** * Read commands — extract data from pages without side effects * * text, html, links, forms, accessibility, js, eval, css, attrs, * console, network, cookies, storage, perf */ import type { BrowserManager } from './browser-manager'; import { consoleBuffer, networkBuffer } from './buffers'; import * as fs from 'fs'; export async function handleReadCommand( command: string, args: string[], bm: BrowserManager ): Promise { const page = bm.getPage(); switch (command) { case 'text': { return await page.evaluate(() => { const body = document.body; if (!body) return ''; const clone = body.cloneNode(true) as HTMLElement; clone.querySelectorAll('script, style, noscript, svg').forEach(el => el.remove()); return clone.innerText .split('\n') .map(line => line.trim()) .filter(line => line.length > 0) .join('\n'); }); } case 'html': { const selector = args[0]; if (selector) { return await page.innerHTML(selector); } return await page.content(); } case 'links': { const links = await page.evaluate(() => [...document.querySelectorAll('a[href]')].map(a => ({ text: a.textContent?.trim().slice(0, 120) || '', href: (a as HTMLAnchorElement).href, })).filter(l => l.text && l.href) ); return links.map(l => `${l.text} → ${l.href}`).join('\n'); } case 'forms': { const forms = await page.evaluate(() => { return [...document.querySelectorAll('form')].map((form, i) => { const fields = [...form.querySelectorAll('input, select, textarea')].map(el => { const input = el as HTMLInputElement; return { tag: el.tagName.toLowerCase(), type: input.type || undefined, name: input.name || undefined, id: input.id || undefined, placeholder: input.placeholder || undefined, required: input.required || undefined, value: input.value || undefined, options: el.tagName === 'SELECT' ? [...(el as HTMLSelectElement).options].map(o => ({ value: o.value, text: o.text })) : undefined, }; }); return { index: i, action: form.action || undefined, method: form.method || 'get', id: form.id || undefined, fields, }; }); }); return JSON.stringify(forms, null, 2); } case 'accessibility': { const snapshot = await page.locator("body").ariaSnapshot(); return snapshot; } case 'js': { const expr = args[0]; if (!expr) throw new Error('Usage: browse js '); const result = await page.evaluate(expr); return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); } case 'eval': { const filePath = args[0]; if (!filePath) throw new Error('Usage: browse eval '); if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`); const code = fs.readFileSync(filePath, 'utf-8'); const result = await page.evaluate(code); return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); } case 'css': { const [selector, property] = args; if (!selector || !property) throw new Error('Usage: browse css '); const value = await page.evaluate( ([sel, prop]) => { const el = document.querySelector(sel); if (!el) return `Element not found: ${sel}`; return getComputedStyle(el).getPropertyValue(prop); }, [selector, property] ); return value; } case 'attrs': { const selector = args[0]; if (!selector) throw new Error('Usage: browse attrs '); const attrs = await page.evaluate((sel) => { const el = document.querySelector(sel); if (!el) return `Element not found: ${sel}`; const result: Record = {}; for (const attr of el.attributes) { result[attr.name] = attr.value; } return result; }, selector); return typeof attrs === 'string' ? attrs : JSON.stringify(attrs, null, 2); } case 'console': { if (args[0] === '--clear') { consoleBuffer.length = 0; return 'Console buffer cleared.'; } if (consoleBuffer.length === 0) return '(no console messages)'; return consoleBuffer.map(e => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}` ).join('\n'); } case 'network': { if (args[0] === '--clear') { networkBuffer.length = 0; return 'Network buffer cleared.'; } if (networkBuffer.length === 0) return '(no network requests)'; return networkBuffer.map(e => `${e.method} ${e.url} → ${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)` ).join('\n'); } case 'cookies': { const cookies = await page.context().cookies(); return JSON.stringify(cookies, null, 2); } case 'storage': { if (args[0] === 'set' && args[1]) { const key = args[1]; const value = args[2] || ''; await page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]); return `Set localStorage["${key}"] = "${value}"`; } const storage = await page.evaluate(() => ({ localStorage: { ...localStorage }, sessionStorage: { ...sessionStorage }, })); return JSON.stringify(storage, null, 2); } case 'perf': { const timings = await page.evaluate(() => { const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; if (!nav) return 'No navigation timing data available.'; return { dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart), tcp: Math.round(nav.connectEnd - nav.connectStart), ssl: Math.round(nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0), ttfb: Math.round(nav.responseStart - nav.requestStart), download: Math.round(nav.responseEnd - nav.responseStart), domParse: Math.round(nav.domInteractive - nav.responseEnd), domReady: Math.round(nav.domContentLoadedEventEnd - nav.startTime), load: Math.round(nav.loadEventEnd - nav.startTime), total: Math.round(nav.loadEventEnd - nav.startTime), }; }); if (typeof timings === 'string') return timings; return Object.entries(timings) .map(([k, v]) => `${k.padEnd(12)} ${v}ms`) .join('\n'); } default: throw new Error(`Unknown read command: ${command}`); } }