@hasna/browser 0.0.2 → 0.0.3

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.
package/dist/mcp/index.js CHANGED
@@ -217,6 +217,213 @@ function runMigrations(db) {
217
217
  var _db = null, _dbPath = null;
218
218
  var init_schema = () => {};
219
219
 
220
+ // src/db/console-log.ts
221
+ var exports_console_log = {};
222
+ __export(exports_console_log, {
223
+ logConsoleMessage: () => logConsoleMessage,
224
+ getConsoleMessage: () => getConsoleMessage,
225
+ getConsoleLog: () => getConsoleLog,
226
+ clearConsoleLog: () => clearConsoleLog
227
+ });
228
+ import { randomUUID as randomUUID3 } from "crypto";
229
+ function logConsoleMessage(data) {
230
+ const db = getDatabase();
231
+ const id = randomUUID3();
232
+ db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
233
+ return getConsoleMessage(id);
234
+ }
235
+ function getConsoleMessage(id) {
236
+ const db = getDatabase();
237
+ return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
238
+ }
239
+ function getConsoleLog(sessionId, level) {
240
+ const db = getDatabase();
241
+ if (level) {
242
+ return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
243
+ }
244
+ return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
245
+ }
246
+ function clearConsoleLog(sessionId) {
247
+ const db = getDatabase();
248
+ db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
249
+ }
250
+ var init_console_log = __esm(() => {
251
+ init_schema();
252
+ });
253
+
254
+ // src/lib/snapshot.ts
255
+ function getLastSnapshot(sessionId) {
256
+ return lastSnapshots.get(sessionId) ?? null;
257
+ }
258
+ function setLastSnapshot(sessionId, snapshot) {
259
+ lastSnapshots.set(sessionId, snapshot);
260
+ }
261
+ async function takeSnapshot(page, sessionId) {
262
+ let ariaTree;
263
+ try {
264
+ ariaTree = await page.locator("body").ariaSnapshot();
265
+ } catch {
266
+ ariaTree = "";
267
+ }
268
+ const refs = {};
269
+ const refMap = new Map;
270
+ let refCounter = 0;
271
+ for (const role of INTERACTIVE_ROLES) {
272
+ const locators = page.getByRole(role);
273
+ const count = await locators.count();
274
+ for (let i = 0;i < count; i++) {
275
+ const el = locators.nth(i);
276
+ let name = "";
277
+ let visible = false;
278
+ let enabled = true;
279
+ let value;
280
+ let checked;
281
+ try {
282
+ visible = await el.isVisible();
283
+ if (!visible)
284
+ continue;
285
+ } catch {
286
+ continue;
287
+ }
288
+ try {
289
+ name = await el.evaluate((e) => {
290
+ const el2 = e;
291
+ return el2.getAttribute("aria-label") ?? el2.textContent?.trim().slice(0, 80) ?? el2.getAttribute("title") ?? el2.getAttribute("placeholder") ?? "";
292
+ });
293
+ } catch {
294
+ continue;
295
+ }
296
+ if (!name)
297
+ continue;
298
+ try {
299
+ enabled = await el.isEnabled();
300
+ } catch {}
301
+ try {
302
+ if (role === "checkbox" || role === "radio" || role === "switch") {
303
+ checked = await el.isChecked();
304
+ }
305
+ } catch {}
306
+ try {
307
+ if (role === "textbox" || role === "searchbox" || role === "spinbutton" || role === "combobox") {
308
+ value = await el.inputValue();
309
+ }
310
+ } catch {}
311
+ const ref = `@e${refCounter}`;
312
+ refCounter++;
313
+ refs[ref] = { role, name, visible, enabled, value, checked };
314
+ const escapedName = name.replace(/"/g, "\\\"");
315
+ refMap.set(ref, { role, name, locatorSelector: `role=${role}[name="${escapedName}"]` });
316
+ }
317
+ }
318
+ let annotatedTree = ariaTree;
319
+ for (const [ref, info] of Object.entries(refs)) {
320
+ const escapedName = info.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321
+ const pattern = new RegExp(`(${info.role}\\s+"${escapedName.slice(0, 40)}[^"]*")`, "m");
322
+ const match = annotatedTree.match(pattern);
323
+ if (match) {
324
+ annotatedTree = annotatedTree.replace(match[0], `${match[0]} [${ref}]`);
325
+ }
326
+ }
327
+ const unmatchedRefs = Object.entries(refs).filter(([ref]) => !annotatedTree.includes(`[${ref}]`));
328
+ if (unmatchedRefs.length > 0) {
329
+ annotatedTree += `
330
+
331
+ --- Interactive elements ---`;
332
+ for (const [ref, info] of unmatchedRefs) {
333
+ const extras = [];
334
+ if (info.checked !== undefined)
335
+ extras.push(`checked=${info.checked}`);
336
+ if (!info.enabled)
337
+ extras.push("disabled");
338
+ if (info.value)
339
+ extras.push(`value="${info.value}"`);
340
+ const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
341
+ annotatedTree += `
342
+ ${info.role} "${info.name}" [${ref}]${extrasStr}`;
343
+ }
344
+ }
345
+ if (sessionId) {
346
+ sessionRefMaps.set(sessionId, refMap);
347
+ }
348
+ return {
349
+ tree: annotatedTree,
350
+ refs,
351
+ interactive_count: refCounter
352
+ };
353
+ }
354
+ function getRefLocator(page, sessionId, ref) {
355
+ const refMap = sessionRefMaps.get(sessionId);
356
+ if (!refMap)
357
+ throw new Error(`No snapshot taken for session ${sessionId}. Call browser_snapshot first.`);
358
+ const entry = refMap.get(ref);
359
+ if (!entry)
360
+ throw new Error(`Ref ${ref} not found. Available refs: ${[...refMap.keys()].slice(0, 20).join(", ")}`);
361
+ return page.getByRole(entry.role, { name: entry.name }).first();
362
+ }
363
+ function refKey(info) {
364
+ return `${info.role}::${info.name}`;
365
+ }
366
+ function diffSnapshots(before, after) {
367
+ const beforeMap = new Map;
368
+ for (const [ref, info] of Object.entries(before.refs)) {
369
+ beforeMap.set(refKey(info), { ref, info });
370
+ }
371
+ const afterMap = new Map;
372
+ for (const [ref, info] of Object.entries(after.refs)) {
373
+ afterMap.set(refKey(info), { ref, info });
374
+ }
375
+ const added = [];
376
+ const removed = [];
377
+ const modified = [];
378
+ for (const [key, afterEntry] of afterMap) {
379
+ const beforeEntry = beforeMap.get(key);
380
+ if (!beforeEntry) {
381
+ added.push({ ref: afterEntry.ref, info: afterEntry.info });
382
+ } else {
383
+ const b = beforeEntry.info;
384
+ const a = afterEntry.info;
385
+ if (b.visible !== a.visible || b.enabled !== a.enabled || b.value !== a.value || b.checked !== a.checked || b.description !== a.description) {
386
+ modified.push({ ref: afterEntry.ref, before: b, after: a });
387
+ }
388
+ }
389
+ }
390
+ for (const [key, beforeEntry] of beforeMap) {
391
+ if (!afterMap.has(key)) {
392
+ removed.push({ ref: beforeEntry.ref, info: beforeEntry.info });
393
+ }
394
+ }
395
+ const url_changed = before.tree.split(`
396
+ `)[0] !== after.tree.split(`
397
+ `)[0];
398
+ const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
399
+ return { added, removed, modified, url_changed, title_changed };
400
+ }
401
+ var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
402
+ var init_snapshot = __esm(() => {
403
+ lastSnapshots = new Map;
404
+ sessionRefMaps = new Map;
405
+ INTERACTIVE_ROLES = [
406
+ "button",
407
+ "link",
408
+ "textbox",
409
+ "checkbox",
410
+ "radio",
411
+ "combobox",
412
+ "menuitem",
413
+ "menuitemcheckbox",
414
+ "menuitemradio",
415
+ "option",
416
+ "searchbox",
417
+ "slider",
418
+ "spinbutton",
419
+ "switch",
420
+ "tab",
421
+ "treeitem",
422
+ "listbox",
423
+ "menu"
424
+ ];
425
+ });
426
+
220
427
  // node_modules/sharp/lib/is.js
221
428
  var require_is = __commonJS((exports, module) => {
222
429
  /*!
@@ -6612,38 +6819,63 @@ var require_lib = __commonJS((exports, module) => {
6612
6819
  module.exports = Sharp;
6613
6820
  });
6614
6821
 
6615
- // src/db/console-log.ts
6616
- var exports_console_log = {};
6617
- __export(exports_console_log, {
6618
- logConsoleMessage: () => logConsoleMessage,
6619
- getConsoleMessage: () => getConsoleMessage,
6620
- getConsoleLog: () => getConsoleLog,
6621
- clearConsoleLog: () => clearConsoleLog
6822
+ // src/lib/annotate.ts
6823
+ var exports_annotate = {};
6824
+ __export(exports_annotate, {
6825
+ annotateScreenshot: () => annotateScreenshot
6622
6826
  });
6623
- import { randomUUID as randomUUID4 } from "crypto";
6624
- function logConsoleMessage(data) {
6625
- const db = getDatabase();
6626
- const id = randomUUID4();
6627
- db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
6628
- return getConsoleMessage(id);
6629
- }
6630
- function getConsoleMessage(id) {
6631
- const db = getDatabase();
6632
- return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
6633
- }
6634
- function getConsoleLog(sessionId, level) {
6635
- const db = getDatabase();
6636
- if (level) {
6637
- return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
6827
+ async function annotateScreenshot(page, sessionId) {
6828
+ const snapshot = await takeSnapshot(page, sessionId);
6829
+ const rawBuffer = await page.screenshot({ type: "png" });
6830
+ const meta = await import_sharp3.default(rawBuffer).metadata();
6831
+ const imgWidth = meta.width ?? 1280;
6832
+ const imgHeight = meta.height ?? 720;
6833
+ const annotations = [];
6834
+ const labelToRef = {};
6835
+ let labelCounter = 1;
6836
+ for (const [ref, info] of Object.entries(snapshot.refs)) {
6837
+ try {
6838
+ const locator = page.getByRole(info.role, { name: info.name }).first();
6839
+ const box = await locator.boundingBox();
6840
+ if (!box)
6841
+ continue;
6842
+ const annotation = {
6843
+ ref,
6844
+ label: labelCounter,
6845
+ x: Math.round(box.x),
6846
+ y: Math.round(box.y),
6847
+ width: Math.round(box.width),
6848
+ height: Math.round(box.height),
6849
+ role: info.role,
6850
+ name: info.name
6851
+ };
6852
+ annotations.push(annotation);
6853
+ labelToRef[labelCounter] = ref;
6854
+ labelCounter++;
6855
+ } catch {}
6638
6856
  }
6639
- return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
6640
- }
6641
- function clearConsoleLog(sessionId) {
6642
- const db = getDatabase();
6643
- db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
6644
- }
6645
- var init_console_log = __esm(() => {
6646
- init_schema();
6857
+ const circleR = 10;
6858
+ const fontSize = 12;
6859
+ const svgParts = [];
6860
+ for (const ann of annotations) {
6861
+ const cx = Math.min(Math.max(ann.x + circleR, circleR), imgWidth - circleR);
6862
+ const cy = Math.min(Math.max(ann.y - circleR - 2, circleR), imgHeight - circleR);
6863
+ svgParts.push(`
6864
+ <circle cx="${cx}" cy="${cy}" r="${circleR}" fill="#e11d48" stroke="white" stroke-width="1.5"/>
6865
+ <text x="${cx}" y="${cy + 4}" text-anchor="middle" fill="white" font-size="${fontSize}" font-family="Arial,sans-serif" font-weight="bold">${ann.label}</text>
6866
+ `);
6867
+ svgParts.push(`
6868
+ <rect x="${ann.x}" y="${ann.y}" width="${ann.width}" height="${ann.height}" fill="none" stroke="#e11d48" stroke-width="1.5" stroke-opacity="0.6" rx="2"/>
6869
+ `);
6870
+ }
6871
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${imgWidth}" height="${imgHeight}">${svgParts.join("")}</svg>`;
6872
+ const annotatedBuffer = await import_sharp3.default(rawBuffer).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).webp({ quality: 85 }).toBuffer();
6873
+ return { buffer: annotatedBuffer, annotations, labelToRef };
6874
+ }
6875
+ var import_sharp3;
6876
+ var init_annotate = __esm(() => {
6877
+ init_snapshot();
6878
+ import_sharp3 = __toESM(require_lib(), 1);
6647
6879
  });
6648
6880
 
6649
6881
  // src/mcp/index.ts
@@ -10887,106 +11119,434 @@ function selectEngine(useCase, explicit) {
10887
11119
  return preferred;
10888
11120
  }
10889
11121
 
10890
- // src/lib/session.ts
10891
- var handles = new Map;
10892
- async function createSession2(opts = {}) {
10893
- const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
10894
- const resolvedEngine = engine === "auto" ? "playwright" : engine;
10895
- let browser;
10896
- let page;
10897
- if (resolvedEngine === "lightpanda") {
10898
- browser = await connectLightpanda();
10899
- const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
10900
- page = await context.newPage();
10901
- } else {
10902
- browser = await launchPlaywright({
10903
- headless: opts.headless ?? true,
10904
- viewport: opts.viewport,
10905
- userAgent: opts.userAgent
10906
- });
10907
- page = await getPage(browser, {
10908
- viewport: opts.viewport,
10909
- userAgent: opts.userAgent
10910
- });
10911
- }
10912
- const session = createSession({
10913
- engine: resolvedEngine,
10914
- projectId: opts.projectId,
10915
- agentId: opts.agentId,
10916
- startUrl: opts.startUrl
10917
- });
10918
- handles.set(session.id, { browser, page, engine: resolvedEngine });
10919
- if (opts.startUrl) {
10920
- try {
10921
- await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
10922
- } catch (err) {}
10923
- }
10924
- return { session, page };
10925
- }
10926
- function getSessionPage(sessionId) {
10927
- const handle = handles.get(sessionId);
10928
- if (!handle)
10929
- throw new SessionNotFoundError(sessionId);
10930
- return handle.page;
10931
- }
10932
- async function closeSession2(sessionId) {
10933
- const handle = handles.get(sessionId);
10934
- if (handle) {
10935
- try {
10936
- await handle.page.context().close();
10937
- } catch {}
10938
- try {
10939
- await closeBrowser(handle.browser);
10940
- } catch {}
10941
- handles.delete(sessionId);
10942
- }
10943
- return closeSession(sessionId);
10944
- }
10945
- function listSessions2(filter) {
10946
- return listSessions(filter);
11122
+ // src/db/network-log.ts
11123
+ init_schema();
11124
+ import { randomUUID as randomUUID2 } from "crypto";
11125
+ function logRequest(data) {
11126
+ const db = getDatabase();
11127
+ const id = randomUUID2();
11128
+ db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
11129
+ response_headers, request_body, body_size, duration_ms, resource_type)
11130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
11131
+ return getNetworkRequest(id);
10947
11132
  }
10948
- function getSessionByName2(name) {
10949
- return getSessionByName(name);
11133
+ function getNetworkRequest(id) {
11134
+ const db = getDatabase();
11135
+ return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
10950
11136
  }
10951
- function renameSession2(id, name) {
10952
- return renameSession(id, name);
11137
+ function getNetworkLog(sessionId) {
11138
+ const db = getDatabase();
11139
+ return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
10953
11140
  }
10954
11141
 
10955
- // src/lib/actions.ts
10956
- async function click(page, selector, opts) {
10957
- try {
10958
- await page.click(selector, {
10959
- button: opts?.button ?? "left",
10960
- clickCount: opts?.clickCount ?? 1,
10961
- delay: opts?.delay,
10962
- timeout: opts?.timeout ?? 1e4
10963
- });
10964
- } catch (err) {
10965
- if (err instanceof Error && err.message.includes("not found")) {
10966
- throw new ElementNotFoundError(selector);
10967
- }
10968
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
10969
- }
11142
+ // src/lib/network.ts
11143
+ function enableNetworkLogging(page, sessionId) {
11144
+ const requestStart = new Map;
11145
+ const onRequest = (req) => {
11146
+ requestStart.set(req.url(), Date.now());
11147
+ };
11148
+ const onResponse = (res) => {
11149
+ const start = requestStart.get(res.url()) ?? Date.now();
11150
+ const duration = Date.now() - start;
11151
+ const req = res.request();
11152
+ try {
11153
+ logRequest({
11154
+ session_id: sessionId,
11155
+ method: req.method(),
11156
+ url: res.url(),
11157
+ status_code: res.status(),
11158
+ request_headers: JSON.stringify(req.headers()),
11159
+ response_headers: JSON.stringify(res.headers()),
11160
+ body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
11161
+ duration_ms: duration,
11162
+ resource_type: req.resourceType()
11163
+ });
11164
+ } catch {}
11165
+ };
11166
+ page.on("request", onRequest);
11167
+ page.on("response", onResponse);
11168
+ return () => {
11169
+ page.off("request", onRequest);
11170
+ page.off("response", onResponse);
11171
+ };
10970
11172
  }
10971
- async function type(page, selector, text, opts) {
10972
- try {
10973
- if (opts?.clear) {
10974
- await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
10975
- }
10976
- await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
10977
- } catch (err) {
10978
- if (err instanceof Error && err.message.includes("not found")) {
10979
- throw new ElementNotFoundError(selector);
11173
+ async function addInterceptRule(page, rule) {
11174
+ await page.route(rule.pattern, async (route) => {
11175
+ if (rule.action === "block") {
11176
+ await route.abort();
11177
+ } else if (rule.action === "modify" && rule.response) {
11178
+ await route.fulfill({
11179
+ status: rule.response.status,
11180
+ body: rule.response.body,
11181
+ headers: rule.response.headers
11182
+ });
11183
+ } else {
11184
+ await route.continue();
10980
11185
  }
10981
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
10982
- }
10983
- }
10984
- async function scroll(page, direction = "down", amount = 300) {
10985
- const x = direction === "left" ? -amount : direction === "right" ? amount : 0;
10986
- const y = direction === "up" ? -amount : direction === "down" ? amount : 0;
10987
- await page.evaluate(({ x: x2, y: y2 }) => window.scrollBy(x2, y2), { x, y });
11186
+ });
10988
11187
  }
10989
- async function hover(page, selector, timeout = 1e4) {
11188
+ function startHAR(page) {
11189
+ const entries = [];
11190
+ const requestStart = new Map;
11191
+ const onRequest = (req) => {
11192
+ requestStart.set(req.url() + req.method(), {
11193
+ time: Date.now(),
11194
+ method: req.method(),
11195
+ headers: req.headers(),
11196
+ postData: req.postData() ?? undefined
11197
+ });
11198
+ };
11199
+ const onResponse = async (res) => {
11200
+ const key = res.url() + res.request().method();
11201
+ const start = requestStart.get(key);
11202
+ if (!start)
11203
+ return;
11204
+ const duration = Date.now() - start.time;
11205
+ const entry = {
11206
+ startedDateTime: new Date(start.time).toISOString(),
11207
+ time: duration,
11208
+ request: {
11209
+ method: start.method,
11210
+ url: res.url(),
11211
+ headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
11212
+ postData: start.postData ? { text: start.postData } : undefined
11213
+ },
11214
+ response: {
11215
+ status: res.status(),
11216
+ statusText: res.statusText(),
11217
+ headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
11218
+ content: {
11219
+ size: parseInt(res.headers()["content-length"] ?? "0") || 0,
11220
+ mimeType: res.headers()["content-type"] ?? "application/octet-stream"
11221
+ }
11222
+ },
11223
+ timings: { send: 0, wait: duration, receive: 0 }
11224
+ };
11225
+ entries.push(entry);
11226
+ requestStart.delete(key);
11227
+ };
11228
+ page.on("request", onRequest);
11229
+ page.on("response", onResponse);
11230
+ return {
11231
+ entries,
11232
+ stop: () => {
11233
+ page.off("request", onRequest);
11234
+ page.off("response", onResponse);
11235
+ return {
11236
+ log: {
11237
+ version: "1.2",
11238
+ creator: { name: "@hasna/browser", version: "0.0.1" },
11239
+ entries
11240
+ }
11241
+ };
11242
+ }
11243
+ };
11244
+ }
11245
+
11246
+ // src/lib/console.ts
11247
+ init_console_log();
11248
+ function enableConsoleCapture(page, sessionId) {
11249
+ const onConsole = (msg) => {
11250
+ const levelMap = {
11251
+ log: "log",
11252
+ warn: "warn",
11253
+ error: "error",
11254
+ debug: "debug",
11255
+ info: "info",
11256
+ warning: "warn"
11257
+ };
11258
+ const level = levelMap[msg.type()] ?? "log";
11259
+ const location = msg.location();
11260
+ try {
11261
+ logConsoleMessage({
11262
+ session_id: sessionId,
11263
+ level,
11264
+ message: msg.text(),
11265
+ source: location.url || undefined,
11266
+ line_number: location.lineNumber || undefined
11267
+ });
11268
+ } catch {}
11269
+ };
11270
+ page.on("console", onConsole);
11271
+ return () => page.off("console", onConsole);
11272
+ }
11273
+
11274
+ // src/lib/stealth.ts
11275
+ var REALISTIC_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
11276
+ var STEALTH_SCRIPT = `
11277
+ // \u2500\u2500 1. Remove navigator.webdriver flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11278
+ Object.defineProperty(navigator, 'webdriver', {
11279
+ get: () => false,
11280
+ configurable: true,
11281
+ });
11282
+
11283
+ // \u2500\u2500 2. Override navigator.plugins to show typical Chrome plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11284
+ Object.defineProperty(navigator, 'plugins', {
11285
+ get: () => {
11286
+ const plugins = [
11287
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
11288
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
11289
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
11290
+ ];
11291
+ // Mimic PluginArray interface
11292
+ const pluginArray = Object.create(PluginArray.prototype);
11293
+ plugins.forEach((p, i) => {
11294
+ const plugin = Object.create(Plugin.prototype);
11295
+ Object.defineProperties(plugin, {
11296
+ name: { value: p.name, enumerable: true },
11297
+ filename: { value: p.filename, enumerable: true },
11298
+ description: { value: p.description, enumerable: true },
11299
+ length: { value: p.length, enumerable: true },
11300
+ });
11301
+ pluginArray[i] = plugin;
11302
+ });
11303
+ Object.defineProperty(pluginArray, 'length', { value: plugins.length });
11304
+ pluginArray.item = (i) => pluginArray[i] || null;
11305
+ pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
11306
+ pluginArray.refresh = () => {};
11307
+ return pluginArray;
11308
+ },
11309
+ configurable: true,
11310
+ });
11311
+
11312
+ // \u2500\u2500 3. Override navigator.languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11313
+ Object.defineProperty(navigator, 'languages', {
11314
+ get: () => ['en-US', 'en'],
11315
+ configurable: true,
11316
+ });
11317
+
11318
+ // \u2500\u2500 4. Override chrome.runtime to appear like real Chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11319
+ if (!window.chrome) {
11320
+ window.chrome = {};
11321
+ }
11322
+ if (!window.chrome.runtime) {
11323
+ window.chrome.runtime = {
11324
+ connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {} }; },
11325
+ sendMessage: function() {},
11326
+ onMessage: { addListener: function() {}, removeListener: function() {} },
11327
+ id: undefined,
11328
+ };
11329
+ }
11330
+ `;
11331
+ async function applyStealthPatches(page) {
11332
+ await page.context().addInitScript(STEALTH_SCRIPT);
11333
+ await page.context().setExtraHTTPHeaders({
11334
+ "User-Agent": REALISTIC_USER_AGENT
11335
+ });
11336
+ }
11337
+
11338
+ // src/lib/dialogs.ts
11339
+ var pendingDialogs = new Map;
11340
+ var AUTO_DISMISS_MS = 5000;
11341
+ function setupDialogHandler(page, sessionId) {
11342
+ const onDialog = (dialog) => {
11343
+ const info = {
11344
+ type: dialog.type(),
11345
+ message: dialog.message(),
11346
+ default_value: dialog.defaultValue(),
11347
+ timestamp: new Date().toISOString()
11348
+ };
11349
+ const autoTimer = setTimeout(() => {
11350
+ try {
11351
+ dialog.dismiss().catch(() => {});
11352
+ } catch {}
11353
+ const list = pendingDialogs.get(sessionId);
11354
+ if (list) {
11355
+ const idx = list.findIndex((p) => p.dialog === dialog);
11356
+ if (idx >= 0)
11357
+ list.splice(idx, 1);
11358
+ if (list.length === 0)
11359
+ pendingDialogs.delete(sessionId);
11360
+ }
11361
+ }, AUTO_DISMISS_MS);
11362
+ const pending = { dialog, info, autoTimer };
11363
+ if (!pendingDialogs.has(sessionId)) {
11364
+ pendingDialogs.set(sessionId, []);
11365
+ }
11366
+ pendingDialogs.get(sessionId).push(pending);
11367
+ };
11368
+ page.on("dialog", onDialog);
11369
+ return () => {
11370
+ page.off("dialog", onDialog);
11371
+ const list = pendingDialogs.get(sessionId);
11372
+ if (list) {
11373
+ for (const p of list)
11374
+ clearTimeout(p.autoTimer);
11375
+ pendingDialogs.delete(sessionId);
11376
+ }
11377
+ };
11378
+ }
11379
+ function getDialogs(sessionId) {
11380
+ const list = pendingDialogs.get(sessionId);
11381
+ if (!list)
11382
+ return [];
11383
+ return list.map((p) => p.info);
11384
+ }
11385
+ async function handleDialog(sessionId, action, promptText) {
11386
+ const list = pendingDialogs.get(sessionId);
11387
+ if (!list || list.length === 0) {
11388
+ return { handled: false };
11389
+ }
11390
+ const pending = list.shift();
11391
+ clearTimeout(pending.autoTimer);
11392
+ if (list.length === 0) {
11393
+ pendingDialogs.delete(sessionId);
11394
+ }
11395
+ try {
11396
+ if (action === "accept") {
11397
+ await pending.dialog.accept(promptText);
11398
+ } else {
11399
+ await pending.dialog.dismiss();
11400
+ }
11401
+ } catch {}
11402
+ return { handled: true, dialog: pending.info };
11403
+ }
11404
+
11405
+ // src/lib/session.ts
11406
+ var handles = new Map;
11407
+ async function createSession2(opts = {}) {
11408
+ const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
11409
+ const resolvedEngine = engine === "auto" ? "playwright" : engine;
11410
+ let browser;
11411
+ let page;
11412
+ if (resolvedEngine === "lightpanda") {
11413
+ browser = await connectLightpanda();
11414
+ const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
11415
+ page = await context.newPage();
11416
+ } else {
11417
+ browser = await launchPlaywright({
11418
+ headless: opts.headless ?? true,
11419
+ viewport: opts.viewport,
11420
+ userAgent: opts.userAgent
11421
+ });
11422
+ page = await getPage(browser, {
11423
+ viewport: opts.viewport,
11424
+ userAgent: opts.userAgent
11425
+ });
11426
+ }
11427
+ const session = createSession({
11428
+ engine: resolvedEngine,
11429
+ projectId: opts.projectId,
11430
+ agentId: opts.agentId,
11431
+ startUrl: opts.startUrl,
11432
+ name: opts.name ?? (opts.startUrl ? new URL(opts.startUrl).hostname : undefined)
11433
+ });
11434
+ if (opts.stealth) {
11435
+ try {
11436
+ await applyStealthPatches(page);
11437
+ } catch {}
11438
+ }
11439
+ const cleanups = [];
11440
+ if (opts.captureNetwork !== false) {
11441
+ try {
11442
+ cleanups.push(enableNetworkLogging(page, session.id));
11443
+ } catch {}
11444
+ }
11445
+ if (opts.captureConsole !== false) {
11446
+ try {
11447
+ cleanups.push(enableConsoleCapture(page, session.id));
11448
+ } catch {}
11449
+ }
11450
+ try {
11451
+ cleanups.push(setupDialogHandler(page, session.id));
11452
+ } catch {}
11453
+ handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
11454
+ if (opts.startUrl) {
11455
+ try {
11456
+ await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
11457
+ } catch {}
11458
+ }
11459
+ return { session, page };
11460
+ }
11461
+ function getSessionPage(sessionId) {
11462
+ const handle = handles.get(sessionId);
11463
+ if (!handle)
11464
+ throw new SessionNotFoundError(sessionId);
11465
+ try {
11466
+ handle.page.url();
11467
+ } catch {
11468
+ handles.delete(sessionId);
11469
+ throw new SessionNotFoundError(sessionId);
11470
+ }
11471
+ return handle.page;
11472
+ }
11473
+ function setSessionPage(sessionId, page) {
11474
+ const handle = handles.get(sessionId);
11475
+ if (!handle)
11476
+ throw new SessionNotFoundError(sessionId);
11477
+ handle.page = page;
11478
+ }
11479
+ async function closeSession2(sessionId) {
11480
+ const handle = handles.get(sessionId);
11481
+ if (handle) {
11482
+ for (const cleanup of handle.cleanups) {
11483
+ try {
11484
+ cleanup();
11485
+ } catch {}
11486
+ }
11487
+ try {
11488
+ await handle.page.context().close();
11489
+ } catch {}
11490
+ try {
11491
+ await closeBrowser(handle.browser);
11492
+ } catch {}
11493
+ handles.delete(sessionId);
11494
+ }
11495
+ return closeSession(sessionId);
11496
+ }
11497
+ function getSession2(sessionId) {
11498
+ return getSession(sessionId);
11499
+ }
11500
+ function listSessions2(filter) {
11501
+ return listSessions(filter);
11502
+ }
11503
+ function getSessionByName2(name) {
11504
+ return getSessionByName(name);
11505
+ }
11506
+ function renameSession2(id, name) {
11507
+ return renameSession(id, name);
11508
+ }
11509
+ function getTokenBudget(sessionId) {
11510
+ const handle = handles.get(sessionId);
11511
+ return handle ? handle.tokenBudget : null;
11512
+ }
11513
+
11514
+ // src/lib/actions.ts
11515
+ init_snapshot();
11516
+ async function click(page, selector, opts) {
11517
+ try {
11518
+ await page.click(selector, {
11519
+ button: opts?.button ?? "left",
11520
+ clickCount: opts?.clickCount ?? 1,
11521
+ delay: opts?.delay,
11522
+ timeout: opts?.timeout ?? 1e4
11523
+ });
11524
+ } catch (err) {
11525
+ if (err instanceof Error && err.message.includes("not found")) {
11526
+ throw new ElementNotFoundError(selector);
11527
+ }
11528
+ throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
11529
+ }
11530
+ }
11531
+ async function type(page, selector, text, opts) {
11532
+ try {
11533
+ if (opts?.clear) {
11534
+ await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
11535
+ }
11536
+ await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
11537
+ } catch (err) {
11538
+ if (err instanceof Error && err.message.includes("not found")) {
11539
+ throw new ElementNotFoundError(selector);
11540
+ }
11541
+ throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
11542
+ }
11543
+ }
11544
+ async function scroll(page, direction = "down", amount = 300) {
11545
+ const x = direction === "left" ? -amount : direction === "right" ? amount : 0;
11546
+ const y = direction === "up" ? -amount : direction === "down" ? amount : 0;
11547
+ await page.evaluate(({ x: x2, y: y2 }) => window.scrollBy(x2, y2), { x, y });
11548
+ }
11549
+ async function hover(page, selector, timeout = 1e4) {
10990
11550
  try {
10991
11551
  await page.hover(selector, { timeout });
10992
11552
  } catch (err) {
@@ -11173,6 +11733,63 @@ function stopWatch(watchId) {
11173
11733
  activeWatches.delete(watchId);
11174
11734
  }
11175
11735
  }
11736
+ async function clickRef(page, sessionId, ref, opts) {
11737
+ try {
11738
+ const locator = getRefLocator(page, sessionId, ref);
11739
+ await locator.click({ timeout: opts?.timeout ?? 1e4 });
11740
+ } catch (err) {
11741
+ if (err instanceof Error && err.message.includes("Ref "))
11742
+ throw new ElementNotFoundError(ref);
11743
+ if (err instanceof Error && err.message.includes("No snapshot"))
11744
+ throw new BrowserError(err.message, "NO_SNAPSHOT");
11745
+ throw new BrowserError(`clickRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CLICK_REF_FAILED");
11746
+ }
11747
+ }
11748
+ async function typeRef(page, sessionId, ref, text, opts) {
11749
+ try {
11750
+ const locator = getRefLocator(page, sessionId, ref);
11751
+ if (opts?.clear)
11752
+ await locator.fill("", { timeout: opts.timeout ?? 1e4 });
11753
+ await locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
11754
+ } catch (err) {
11755
+ if (err instanceof Error && err.message.includes("Ref "))
11756
+ throw new ElementNotFoundError(ref);
11757
+ throw new BrowserError(`typeRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "TYPE_REF_FAILED");
11758
+ }
11759
+ }
11760
+ async function selectRef(page, sessionId, ref, value, timeout = 1e4) {
11761
+ try {
11762
+ const locator = getRefLocator(page, sessionId, ref);
11763
+ return await locator.selectOption(value, { timeout });
11764
+ } catch (err) {
11765
+ if (err instanceof Error && err.message.includes("Ref "))
11766
+ throw new ElementNotFoundError(ref);
11767
+ throw new BrowserError(`selectRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "SELECT_REF_FAILED");
11768
+ }
11769
+ }
11770
+ async function checkRef(page, sessionId, ref, checked, timeout = 1e4) {
11771
+ try {
11772
+ const locator = getRefLocator(page, sessionId, ref);
11773
+ if (checked)
11774
+ await locator.check({ timeout });
11775
+ else
11776
+ await locator.uncheck({ timeout });
11777
+ } catch (err) {
11778
+ if (err instanceof Error && err.message.includes("Ref "))
11779
+ throw new ElementNotFoundError(ref);
11780
+ throw new BrowserError(`checkRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CHECK_REF_FAILED");
11781
+ }
11782
+ }
11783
+ async function hoverRef(page, sessionId, ref, timeout = 1e4) {
11784
+ try {
11785
+ const locator = getRefLocator(page, sessionId, ref);
11786
+ await locator.hover({ timeout });
11787
+ } catch (err) {
11788
+ if (err instanceof Error && err.message.includes("Ref "))
11789
+ throw new ElementNotFoundError(ref);
11790
+ throw new BrowserError(`hoverRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "HOVER_REF_FAILED");
11791
+ }
11792
+ }
11176
11793
 
11177
11794
  // src/lib/extractor.ts
11178
11795
  async function getText(page, selector) {
@@ -11239,24 +11856,6 @@ async function extractTable(page, selector) {
11239
11856
  return rows.map((row) => Array.from(row.querySelectorAll("th, td")).map((cell) => cell.textContent?.trim() ?? ""));
11240
11857
  }, selector);
11241
11858
  }
11242
- async function getAriaSnapshot(page) {
11243
- try {
11244
- return await page.ariaSnapshot?.() ?? page.evaluate(() => {
11245
- function walk(el, indent = 0) {
11246
- const role = el.getAttribute("role") ?? el.tagName.toLowerCase();
11247
- const label = el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 50) ?? "";
11248
- const line = " ".repeat(indent) + `[${role}] ${label}`;
11249
- const children = Array.from(el.children).map((c) => walk(c, indent + 1)).join(`
11250
- `);
11251
- return children ? `${line}
11252
- ${children}` : line;
11253
- }
11254
- return walk(document.body);
11255
- });
11256
- } catch {
11257
- return page.evaluate(() => document.body.innerText?.slice(0, 2000) ?? "");
11258
- }
11259
- }
11260
11859
  async function extract(page, opts = {}) {
11261
11860
  const result = {};
11262
11861
  const format = opts.format ?? "text";
@@ -11325,7 +11924,7 @@ import { homedir as homedir2 } from "os";
11325
11924
 
11326
11925
  // src/db/gallery.ts
11327
11926
  init_schema();
11328
- import { randomUUID as randomUUID2 } from "crypto";
11927
+ import { randomUUID as randomUUID4 } from "crypto";
11329
11928
  function deserialize(row) {
11330
11929
  return {
11331
11930
  id: row.id,
@@ -11349,7 +11948,7 @@ function deserialize(row) {
11349
11948
  }
11350
11949
  function createEntry(data) {
11351
11950
  const db = getDatabase();
11352
- const id = randomUUID2();
11951
+ const id = randomUUID4();
11353
11952
  db.prepare(`
11354
11953
  INSERT INTO gallery_entries
11355
11954
  (id, session_id, project_id, url, title, path, thumbnail_path, format,
@@ -11562,175 +12161,51 @@ async function takeScreenshot(page, opts) {
11562
12161
  session_id: opts?.sessionId,
11563
12162
  project_id: opts?.projectId,
11564
12163
  url,
11565
- title,
11566
- path: screenshotPath,
11567
- thumbnail_path: thumbnailPath,
11568
- format: ext,
11569
- width,
11570
- height,
11571
- original_size_bytes: originalSizeBytes,
11572
- compressed_size_bytes: compressedSizeBytes,
11573
- compression_ratio: compressionRatio,
11574
- tags: [],
11575
- is_favorite: false
11576
- });
11577
- result.gallery_id = entry.id;
11578
- } catch {}
11579
- }
11580
- return result;
11581
- } catch (err) {
11582
- if (err instanceof BrowserError)
11583
- throw err;
11584
- throw new BrowserError(`Screenshot failed: ${err instanceof Error ? err.message : String(err)}`, "SCREENSHOT_FAILED");
11585
- }
11586
- }
11587
- async function generatePDF(page, opts) {
11588
- try {
11589
- const base = join2(getDataDir2(), "pdfs");
11590
- const date = new Date().toISOString().split("T")[0];
11591
- const dir = opts?.projectId ? join2(base, opts.projectId, date) : join2(base, date);
11592
- mkdirSync2(dir, { recursive: true });
11593
- const timestamp = Date.now();
11594
- const pdfPath = opts?.path ?? join2(dir, `${timestamp}.pdf`);
11595
- const buffer = await page.pdf({
11596
- path: pdfPath,
11597
- format: opts?.format ?? "A4",
11598
- landscape: opts?.landscape ?? false,
11599
- margin: opts?.margin,
11600
- printBackground: opts?.printBackground ?? true
11601
- });
11602
- return {
11603
- path: pdfPath,
11604
- base64: Buffer.from(buffer).toString("base64"),
11605
- size_bytes: buffer.length
11606
- };
11607
- } catch (err) {
11608
- throw new BrowserError(`PDF generation failed: ${err instanceof Error ? err.message : String(err)}`, "PDF_FAILED");
11609
- }
11610
- }
11611
-
11612
- // src/db/network-log.ts
11613
- init_schema();
11614
- import { randomUUID as randomUUID3 } from "crypto";
11615
- function logRequest(data) {
11616
- const db = getDatabase();
11617
- const id = randomUUID3();
11618
- db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
11619
- response_headers, request_body, body_size, duration_ms, resource_type)
11620
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
11621
- return getNetworkRequest(id);
11622
- }
11623
- function getNetworkRequest(id) {
11624
- const db = getDatabase();
11625
- return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
11626
- }
11627
- function getNetworkLog(sessionId) {
11628
- const db = getDatabase();
11629
- return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
11630
- }
11631
-
11632
- // src/lib/network.ts
11633
- function enableNetworkLogging(page, sessionId) {
11634
- const requestStart = new Map;
11635
- const onRequest = (req) => {
11636
- requestStart.set(req.url(), Date.now());
11637
- };
11638
- const onResponse = (res) => {
11639
- const start = requestStart.get(res.url()) ?? Date.now();
11640
- const duration = Date.now() - start;
11641
- const req = res.request();
11642
- try {
11643
- logRequest({
11644
- session_id: sessionId,
11645
- method: req.method(),
11646
- url: res.url(),
11647
- status_code: res.status(),
11648
- request_headers: JSON.stringify(req.headers()),
11649
- response_headers: JSON.stringify(res.headers()),
11650
- body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
11651
- duration_ms: duration,
11652
- resource_type: req.resourceType()
11653
- });
11654
- } catch {}
11655
- };
11656
- page.on("request", onRequest);
11657
- page.on("response", onResponse);
11658
- return () => {
11659
- page.off("request", onRequest);
11660
- page.off("response", onResponse);
11661
- };
11662
- }
11663
- async function addInterceptRule(page, rule) {
11664
- await page.route(rule.pattern, async (route) => {
11665
- if (rule.action === "block") {
11666
- await route.abort();
11667
- } else if (rule.action === "modify" && rule.response) {
11668
- await route.fulfill({
11669
- status: rule.response.status,
11670
- body: rule.response.body,
11671
- headers: rule.response.headers
11672
- });
11673
- } else {
11674
- await route.continue();
11675
- }
11676
- });
11677
- }
11678
- function startHAR(page) {
11679
- const entries = [];
11680
- const requestStart = new Map;
11681
- const onRequest = (req) => {
11682
- requestStart.set(req.url() + req.method(), {
11683
- time: Date.now(),
11684
- method: req.method(),
11685
- headers: req.headers(),
11686
- postData: req.postData() ?? undefined
11687
- });
11688
- };
11689
- const onResponse = async (res) => {
11690
- const key = res.url() + res.request().method();
11691
- const start = requestStart.get(key);
11692
- if (!start)
11693
- return;
11694
- const duration = Date.now() - start.time;
11695
- const entry = {
11696
- startedDateTime: new Date(start.time).toISOString(),
11697
- time: duration,
11698
- request: {
11699
- method: start.method,
11700
- url: res.url(),
11701
- headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
11702
- postData: start.postData ? { text: start.postData } : undefined
11703
- },
11704
- response: {
11705
- status: res.status(),
11706
- statusText: res.statusText(),
11707
- headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
11708
- content: {
11709
- size: parseInt(res.headers()["content-length"] ?? "0") || 0,
11710
- mimeType: res.headers()["content-type"] ?? "application/octet-stream"
11711
- }
11712
- },
11713
- timings: { send: 0, wait: duration, receive: 0 }
11714
- };
11715
- entries.push(entry);
11716
- requestStart.delete(key);
11717
- };
11718
- page.on("request", onRequest);
11719
- page.on("response", onResponse);
11720
- return {
11721
- entries,
11722
- stop: () => {
11723
- page.off("request", onRequest);
11724
- page.off("response", onResponse);
11725
- return {
11726
- log: {
11727
- version: "1.2",
11728
- creator: { name: "@hasna/browser", version: "0.0.1" },
11729
- entries
11730
- }
11731
- };
12164
+ title,
12165
+ path: screenshotPath,
12166
+ thumbnail_path: thumbnailPath,
12167
+ format: ext,
12168
+ width,
12169
+ height,
12170
+ original_size_bytes: originalSizeBytes,
12171
+ compressed_size_bytes: compressedSizeBytes,
12172
+ compression_ratio: compressionRatio,
12173
+ tags: [],
12174
+ is_favorite: false
12175
+ });
12176
+ result.gallery_id = entry.id;
12177
+ } catch {}
11732
12178
  }
11733
- };
12179
+ return result;
12180
+ } catch (err) {
12181
+ if (err instanceof BrowserError)
12182
+ throw err;
12183
+ throw new BrowserError(`Screenshot failed: ${err instanceof Error ? err.message : String(err)}`, "SCREENSHOT_FAILED");
12184
+ }
12185
+ }
12186
+ async function generatePDF(page, opts) {
12187
+ try {
12188
+ const base = join2(getDataDir2(), "pdfs");
12189
+ const date = new Date().toISOString().split("T")[0];
12190
+ const dir = opts?.projectId ? join2(base, opts.projectId, date) : join2(base, date);
12191
+ mkdirSync2(dir, { recursive: true });
12192
+ const timestamp = Date.now();
12193
+ const pdfPath = opts?.path ?? join2(dir, `${timestamp}.pdf`);
12194
+ const buffer = await page.pdf({
12195
+ path: pdfPath,
12196
+ format: opts?.format ?? "A4",
12197
+ landscape: opts?.landscape ?? false,
12198
+ margin: opts?.margin,
12199
+ printBackground: opts?.printBackground ?? true
12200
+ });
12201
+ return {
12202
+ path: pdfPath,
12203
+ base64: Buffer.from(buffer).toString("base64"),
12204
+ size_bytes: buffer.length
12205
+ };
12206
+ } catch (err) {
12207
+ throw new BrowserError(`PDF generation failed: ${err instanceof Error ? err.message : String(err)}`, "PDF_FAILED");
12208
+ }
11734
12209
  }
11735
12210
 
11736
12211
  // src/engines/cdp.ts
@@ -11878,34 +12353,6 @@ async function getPerformanceMetrics(page) {
11878
12353
  };
11879
12354
  }
11880
12355
 
11881
- // src/lib/console.ts
11882
- init_console_log();
11883
- function enableConsoleCapture(page, sessionId) {
11884
- const onConsole = (msg) => {
11885
- const levelMap = {
11886
- log: "log",
11887
- warn: "warn",
11888
- error: "error",
11889
- debug: "debug",
11890
- info: "info",
11891
- warning: "warn"
11892
- };
11893
- const level = levelMap[msg.type()] ?? "log";
11894
- const location = msg.location();
11895
- try {
11896
- logConsoleMessage({
11897
- session_id: sessionId,
11898
- level,
11899
- message: msg.text(),
11900
- source: location.url || undefined,
11901
- line_number: location.lineNumber || undefined
11902
- });
11903
- } catch {}
11904
- };
11905
- page.on("console", onConsole);
11906
- return () => page.off("console", onConsole);
11907
- }
11908
-
11909
12356
  // src/lib/storage.ts
11910
12357
  async function getCookies(page, filter) {
11911
12358
  const cookies = await page.context().cookies();
@@ -12455,6 +12902,9 @@ async function diffImages(path1, path2) {
12455
12902
  };
12456
12903
  }
12457
12904
 
12905
+ // src/mcp/index.ts
12906
+ init_snapshot();
12907
+
12458
12908
  // src/lib/files-integration.ts
12459
12909
  import { join as join5 } from "path";
12460
12910
  import { mkdirSync as mkdirSync5, copyFileSync as copyFileSync2 } from "fs";
@@ -12482,6 +12932,222 @@ async function persistFile(localPath, opts) {
12482
12932
  };
12483
12933
  }
12484
12934
 
12935
+ // src/lib/tabs.ts
12936
+ async function newTab(page, url) {
12937
+ const context = page.context();
12938
+ const newPage = await context.newPage();
12939
+ if (url) {
12940
+ await newPage.goto(url, { waitUntil: "domcontentloaded" });
12941
+ }
12942
+ const pages = context.pages();
12943
+ const index = pages.indexOf(newPage);
12944
+ return {
12945
+ index,
12946
+ url: newPage.url(),
12947
+ title: await newPage.title(),
12948
+ is_active: true
12949
+ };
12950
+ }
12951
+ async function listTabs(page) {
12952
+ const context = page.context();
12953
+ const pages = context.pages();
12954
+ const activePage = page;
12955
+ const tabs = [];
12956
+ for (let i = 0;i < pages.length; i++) {
12957
+ let url = "";
12958
+ let title = "";
12959
+ try {
12960
+ url = pages[i].url();
12961
+ title = await pages[i].title();
12962
+ } catch {}
12963
+ tabs.push({
12964
+ index: i,
12965
+ url,
12966
+ title,
12967
+ is_active: pages[i] === activePage
12968
+ });
12969
+ }
12970
+ return tabs;
12971
+ }
12972
+ async function switchTab(page, index) {
12973
+ const context = page.context();
12974
+ const pages = context.pages();
12975
+ if (index < 0 || index >= pages.length) {
12976
+ throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
12977
+ }
12978
+ const targetPage = pages[index];
12979
+ await targetPage.bringToFront();
12980
+ return {
12981
+ page: targetPage,
12982
+ tab: {
12983
+ index,
12984
+ url: targetPage.url(),
12985
+ title: await targetPage.title(),
12986
+ is_active: true
12987
+ }
12988
+ };
12989
+ }
12990
+ async function closeTab(page, index) {
12991
+ const context = page.context();
12992
+ const pages = context.pages();
12993
+ if (index < 0 || index >= pages.length) {
12994
+ throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
12995
+ }
12996
+ if (pages.length <= 1) {
12997
+ throw new Error("Cannot close the last tab");
12998
+ }
12999
+ const targetPage = pages[index];
13000
+ const isActivePage = targetPage === page;
13001
+ await targetPage.close();
13002
+ const remainingPages = context.pages();
13003
+ const activeIndex = isActivePage ? Math.min(index, remainingPages.length - 1) : remainingPages.indexOf(page);
13004
+ const activePage = remainingPages[activeIndex >= 0 ? activeIndex : 0];
13005
+ return {
13006
+ closed_index: index,
13007
+ active_tab: {
13008
+ index: activeIndex >= 0 ? activeIndex : 0,
13009
+ url: activePage.url(),
13010
+ title: await activePage.title(),
13011
+ is_active: true
13012
+ }
13013
+ };
13014
+ }
13015
+
13016
+ // src/lib/profiles.ts
13017
+ import { mkdirSync as mkdirSync6, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
13018
+ import { join as join6 } from "path";
13019
+ import { homedir as homedir6 } from "os";
13020
+ function getProfilesDir() {
13021
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
13022
+ const dir = join6(dataDir, "profiles");
13023
+ mkdirSync6(dir, { recursive: true });
13024
+ return dir;
13025
+ }
13026
+ function getProfileDir(name) {
13027
+ return join6(getProfilesDir(), name);
13028
+ }
13029
+ async function saveProfile(page, name) {
13030
+ const dir = getProfileDir(name);
13031
+ mkdirSync6(dir, { recursive: true });
13032
+ const cookies = await page.context().cookies();
13033
+ writeFileSync2(join6(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
13034
+ let localStorage2 = {};
13035
+ try {
13036
+ localStorage2 = await page.evaluate(() => {
13037
+ const result = {};
13038
+ for (let i = 0;i < window.localStorage.length; i++) {
13039
+ const key = window.localStorage.key(i);
13040
+ result[key] = window.localStorage.getItem(key);
13041
+ }
13042
+ return result;
13043
+ });
13044
+ } catch {}
13045
+ writeFileSync2(join6(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
13046
+ const savedAt = new Date().toISOString();
13047
+ const url = page.url();
13048
+ const meta = { saved_at: savedAt, url };
13049
+ writeFileSync2(join6(dir, "meta.json"), JSON.stringify(meta, null, 2));
13050
+ return {
13051
+ name,
13052
+ saved_at: savedAt,
13053
+ url,
13054
+ cookie_count: cookies.length,
13055
+ storage_key_count: Object.keys(localStorage2).length
13056
+ };
13057
+ }
13058
+ function loadProfile(name) {
13059
+ const dir = getProfileDir(name);
13060
+ if (!existsSync3(dir)) {
13061
+ throw new Error(`Profile not found: ${name}`);
13062
+ }
13063
+ const cookiesPath = join6(dir, "cookies.json");
13064
+ const storagePath = join6(dir, "storage.json");
13065
+ const metaPath2 = join6(dir, "meta.json");
13066
+ const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
13067
+ const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
13068
+ let savedAt = new Date().toISOString();
13069
+ let url;
13070
+ if (existsSync3(metaPath2)) {
13071
+ const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
13072
+ savedAt = meta.saved_at ?? savedAt;
13073
+ url = meta.url;
13074
+ }
13075
+ return { cookies, localStorage: localStorage2, saved_at: savedAt, url };
13076
+ }
13077
+ async function applyProfile(page, profileData) {
13078
+ if (profileData.cookies.length > 0) {
13079
+ await page.context().addCookies(profileData.cookies);
13080
+ }
13081
+ const storageKeys = Object.keys(profileData.localStorage);
13082
+ if (storageKeys.length > 0) {
13083
+ try {
13084
+ await page.evaluate((storage) => {
13085
+ for (const [key, value] of Object.entries(storage)) {
13086
+ window.localStorage.setItem(key, value);
13087
+ }
13088
+ }, profileData.localStorage);
13089
+ } catch {}
13090
+ }
13091
+ return {
13092
+ cookies_applied: profileData.cookies.length,
13093
+ storage_keys_applied: storageKeys.length
13094
+ };
13095
+ }
13096
+ function listProfiles() {
13097
+ const dir = getProfilesDir();
13098
+ if (!existsSync3(dir))
13099
+ return [];
13100
+ const entries = readdirSync2(dir, { withFileTypes: true });
13101
+ const profiles = [];
13102
+ for (const entry of entries) {
13103
+ if (!entry.isDirectory())
13104
+ continue;
13105
+ const name = entry.name;
13106
+ const profileDir = join6(dir, name);
13107
+ let savedAt = "";
13108
+ let url;
13109
+ let cookieCount = 0;
13110
+ let storageKeyCount = 0;
13111
+ try {
13112
+ const metaPath2 = join6(profileDir, "meta.json");
13113
+ if (existsSync3(metaPath2)) {
13114
+ const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
13115
+ savedAt = meta.saved_at ?? "";
13116
+ url = meta.url;
13117
+ }
13118
+ const cookiesPath = join6(profileDir, "cookies.json");
13119
+ if (existsSync3(cookiesPath)) {
13120
+ const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
13121
+ cookieCount = Array.isArray(cookies) ? cookies.length : 0;
13122
+ }
13123
+ const storagePath = join6(profileDir, "storage.json");
13124
+ if (existsSync3(storagePath)) {
13125
+ const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
13126
+ storageKeyCount = Object.keys(storage).length;
13127
+ }
13128
+ } catch {}
13129
+ profiles.push({
13130
+ name,
13131
+ saved_at: savedAt,
13132
+ url,
13133
+ cookie_count: cookieCount,
13134
+ storage_key_count: storageKeyCount
13135
+ });
13136
+ }
13137
+ return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
13138
+ }
13139
+ function deleteProfile(name) {
13140
+ const dir = getProfileDir(name);
13141
+ if (!existsSync3(dir))
13142
+ return false;
13143
+ try {
13144
+ rmSync(dir, { recursive: true, force: true });
13145
+ return true;
13146
+ } catch {
13147
+ return false;
13148
+ }
13149
+ }
13150
+
12485
13151
  // src/mcp/index.ts
12486
13152
  var networkLogCleanup = new Map;
12487
13153
  var consoleCaptureCleanup = new Map;
@@ -12509,8 +13175,9 @@ server.tool("browser_session_create", "Create a new browser session with the spe
12509
13175
  start_url: exports_external.string().optional(),
12510
13176
  headless: exports_external.boolean().optional().default(true),
12511
13177
  viewport_width: exports_external.number().optional().default(1280),
12512
- viewport_height: exports_external.number().optional().default(720)
12513
- }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height }) => {
13178
+ viewport_height: exports_external.number().optional().default(720),
13179
+ stealth: exports_external.boolean().optional().default(false)
13180
+ }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth }) => {
12514
13181
  try {
12515
13182
  const { session } = await createSession2({
12516
13183
  engine,
@@ -12519,7 +13186,8 @@ server.tool("browser_session_create", "Create a new browser session with the spe
12519
13186
  agentId: agent_id,
12520
13187
  startUrl: start_url,
12521
13188
  headless,
12522
- viewport: { width: viewport_width, height: viewport_height }
13189
+ viewport: { width: viewport_width, height: viewport_height },
13190
+ stealth
12523
13191
  });
12524
13192
  return json({ session });
12525
13193
  } catch (e) {
@@ -12546,11 +13214,28 @@ server.tool("browser_session_close", "Close a browser session", { session_id: ex
12546
13214
  return err(e);
12547
13215
  }
12548
13216
  });
12549
- server.tool("browser_navigate", "Navigate to a URL in the session", { session_id: exports_external.string(), url: exports_external.string(), timeout: exports_external.number().optional().default(30000) }, async ({ session_id, url, timeout }) => {
13217
+ server.tool("browser_navigate", "Navigate to a URL. Returns title + thumbnail + accessibility snapshot preview with refs.", { session_id: exports_external.string(), url: exports_external.string(), timeout: exports_external.number().optional().default(30000), auto_snapshot: exports_external.boolean().optional().default(true), auto_thumbnail: exports_external.boolean().optional().default(true) }, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
12550
13218
  try {
12551
13219
  const page = getSessionPage(session_id);
12552
13220
  await navigate(page, url, timeout);
12553
- return json({ url, title: await getTitle(page), current_url: await getUrl(page) });
13221
+ const title = await getTitle(page);
13222
+ const current_url = await getUrl(page);
13223
+ const result = { url, title, current_url };
13224
+ if (auto_thumbnail) {
13225
+ try {
13226
+ const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
13227
+ result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
13228
+ } catch {}
13229
+ }
13230
+ if (auto_snapshot) {
13231
+ try {
13232
+ const snap = await takeSnapshot(page, session_id);
13233
+ result.snapshot_preview = snap.tree.slice(0, 3000);
13234
+ result.interactive_count = snap.interactive_count;
13235
+ result.has_errors = getConsoleLog(session_id, "error").length > 0;
13236
+ } catch {}
13237
+ }
13238
+ return json(result);
12554
13239
  } catch (e) {
12555
13240
  return err(e);
12556
13241
  }
@@ -12582,29 +13267,47 @@ server.tool("browser_reload", "Reload the current page", { session_id: exports_e
12582
13267
  return err(e);
12583
13268
  }
12584
13269
  });
12585
- server.tool("browser_click", "Click an element matching the selector", { session_id: exports_external.string(), selector: exports_external.string(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, button, timeout }) => {
13270
+ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
12586
13271
  try {
12587
13272
  const page = getSessionPage(session_id);
13273
+ if (ref) {
13274
+ await clickRef(page, session_id, ref, { timeout });
13275
+ return json({ clicked: ref, method: "ref" });
13276
+ }
13277
+ if (!selector)
13278
+ return err(new Error("Either ref or selector is required"));
12588
13279
  await click(page, selector, { button, timeout });
12589
- return json({ clicked: selector });
13280
+ return json({ clicked: selector, method: "selector" });
12590
13281
  } catch (e) {
12591
13282
  return err(e);
12592
13283
  }
12593
13284
  });
12594
- server.tool("browser_type", "Type text into an element", { session_id: exports_external.string(), selector: exports_external.string(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, text, clear, delay }) => {
13285
+ server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
12595
13286
  try {
12596
13287
  const page = getSessionPage(session_id);
13288
+ if (ref) {
13289
+ await typeRef(page, session_id, ref, text, { clear, delay });
13290
+ return json({ typed: text, ref, method: "ref" });
13291
+ }
13292
+ if (!selector)
13293
+ return err(new Error("Either ref or selector is required"));
12597
13294
  await type(page, selector, text, { clear, delay });
12598
- return json({ typed: text, selector });
13295
+ return json({ typed: text, selector, method: "selector" });
12599
13296
  } catch (e) {
12600
13297
  return err(e);
12601
13298
  }
12602
13299
  });
12603
- server.tool("browser_hover", "Hover over an element", { session_id: exports_external.string(), selector: exports_external.string() }, async ({ session_id, selector }) => {
13300
+ server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
12604
13301
  try {
12605
13302
  const page = getSessionPage(session_id);
13303
+ if (ref) {
13304
+ await hoverRef(page, session_id, ref);
13305
+ return json({ hovered: ref, method: "ref" });
13306
+ }
13307
+ if (!selector)
13308
+ return err(new Error("Either ref or selector is required"));
12606
13309
  await hover(page, selector);
12607
- return json({ hovered: selector });
13310
+ return json({ hovered: selector, method: "selector" });
12608
13311
  } catch (e) {
12609
13312
  return err(e);
12610
13313
  }
@@ -12618,20 +13321,32 @@ server.tool("browser_scroll", "Scroll the page", { session_id: exports_external.
12618
13321
  return err(e);
12619
13322
  }
12620
13323
  });
12621
- server.tool("browser_select", "Select a dropdown option", { session_id: exports_external.string(), selector: exports_external.string(), value: exports_external.string() }, async ({ session_id, selector, value }) => {
13324
+ server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
12622
13325
  try {
12623
13326
  const page = getSessionPage(session_id);
13327
+ if (ref) {
13328
+ const selected2 = await selectRef(page, session_id, ref, value);
13329
+ return json({ selected: selected2, method: "ref" });
13330
+ }
13331
+ if (!selector)
13332
+ return err(new Error("Either ref or selector is required"));
12624
13333
  const selected = await selectOption(page, selector, value);
12625
- return json({ selected });
13334
+ return json({ selected, method: "selector" });
12626
13335
  } catch (e) {
12627
13336
  return err(e);
12628
13337
  }
12629
13338
  });
12630
- server.tool("browser_check", "Check or uncheck a checkbox", { session_id: exports_external.string(), selector: exports_external.string(), checked: exports_external.boolean() }, async ({ session_id, selector, checked }) => {
13339
+ server.tool("browser_check", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
12631
13340
  try {
12632
13341
  const page = getSessionPage(session_id);
13342
+ if (ref) {
13343
+ await checkRef(page, session_id, ref, checked);
13344
+ return json({ checked, ref, method: "ref" });
13345
+ }
13346
+ if (!selector)
13347
+ return err(new Error("Either ref or selector is required"));
12633
13348
  await checkBox(page, selector, checked);
12634
- return json({ checked, selector });
13349
+ return json({ checked, selector, method: "selector" });
12635
13350
  } catch (e) {
12636
13351
  return err(e);
12637
13352
  }
@@ -12712,15 +13427,17 @@ server.tool("browser_find", "Find elements matching a selector and return their
12712
13427
  return err(e);
12713
13428
  }
12714
13429
  });
12715
- server.tool("browser_snapshot", "Get an accessibility (ARIA) snapshot of the page", { session_id: exports_external.string() }, async ({ session_id }) => {
13430
+ server.tool("browser_snapshot", "Get a structured accessibility snapshot with element refs (@e0, @e1...). Use refs in browser_click, browser_type, etc.", { session_id: exports_external.string() }, async ({ session_id }) => {
12716
13431
  try {
12717
13432
  const page = getSessionPage(session_id);
12718
- return json({ snapshot: await getAriaSnapshot(page) });
13433
+ const result = await takeSnapshot(page, session_id);
13434
+ setLastSnapshot(session_id, result);
13435
+ return json({ snapshot: result.tree, refs: result.refs, interactive_count: result.interactive_count });
12719
13436
  } catch (e) {
12720
13437
  return err(e);
12721
13438
  }
12722
13439
  });
12723
- server.tool("browser_screenshot", "Take a screenshot of the page or an element", {
13440
+ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
12724
13441
  session_id: exports_external.string(),
12725
13442
  selector: exports_external.string().optional(),
12726
13443
  full_page: exports_external.boolean().optional().default(false),
@@ -12728,17 +13445,37 @@ server.tool("browser_screenshot", "Take a screenshot of the page or an element",
12728
13445
  quality: exports_external.number().optional(),
12729
13446
  max_width: exports_external.number().optional().default(1280),
12730
13447
  compress: exports_external.boolean().optional().default(true),
12731
- thumbnail: exports_external.boolean().optional().default(true)
12732
- }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail }) => {
13448
+ thumbnail: exports_external.boolean().optional().default(true),
13449
+ annotate: exports_external.boolean().optional().default(false)
13450
+ }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate }) => {
12733
13451
  try {
12734
13452
  const page = getSessionPage(session_id);
13453
+ if (annotate && !selector && !full_page) {
13454
+ const { annotateScreenshot: annotateScreenshot2 } = await Promise.resolve().then(() => (init_annotate(), exports_annotate));
13455
+ const annotated = await annotateScreenshot2(page, session_id);
13456
+ const base64 = annotated.buffer.toString("base64");
13457
+ return json({
13458
+ base64: base64.length > 50000 ? undefined : base64,
13459
+ base64_truncated: base64.length > 50000,
13460
+ size_bytes: annotated.buffer.length,
13461
+ annotations: annotated.annotations,
13462
+ label_to_ref: annotated.labelToRef,
13463
+ annotation_count: annotated.annotations.length
13464
+ });
13465
+ }
12735
13466
  const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
13467
+ result.url = page.url();
12736
13468
  try {
12737
13469
  const buf = Buffer.from(result.base64, "base64");
12738
13470
  const filename = result.path.split("/").pop() ?? `screenshot.${format ?? "webp"}`;
12739
13471
  const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "screenshot", sourceUrl: page.url() });
12740
13472
  result.download_id = dl.id;
12741
13473
  } catch {}
13474
+ if (result.base64.length > 50000) {
13475
+ result.base64_truncated = true;
13476
+ result.full_image_path = result.path;
13477
+ result.base64 = result.thumbnail_base64 ?? "";
13478
+ }
12742
13479
  return json(result);
12743
13480
  } catch (e) {
12744
13481
  return err(e);
@@ -13033,6 +13770,37 @@ server.tool("browser_project_list", "List all registered projects", {}, async ()
13033
13770
  return err(e);
13034
13771
  }
13035
13772
  });
13773
+ server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
13774
+ try {
13775
+ const page = getSessionPage(session_id);
13776
+ await scroll(page, direction, amount);
13777
+ await new Promise((r) => setTimeout(r, wait_ms));
13778
+ const result = await takeScreenshot(page, { maxWidth: 1280, track: true });
13779
+ result.url = page.url();
13780
+ if (result.base64.length > 50000) {
13781
+ result.base64_truncated = true;
13782
+ result.full_image_path = result.path;
13783
+ result.base64 = result.thumbnail_base64 ?? "";
13784
+ }
13785
+ return json({ scrolled: { direction, amount }, screenshot: result });
13786
+ } catch (e) {
13787
+ return err(e);
13788
+ }
13789
+ });
13790
+ server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
13791
+ try {
13792
+ const page = getSessionPage(session_id);
13793
+ const start = Date.now();
13794
+ if (url_pattern) {
13795
+ await page.waitForURL(url_pattern, { timeout });
13796
+ } else {
13797
+ await page.waitForLoadState("domcontentloaded", { timeout });
13798
+ }
13799
+ return json({ url: page.url(), title: await getTitle(page), elapsed_ms: Date.now() - start });
13800
+ } catch (e) {
13801
+ return err(e);
13802
+ }
13803
+ });
13036
13804
  server.tool("browser_session_get_by_name", "Get a session by its name", { name: exports_external.string() }, async ({ name }) => {
13037
13805
  try {
13038
13806
  const session = getSessionByName2(name);
@@ -13146,6 +13914,40 @@ server.tool("browser_watch_stop", "Stop a DOM change watcher", { watch_id: expor
13146
13914
  return err(e);
13147
13915
  }
13148
13916
  });
13917
+ server.tool("browser_page_check", "One-call page summary: page info + console errors + performance metrics + thumbnail + accessibility snapshot preview. Replaces 4-5 separate tool calls.", { session_id: exports_external.string() }, async ({ session_id }) => {
13918
+ try {
13919
+ const page = getSessionPage(session_id);
13920
+ const info = await getPageInfo(page);
13921
+ const errors2 = getConsoleLog(session_id, "error");
13922
+ info.has_console_errors = errors2.length > 0;
13923
+ let perf = {};
13924
+ try {
13925
+ perf = await getPerformanceMetrics(page);
13926
+ } catch {}
13927
+ let thumbnail_base64 = "";
13928
+ try {
13929
+ const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
13930
+ thumbnail_base64 = ss.base64;
13931
+ } catch {}
13932
+ let snapshot_preview = "";
13933
+ let interactive_count = 0;
13934
+ try {
13935
+ const snap = await takeSnapshot(page, session_id);
13936
+ snapshot_preview = snap.tree.slice(0, 2000);
13937
+ interactive_count = snap.interactive_count;
13938
+ } catch {}
13939
+ return json({
13940
+ ...info,
13941
+ error_count: errors2.length,
13942
+ performance: perf,
13943
+ thumbnail_base64: thumbnail_base64.length > 50000 ? "" : thumbnail_base64,
13944
+ snapshot_preview,
13945
+ interactive_count
13946
+ });
13947
+ } catch (e) {
13948
+ return err(e);
13949
+ }
13950
+ });
13149
13951
  server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
13150
13952
  project_id: exports_external.string().optional(),
13151
13953
  session_id: exports_external.string().optional(),
@@ -13294,5 +14096,296 @@ server.tool("browser_persist_file", "Persist a file permanently via open-files S
13294
14096
  return err(e);
13295
14097
  }
13296
14098
  });
14099
+ server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string() }, async ({ session_id }) => {
14100
+ try {
14101
+ const page = getSessionPage(session_id);
14102
+ const before = getLastSnapshot(session_id);
14103
+ const after = await takeSnapshot(page, session_id);
14104
+ setLastSnapshot(session_id, after);
14105
+ if (!before) {
14106
+ return json({
14107
+ message: "No previous snapshot \u2014 returning current snapshot only.",
14108
+ snapshot: after.tree,
14109
+ refs: after.refs,
14110
+ interactive_count: after.interactive_count
14111
+ });
14112
+ }
14113
+ const diff = diffSnapshots(before, after);
14114
+ return json({
14115
+ diff,
14116
+ added_count: diff.added.length,
14117
+ removed_count: diff.removed.length,
14118
+ modified_count: diff.modified.length,
14119
+ url_changed: diff.url_changed,
14120
+ title_changed: diff.title_changed,
14121
+ current_interactive_count: after.interactive_count
14122
+ });
14123
+ } catch (e) {
14124
+ return err(e);
14125
+ }
14126
+ });
14127
+ server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string() }, async ({ session_id }) => {
14128
+ try {
14129
+ const session = getSession2(session_id);
14130
+ const networkLog = getNetworkLog(session_id);
14131
+ const consoleLog = getConsoleLog(session_id);
14132
+ const galleryEntries = listEntries({ sessionId: session_id, limit: 1000 });
14133
+ let totalChars = 0;
14134
+ for (const req of networkLog) {
14135
+ totalChars += (req.url?.length ?? 0) + (req.request_headers?.length ?? 0) + (req.response_headers?.length ?? 0) + (req.request_body?.length ?? 0);
14136
+ }
14137
+ for (const msg of consoleLog) {
14138
+ totalChars += (msg.message?.length ?? 0) + (msg.source?.length ?? 0);
14139
+ }
14140
+ for (const entry of galleryEntries) {
14141
+ totalChars += (entry.url?.length ?? 0) + (entry.title?.length ?? 0) + (entry.notes?.length ?? 0) + (entry.tags?.join(",").length ?? 0);
14142
+ }
14143
+ const estimatedTokens = Math.ceil(totalChars / 4);
14144
+ const tokenBudget = getTokenBudget(session_id);
14145
+ return json({
14146
+ session,
14147
+ network_request_count: networkLog.length,
14148
+ console_message_count: consoleLog.length,
14149
+ gallery_entry_count: galleryEntries.length,
14150
+ estimated_tokens_used: estimatedTokens,
14151
+ token_budget: tokenBudget,
14152
+ data_size_chars: totalChars
14153
+ });
14154
+ } catch (e) {
14155
+ return err(e);
14156
+ }
14157
+ });
14158
+ server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
14159
+ try {
14160
+ const page = getSessionPage(session_id);
14161
+ const tab = await newTab(page, url);
14162
+ return json(tab);
14163
+ } catch (e) {
14164
+ return err(e);
14165
+ }
14166
+ });
14167
+ server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string() }, async ({ session_id }) => {
14168
+ try {
14169
+ const page = getSessionPage(session_id);
14170
+ const tabs = await listTabs(page);
14171
+ return json({ tabs, count: tabs.length });
14172
+ } catch (e) {
14173
+ return err(e);
14174
+ }
14175
+ });
14176
+ server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
14177
+ try {
14178
+ const page = getSessionPage(session_id);
14179
+ const result = await switchTab(page, tab_id);
14180
+ setSessionPage(session_id, result.page);
14181
+ return json(result.tab);
14182
+ } catch (e) {
14183
+ return err(e);
14184
+ }
14185
+ });
14186
+ server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
14187
+ try {
14188
+ const page = getSessionPage(session_id);
14189
+ const context = page.context();
14190
+ const result = await closeTab(page, tab_id);
14191
+ const remainingPages = context.pages();
14192
+ const newActivePage = remainingPages[result.active_tab.index];
14193
+ if (newActivePage) {
14194
+ setSessionPage(session_id, newActivePage);
14195
+ }
14196
+ return json(result);
14197
+ } catch (e) {
14198
+ return err(e);
14199
+ }
14200
+ });
14201
+ server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
14202
+ try {
14203
+ const result = await handleDialog(session_id, action, prompt_text);
14204
+ if (!result.handled)
14205
+ return err(new Error("No pending dialogs for this session"));
14206
+ return json(result);
14207
+ } catch (e) {
14208
+ return err(e);
14209
+ }
14210
+ });
14211
+ server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
14212
+ try {
14213
+ const dialogs = getDialogs(session_id);
14214
+ return json({ dialogs, count: dialogs.length });
14215
+ } catch (e) {
14216
+ return err(e);
14217
+ }
14218
+ });
14219
+ server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
14220
+ try {
14221
+ const page = getSessionPage(session_id);
14222
+ const info = await saveProfile(page, name);
14223
+ return json(info);
14224
+ } catch (e) {
14225
+ return err(e);
14226
+ }
14227
+ });
14228
+ server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
14229
+ try {
14230
+ const profileData = loadProfile(name);
14231
+ if (session_id) {
14232
+ const page = getSessionPage(session_id);
14233
+ const applied = await applyProfile(page, profileData);
14234
+ return json({ ...applied, profile: name });
14235
+ }
14236
+ return json({ profile: name, cookies: profileData.cookies.length, storage_keys: Object.keys(profileData.localStorage).length });
14237
+ } catch (e) {
14238
+ return err(e);
14239
+ }
14240
+ });
14241
+ server.tool("browser_profile_list", "List all saved browser profiles", {}, async () => {
14242
+ try {
14243
+ return json({ profiles: listProfiles() });
14244
+ } catch (e) {
14245
+ return err(e);
14246
+ }
14247
+ });
14248
+ server.tool("browser_profile_delete", "Delete a saved browser profile", { name: exports_external.string() }, async ({ name }) => {
14249
+ try {
14250
+ const deleted = deleteProfile(name);
14251
+ if (!deleted)
14252
+ return err(new Error(`Profile not found: ${name}`));
14253
+ return json({ deleted: name });
14254
+ } catch (e) {
14255
+ return err(e);
14256
+ }
14257
+ });
14258
+ server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
14259
+ try {
14260
+ const groups = {
14261
+ Navigation: [
14262
+ { tool: "browser_navigate", description: "Navigate to a URL" },
14263
+ { tool: "browser_back", description: "Navigate back in history" },
14264
+ { tool: "browser_forward", description: "Navigate forward in history" },
14265
+ { tool: "browser_reload", description: "Reload the current page" },
14266
+ { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" }
14267
+ ],
14268
+ Interaction: [
14269
+ { tool: "browser_click", description: "Click element by ref or selector" },
14270
+ { tool: "browser_click_text", description: "Click element by visible text" },
14271
+ { tool: "browser_type", description: "Type text into an element" },
14272
+ { tool: "browser_hover", description: "Hover over an element" },
14273
+ { tool: "browser_scroll", description: "Scroll the page" },
14274
+ { tool: "browser_select", description: "Select a dropdown option" },
14275
+ { tool: "browser_check", description: "Check/uncheck a checkbox" },
14276
+ { tool: "browser_upload", description: "Upload a file to an input" },
14277
+ { tool: "browser_press_key", description: "Press a keyboard key" },
14278
+ { tool: "browser_wait", description: "Wait for a selector to appear" },
14279
+ { tool: "browser_wait_for_text", description: "Wait for text to appear" },
14280
+ { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
14281
+ { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
14282
+ ],
14283
+ Extraction: [
14284
+ { tool: "browser_get_text", description: "Get text content from page/selector" },
14285
+ { tool: "browser_get_html", description: "Get HTML content from page/selector" },
14286
+ { tool: "browser_get_links", description: "Get all links on the page" },
14287
+ { tool: "browser_get_page_info", description: "Full page summary in one call" },
14288
+ { tool: "browser_extract", description: "Extract content in various formats" },
14289
+ { tool: "browser_find", description: "Find elements by selector" },
14290
+ { tool: "browser_element_exists", description: "Check if a selector exists" },
14291
+ { tool: "browser_snapshot", description: "Get accessibility snapshot with refs" },
14292
+ { tool: "browser_evaluate", description: "Execute JavaScript in page context" }
14293
+ ],
14294
+ Capture: [
14295
+ { tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP)" },
14296
+ { tool: "browser_pdf", description: "Generate a PDF of the page" },
14297
+ { tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" }
14298
+ ],
14299
+ Storage: [
14300
+ { tool: "browser_cookies_get", description: "Get cookies" },
14301
+ { tool: "browser_cookies_set", description: "Set a cookie" },
14302
+ { tool: "browser_cookies_clear", description: "Clear cookies" },
14303
+ { tool: "browser_storage_get", description: "Get localStorage/sessionStorage" },
14304
+ { tool: "browser_storage_set", description: "Set localStorage/sessionStorage" },
14305
+ { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
14306
+ { tool: "browser_profile_load", description: "Load and apply a saved profile" },
14307
+ { tool: "browser_profile_list", description: "List saved profiles" },
14308
+ { tool: "browser_profile_delete", description: "Delete a saved profile" }
14309
+ ],
14310
+ Network: [
14311
+ { tool: "browser_network_log", description: "Get captured network requests" },
14312
+ { tool: "browser_network_intercept", description: "Add a network interception rule" },
14313
+ { tool: "browser_har_start", description: "Start HAR capture" },
14314
+ { tool: "browser_har_stop", description: "Stop HAR capture and get data" }
14315
+ ],
14316
+ Performance: [
14317
+ { tool: "browser_performance", description: "Get performance metrics" }
14318
+ ],
14319
+ Console: [
14320
+ { tool: "browser_console_log", description: "Get console messages" },
14321
+ { tool: "browser_has_errors", description: "Check for console errors" },
14322
+ { tool: "browser_clear_errors", description: "Clear console error log" },
14323
+ { tool: "browser_get_dialogs", description: "Get pending dialogs" }
14324
+ ],
14325
+ Recording: [
14326
+ { tool: "browser_record_start", description: "Start recording actions" },
14327
+ { tool: "browser_record_step", description: "Add a step to recording" },
14328
+ { tool: "browser_record_stop", description: "Stop and save recording" },
14329
+ { tool: "browser_record_replay", description: "Replay a recorded sequence" },
14330
+ { tool: "browser_recordings_list", description: "List all recordings" }
14331
+ ],
14332
+ Crawl: [
14333
+ { tool: "browser_crawl", description: "Crawl a URL recursively" }
14334
+ ],
14335
+ Agent: [
14336
+ { tool: "browser_register_agent", description: "Register an agent" },
14337
+ { tool: "browser_heartbeat", description: "Send agent heartbeat" },
14338
+ { tool: "browser_agent_list", description: "List registered agents" }
14339
+ ],
14340
+ Project: [
14341
+ { tool: "browser_project_create", description: "Create or ensure a project" },
14342
+ { tool: "browser_project_list", description: "List all projects" }
14343
+ ],
14344
+ Gallery: [
14345
+ { tool: "browser_gallery_list", description: "List screenshot gallery entries" },
14346
+ { tool: "browser_gallery_get", description: "Get a gallery entry by id" },
14347
+ { tool: "browser_gallery_tag", description: "Add a tag to gallery entry" },
14348
+ { tool: "browser_gallery_untag", description: "Remove a tag from gallery entry" },
14349
+ { tool: "browser_gallery_favorite", description: "Mark/unmark as favorite" },
14350
+ { tool: "browser_gallery_delete", description: "Delete a gallery entry" },
14351
+ { tool: "browser_gallery_search", description: "Search gallery entries" },
14352
+ { tool: "browser_gallery_stats", description: "Get gallery statistics" },
14353
+ { tool: "browser_gallery_diff", description: "Pixel-diff two screenshots" }
14354
+ ],
14355
+ Downloads: [
14356
+ { tool: "browser_downloads_list", description: "List downloaded files" },
14357
+ { tool: "browser_downloads_get", description: "Get a download by id" },
14358
+ { tool: "browser_downloads_delete", description: "Delete a download" },
14359
+ { tool: "browser_downloads_clean", description: "Clean old downloads" },
14360
+ { tool: "browser_downloads_export", description: "Copy download to a path" },
14361
+ { tool: "browser_persist_file", description: "Persist file permanently" }
14362
+ ],
14363
+ Session: [
14364
+ { tool: "browser_session_create", description: "Create a new browser session" },
14365
+ { tool: "browser_session_list", description: "List all sessions" },
14366
+ { tool: "browser_session_close", description: "Close a session" },
14367
+ { tool: "browser_session_get_by_name", description: "Get session by name" },
14368
+ { tool: "browser_session_rename", description: "Rename a session" },
14369
+ { tool: "browser_session_stats", description: "Get session stats and token usage" },
14370
+ { tool: "browser_tab_new", description: "Open a new tab" },
14371
+ { tool: "browser_tab_list", description: "List all open tabs" },
14372
+ { tool: "browser_tab_switch", description: "Switch to a tab by index" },
14373
+ { tool: "browser_tab_close", description: "Close a tab by index" }
14374
+ ],
14375
+ Meta: [
14376
+ { tool: "browser_page_check", description: "One-call page summary with diagnostics" },
14377
+ { tool: "browser_help", description: "Show this help (all tools)" },
14378
+ { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
14379
+ { tool: "browser_watch_start", description: "Watch page for DOM changes" },
14380
+ { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
14381
+ { tool: "browser_watch_stop", description: "Stop DOM watcher" }
14382
+ ]
14383
+ };
14384
+ const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
14385
+ return json({ groups, total_tools: totalTools });
14386
+ } catch (e) {
14387
+ return err(e);
14388
+ }
14389
+ });
13297
14390
  var transport = new StdioServerTransport;
13298
14391
  await server.connect(transport);