@hasna/browser 0.0.4 → 0.0.5
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/cli/index.js +669 -106
- package/dist/engines/bun-webview.d.ts +147 -0
- package/dist/engines/bun-webview.d.ts.map +1 -0
- package/dist/engines/bun-webview.test.d.ts +2 -0
- package/dist/engines/bun-webview.test.d.ts.map +1 -0
- package/dist/engines/selector.d.ts +2 -2
- package/dist/engines/selector.d.ts.map +1 -1
- package/dist/index.js +804 -278
- package/dist/lib/extractor.d.ts.map +1 -1
- package/dist/lib/screenshot.d.ts.map +1 -1
- package/dist/lib/session.d.ts +3 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/snapshot.d.ts +1 -0
- package/dist/lib/snapshot.d.ts.map +1 -1
- package/dist/mcp/index.js +659 -100
- package/dist/server/index.js +565 -86
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -270,6 +270,7 @@ var init_console_log = __esm(() => {
|
|
|
270
270
|
var exports_snapshot = {};
|
|
271
271
|
__export(exports_snapshot, {
|
|
272
272
|
takeSnapshot: () => takeSnapshot,
|
|
273
|
+
takeBunSnapshot: () => takeBunSnapshot,
|
|
273
274
|
setLastSnapshot: () => setLastSnapshot,
|
|
274
275
|
hasRefs: () => hasRefs,
|
|
275
276
|
getSessionRefs: () => getSessionRefs,
|
|
@@ -290,6 +291,10 @@ function clearLastSnapshot(sessionId) {
|
|
|
290
291
|
lastSnapshots.delete(sessionId);
|
|
291
292
|
}
|
|
292
293
|
async function takeSnapshot(page, sessionId) {
|
|
294
|
+
const isBunView = typeof page.getNativeView === "function" || typeof page.bunView !== "undefined";
|
|
295
|
+
if (isBunView) {
|
|
296
|
+
return takeBunSnapshot(page, sessionId);
|
|
297
|
+
}
|
|
293
298
|
let ariaTree;
|
|
294
299
|
try {
|
|
295
300
|
ariaTree = await page.locator("body").ariaSnapshot();
|
|
@@ -444,6 +449,71 @@ function diffSnapshots(before, after) {
|
|
|
444
449
|
const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
|
|
445
450
|
return { added, removed, modified, url_changed, title_changed };
|
|
446
451
|
}
|
|
452
|
+
async function takeBunSnapshot(page, sessionId) {
|
|
453
|
+
const refs = {};
|
|
454
|
+
const refMap = new Map;
|
|
455
|
+
let refCounter = 0;
|
|
456
|
+
const lines = [];
|
|
457
|
+
try {
|
|
458
|
+
const elements = await page.evaluate(`
|
|
459
|
+
(() => {
|
|
460
|
+
const SELECTOR = 'a[href], button, input:not([type=hidden]), select, textarea, [role=button], [role=link], [role=checkbox], [role=combobox], [role=menuitem], [role=tab], [role=option]';
|
|
461
|
+
const els = Array.from(document.querySelectorAll(SELECTOR));
|
|
462
|
+
return els.slice(0, 100).map(el => {
|
|
463
|
+
const tag = el.tagName.toLowerCase();
|
|
464
|
+
const inputType = el.getAttribute('type') ?? '';
|
|
465
|
+
let role = el.getAttribute('role') || (['a'].includes(tag) ? 'link' : ['button'].includes(tag) ? 'button' : ['input'].includes(tag) ? (inputType === 'checkbox' ? 'checkbox' : inputType === 'radio' ? 'radio' : 'textbox') : ['select'].includes(tag) ? 'combobox' : ['textarea'].includes(tag) ? 'textbox' : tag);
|
|
466
|
+
const name = (el.getAttribute('aria-label') || el.textContent?.trim() || el.getAttribute('placeholder') || el.getAttribute('title') || el.getAttribute('value') || el.id || '').slice(0, 80);
|
|
467
|
+
const enabled = !el.disabled && !el.getAttribute('disabled');
|
|
468
|
+
const style = window.getComputedStyle(el);
|
|
469
|
+
const visible = style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
|
|
470
|
+
const checked = el.type === 'checkbox' || el.type === 'radio' ? el.checked : undefined;
|
|
471
|
+
const value = ['input', 'select', 'textarea'].includes(tag) && el.type !== 'checkbox' && el.type !== 'radio' ? el.value : undefined;
|
|
472
|
+
const selector = el.id ? '#' + el.id : (el.getAttribute('aria-label') ? '[aria-label="' + el.getAttribute('aria-label') + '"]' : tag);
|
|
473
|
+
return { role, name, enabled, visible, checked, value, selector };
|
|
474
|
+
}).filter(e => e.visible && e.name);
|
|
475
|
+
})()
|
|
476
|
+
`);
|
|
477
|
+
const pageTitle = await page.evaluate("document.title");
|
|
478
|
+
const pageUrl = typeof page.url === "function" ? page.url() : "";
|
|
479
|
+
lines.push(`# ${pageTitle || "Page"} (${pageUrl})`);
|
|
480
|
+
for (const el of elements) {
|
|
481
|
+
if (!el.name)
|
|
482
|
+
continue;
|
|
483
|
+
const ref = `@e${refCounter}`;
|
|
484
|
+
refCounter++;
|
|
485
|
+
refs[ref] = {
|
|
486
|
+
role: el.role,
|
|
487
|
+
name: el.name,
|
|
488
|
+
visible: el.visible,
|
|
489
|
+
enabled: el.enabled,
|
|
490
|
+
value: el.value,
|
|
491
|
+
checked: el.checked
|
|
492
|
+
};
|
|
493
|
+
refMap.set(ref, { role: el.role, name: el.name, locatorSelector: el.selector });
|
|
494
|
+
const extras = [];
|
|
495
|
+
if (el.checked !== undefined)
|
|
496
|
+
extras.push(`checked=${el.checked}`);
|
|
497
|
+
if (!el.enabled)
|
|
498
|
+
extras.push("disabled");
|
|
499
|
+
if (el.value && el.value !== el.name)
|
|
500
|
+
extras.push(`value="${el.value.slice(0, 30)}"`);
|
|
501
|
+
const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
|
|
502
|
+
lines.push(`${el.role} "${el.name}" [${ref}]${extrasStr}`);
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
lines.push(`# (snapshot error: ${err instanceof Error ? err.message : String(err)})`);
|
|
506
|
+
}
|
|
507
|
+
if (sessionId) {
|
|
508
|
+
sessionRefMaps.set(sessionId, refMap);
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
tree: lines.join(`
|
|
512
|
+
`),
|
|
513
|
+
refs,
|
|
514
|
+
interactive_count: refCounter
|
|
515
|
+
};
|
|
516
|
+
}
|
|
447
517
|
var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
|
|
448
518
|
var init_snapshot = __esm(() => {
|
|
449
519
|
lastSnapshots = new Map;
|
|
@@ -10904,7 +10974,7 @@ var coerce = {
|
|
|
10904
10974
|
var NEVER = INVALID;
|
|
10905
10975
|
// src/mcp/index.ts
|
|
10906
10976
|
import { readFileSync as readFileSync3 } from "fs";
|
|
10907
|
-
import { join as
|
|
10977
|
+
import { join as join8 } from "path";
|
|
10908
10978
|
|
|
10909
10979
|
// src/types/index.ts
|
|
10910
10980
|
class BrowserError extends Error {
|
|
@@ -11150,14 +11220,413 @@ async function connectLightpanda(port) {
|
|
|
11150
11220
|
}
|
|
11151
11221
|
}
|
|
11152
11222
|
|
|
11223
|
+
// src/engines/bun-webview.ts
|
|
11224
|
+
import { join as join2 } from "path";
|
|
11225
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
11226
|
+
import { homedir as homedir2 } from "os";
|
|
11227
|
+
function isBunWebViewAvailable() {
|
|
11228
|
+
return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.WebView !== "undefined";
|
|
11229
|
+
}
|
|
11230
|
+
function getProfileDir(profileName) {
|
|
11231
|
+
const base = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
|
|
11232
|
+
const dir = join2(base, "profiles", profileName);
|
|
11233
|
+
mkdirSync2(dir, { recursive: true });
|
|
11234
|
+
return dir;
|
|
11235
|
+
}
|
|
11236
|
+
|
|
11237
|
+
class BunWebViewSession {
|
|
11238
|
+
view;
|
|
11239
|
+
_sessionId;
|
|
11240
|
+
_eventListeners = new Map;
|
|
11241
|
+
constructor(opts = {}) {
|
|
11242
|
+
if (!isBunWebViewAvailable()) {
|
|
11243
|
+
throw new Error("Bun.WebView is not available. Install Bun canary: bun upgrade --canary");
|
|
11244
|
+
}
|
|
11245
|
+
const BunWebView = globalThis.Bun.WebView;
|
|
11246
|
+
const constructorOpts = {
|
|
11247
|
+
width: opts.width ?? 1280,
|
|
11248
|
+
height: opts.height ?? 720
|
|
11249
|
+
};
|
|
11250
|
+
if (opts.profile) {
|
|
11251
|
+
constructorOpts.dataStore = { directory: getProfileDir(opts.profile) };
|
|
11252
|
+
} else {
|
|
11253
|
+
constructorOpts.dataStore = "ephemeral";
|
|
11254
|
+
}
|
|
11255
|
+
if (opts.onConsole) {
|
|
11256
|
+
constructorOpts.console = opts.onConsole;
|
|
11257
|
+
}
|
|
11258
|
+
this.view = new BunWebView(constructorOpts);
|
|
11259
|
+
this.view.onNavigated = (url) => {
|
|
11260
|
+
this._emit("navigated", url);
|
|
11261
|
+
};
|
|
11262
|
+
this.view.onNavigationFailed = (error) => {
|
|
11263
|
+
this._emit("navigationfailed", error);
|
|
11264
|
+
};
|
|
11265
|
+
}
|
|
11266
|
+
async goto(url, opts) {
|
|
11267
|
+
await this.view.navigate(url);
|
|
11268
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
11269
|
+
}
|
|
11270
|
+
async goBack() {
|
|
11271
|
+
await this.view.goBack();
|
|
11272
|
+
}
|
|
11273
|
+
async goForward() {
|
|
11274
|
+
await this.view.goForward();
|
|
11275
|
+
}
|
|
11276
|
+
async reload() {
|
|
11277
|
+
await this.view.reload();
|
|
11278
|
+
}
|
|
11279
|
+
async evaluate(fnOrExpr, ...args) {
|
|
11280
|
+
let expr;
|
|
11281
|
+
if (typeof fnOrExpr === "function") {
|
|
11282
|
+
const serializedArgs = args.map((a) => JSON.stringify(a)).join(", ");
|
|
11283
|
+
expr = `(${fnOrExpr.toString()})(${serializedArgs})`;
|
|
11284
|
+
} else {
|
|
11285
|
+
expr = fnOrExpr;
|
|
11286
|
+
}
|
|
11287
|
+
return this.view.evaluate(expr);
|
|
11288
|
+
}
|
|
11289
|
+
async screenshot(opts) {
|
|
11290
|
+
const uint8 = await this.view.screenshot();
|
|
11291
|
+
return Buffer.from(uint8);
|
|
11292
|
+
}
|
|
11293
|
+
async click(selector, opts) {
|
|
11294
|
+
await this.view.click(selector, opts ? { button: opts.button } : undefined);
|
|
11295
|
+
}
|
|
11296
|
+
async type(selector, text, opts) {
|
|
11297
|
+
try {
|
|
11298
|
+
await this.view.click(selector);
|
|
11299
|
+
} catch {}
|
|
11300
|
+
await this.view.type(text);
|
|
11301
|
+
}
|
|
11302
|
+
async fill(selector, value) {
|
|
11303
|
+
await this.view.evaluate(`
|
|
11304
|
+
(() => {
|
|
11305
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11306
|
+
if (el) { el.value = ''; el.dispatchEvent(new Event('input')); }
|
|
11307
|
+
})()
|
|
11308
|
+
`);
|
|
11309
|
+
await this.type(selector, value);
|
|
11310
|
+
}
|
|
11311
|
+
async press(key, opts) {
|
|
11312
|
+
await this.view.press(key, opts);
|
|
11313
|
+
}
|
|
11314
|
+
async scroll(direction, amount) {
|
|
11315
|
+
const dx = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
11316
|
+
const dy = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
11317
|
+
await this.view.scroll(dx, dy);
|
|
11318
|
+
}
|
|
11319
|
+
async scrollIntoView(selector) {
|
|
11320
|
+
await this.view.scrollTo(selector);
|
|
11321
|
+
}
|
|
11322
|
+
async hover(selector) {
|
|
11323
|
+
try {
|
|
11324
|
+
await this.view.scrollTo(selector);
|
|
11325
|
+
} catch {}
|
|
11326
|
+
}
|
|
11327
|
+
async resize(width, height) {
|
|
11328
|
+
await this.view.resize(width, height);
|
|
11329
|
+
}
|
|
11330
|
+
async $(selector) {
|
|
11331
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
11332
|
+
if (!exists)
|
|
11333
|
+
return null;
|
|
11334
|
+
return {
|
|
11335
|
+
textContent: async () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`)
|
|
11336
|
+
};
|
|
11337
|
+
}
|
|
11338
|
+
async $$(selector) {
|
|
11339
|
+
const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
|
|
11340
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
11341
|
+
textContent: async () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${i}]?.textContent ?? null`)
|
|
11342
|
+
}));
|
|
11343
|
+
}
|
|
11344
|
+
async inputValue(selector) {
|
|
11345
|
+
return this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.value ?? ''`);
|
|
11346
|
+
}
|
|
11347
|
+
async isChecked(selector) {
|
|
11348
|
+
return this.view.evaluate(`!!(document.querySelector(${JSON.stringify(selector)})?.checked)`);
|
|
11349
|
+
}
|
|
11350
|
+
async isVisible(selector) {
|
|
11351
|
+
return this.view.evaluate(`
|
|
11352
|
+
(() => {
|
|
11353
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11354
|
+
if (!el) return false;
|
|
11355
|
+
const style = window.getComputedStyle(el);
|
|
11356
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
|
|
11357
|
+
})()
|
|
11358
|
+
`);
|
|
11359
|
+
}
|
|
11360
|
+
async isEnabled(selector) {
|
|
11361
|
+
return this.view.evaluate(`!(document.querySelector(${JSON.stringify(selector)})?.disabled)`);
|
|
11362
|
+
}
|
|
11363
|
+
async selectOption(selector, value) {
|
|
11364
|
+
await this.view.evaluate(`
|
|
11365
|
+
(() => {
|
|
11366
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11367
|
+
if (el) {
|
|
11368
|
+
el.value = ${JSON.stringify(value)};
|
|
11369
|
+
el.dispatchEvent(new Event('change'));
|
|
11370
|
+
}
|
|
11371
|
+
})()
|
|
11372
|
+
`);
|
|
11373
|
+
return [value];
|
|
11374
|
+
}
|
|
11375
|
+
async check(selector) {
|
|
11376
|
+
await this.view.evaluate(`
|
|
11377
|
+
(() => {
|
|
11378
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11379
|
+
if (el && !el.checked) { el.checked = true; el.dispatchEvent(new Event('change')); }
|
|
11380
|
+
})()
|
|
11381
|
+
`);
|
|
11382
|
+
}
|
|
11383
|
+
async uncheck(selector) {
|
|
11384
|
+
await this.view.evaluate(`
|
|
11385
|
+
(() => {
|
|
11386
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11387
|
+
if (el && el.checked) { el.checked = false; el.dispatchEvent(new Event('change')); }
|
|
11388
|
+
})()
|
|
11389
|
+
`);
|
|
11390
|
+
}
|
|
11391
|
+
async setInputFiles(selector, files) {
|
|
11392
|
+
throw new Error("File upload not supported in Bun.WebView engine. Use engine: 'playwright' instead.");
|
|
11393
|
+
}
|
|
11394
|
+
getByRole(role, opts) {
|
|
11395
|
+
const name = opts?.name?.toString() ?? "";
|
|
11396
|
+
const selector = name ? `[role="${role}"][aria-label*="${name}"], ${role}[aria-label*="${name}"]` : `[role="${role}"], ${role}`;
|
|
11397
|
+
return {
|
|
11398
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
11399
|
+
fill: (value) => this.fill(selector, value),
|
|
11400
|
+
check: () => this.check(selector),
|
|
11401
|
+
uncheck: () => this.uncheck(selector),
|
|
11402
|
+
isVisible: () => this.isVisible(selector),
|
|
11403
|
+
textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
|
|
11404
|
+
inputValue: () => this.inputValue(selector),
|
|
11405
|
+
first: () => ({
|
|
11406
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
11407
|
+
fill: (value) => this.fill(selector, value),
|
|
11408
|
+
textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
|
|
11409
|
+
isVisible: () => this.isVisible(selector),
|
|
11410
|
+
hover: () => this.hover(selector),
|
|
11411
|
+
boundingBox: async () => null,
|
|
11412
|
+
scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
|
|
11413
|
+
evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
|
|
11414
|
+
waitFor: (opts2) => {
|
|
11415
|
+
return new Promise((resolve, reject) => {
|
|
11416
|
+
const timeout = opts2?.timeout ?? 1e4;
|
|
11417
|
+
const start = Date.now();
|
|
11418
|
+
const check = async () => {
|
|
11419
|
+
const visible = await this.isVisible(selector);
|
|
11420
|
+
if (visible)
|
|
11421
|
+
return resolve();
|
|
11422
|
+
if (Date.now() - start > timeout)
|
|
11423
|
+
return reject(new Error(`Timeout waiting for ${selector}`));
|
|
11424
|
+
setTimeout(check, 100);
|
|
11425
|
+
};
|
|
11426
|
+
check();
|
|
11427
|
+
});
|
|
11428
|
+
}
|
|
11429
|
+
}),
|
|
11430
|
+
count: async () => {
|
|
11431
|
+
const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
|
|
11432
|
+
return count;
|
|
11433
|
+
},
|
|
11434
|
+
nth: (n) => ({
|
|
11435
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
11436
|
+
textContent: () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${n}]?.textContent ?? null`),
|
|
11437
|
+
isVisible: () => this.isVisible(selector)
|
|
11438
|
+
})
|
|
11439
|
+
};
|
|
11440
|
+
}
|
|
11441
|
+
getByText(text, opts) {
|
|
11442
|
+
const selector = opts?.exact ? `*:is(button, a, span, div, p, h1, h2, h3, h4, label)` : "*";
|
|
11443
|
+
return {
|
|
11444
|
+
first: () => ({
|
|
11445
|
+
click: async (clickOpts) => {
|
|
11446
|
+
await this.view.evaluate(`
|
|
11447
|
+
(() => {
|
|
11448
|
+
const text = ${JSON.stringify(text)};
|
|
11449
|
+
const all = document.querySelectorAll('*');
|
|
11450
|
+
for (const el of all) {
|
|
11451
|
+
if (el.children.length === 0 && el.textContent?.trim() === text) {
|
|
11452
|
+
el.click(); return;
|
|
11453
|
+
}
|
|
11454
|
+
}
|
|
11455
|
+
for (const el of all) {
|
|
11456
|
+
if (el.textContent?.includes(text)) { el.click(); return; }
|
|
11457
|
+
}
|
|
11458
|
+
})()
|
|
11459
|
+
`);
|
|
11460
|
+
},
|
|
11461
|
+
waitFor: (waitOpts) => {
|
|
11462
|
+
const timeout = waitOpts?.timeout ?? 1e4;
|
|
11463
|
+
return new Promise((resolve, reject) => {
|
|
11464
|
+
const start = Date.now();
|
|
11465
|
+
const check = async () => {
|
|
11466
|
+
const found = await this.view.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)})`);
|
|
11467
|
+
if (found)
|
|
11468
|
+
return resolve();
|
|
11469
|
+
if (Date.now() - start > timeout)
|
|
11470
|
+
return reject(new Error(`Timeout: text "${text}" not found`));
|
|
11471
|
+
setTimeout(check, 100);
|
|
11472
|
+
};
|
|
11473
|
+
check();
|
|
11474
|
+
});
|
|
11475
|
+
}
|
|
11476
|
+
})
|
|
11477
|
+
};
|
|
11478
|
+
}
|
|
11479
|
+
locator(selector) {
|
|
11480
|
+
return {
|
|
11481
|
+
click: (opts) => this.click(selector, opts),
|
|
11482
|
+
fill: (value) => this.fill(selector, value),
|
|
11483
|
+
scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
|
|
11484
|
+
first: () => this.getByRole("*").first(),
|
|
11485
|
+
evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
|
|
11486
|
+
waitFor: (opts) => {
|
|
11487
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
11488
|
+
return new Promise((resolve, reject) => {
|
|
11489
|
+
const start = Date.now();
|
|
11490
|
+
const check = async () => {
|
|
11491
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
11492
|
+
if (exists)
|
|
11493
|
+
return resolve();
|
|
11494
|
+
if (Date.now() - start > timeout)
|
|
11495
|
+
return reject(new Error(`Timeout: ${selector}`));
|
|
11496
|
+
setTimeout(check, 100);
|
|
11497
|
+
};
|
|
11498
|
+
check();
|
|
11499
|
+
});
|
|
11500
|
+
}
|
|
11501
|
+
};
|
|
11502
|
+
}
|
|
11503
|
+
url() {
|
|
11504
|
+
return this.view.url;
|
|
11505
|
+
}
|
|
11506
|
+
async title() {
|
|
11507
|
+
return this.view.title || await this.evaluate("document.title");
|
|
11508
|
+
}
|
|
11509
|
+
viewportSize() {
|
|
11510
|
+
return { width: 1280, height: 720 };
|
|
11511
|
+
}
|
|
11512
|
+
async waitForLoadState(state, opts) {
|
|
11513
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
11514
|
+
}
|
|
11515
|
+
async waitForURL(pattern, opts) {
|
|
11516
|
+
const timeout = opts?.timeout ?? 30000;
|
|
11517
|
+
const start = Date.now();
|
|
11518
|
+
while (Date.now() - start < timeout) {
|
|
11519
|
+
const url = this.view.url;
|
|
11520
|
+
const matches = pattern instanceof RegExp ? pattern.test(url) : url.includes(pattern);
|
|
11521
|
+
if (matches)
|
|
11522
|
+
return;
|
|
11523
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
11524
|
+
}
|
|
11525
|
+
throw new Error(`Timeout waiting for URL to match ${pattern}`);
|
|
11526
|
+
}
|
|
11527
|
+
async waitForSelector(selector, opts) {
|
|
11528
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
11529
|
+
const start = Date.now();
|
|
11530
|
+
while (Date.now() - start < timeout) {
|
|
11531
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
11532
|
+
if (exists)
|
|
11533
|
+
return;
|
|
11534
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
11535
|
+
}
|
|
11536
|
+
throw new Error(`Timeout waiting for ${selector}`);
|
|
11537
|
+
}
|
|
11538
|
+
async setContent(html) {
|
|
11539
|
+
await this.view.navigate(`data:text/html,${encodeURIComponent(html)}`);
|
|
11540
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
11541
|
+
}
|
|
11542
|
+
async content() {
|
|
11543
|
+
return this.view.evaluate("document.documentElement.outerHTML");
|
|
11544
|
+
}
|
|
11545
|
+
async addInitScript(script) {
|
|
11546
|
+
const expr = typeof script === "function" ? `(${script.toString()})()` : script;
|
|
11547
|
+
await this.view.evaluate(expr);
|
|
11548
|
+
}
|
|
11549
|
+
keyboard = {
|
|
11550
|
+
press: (key) => this.view.press(key)
|
|
11551
|
+
};
|
|
11552
|
+
context() {
|
|
11553
|
+
return {
|
|
11554
|
+
close: async () => {
|
|
11555
|
+
await this.close();
|
|
11556
|
+
},
|
|
11557
|
+
newPage: async () => {
|
|
11558
|
+
throw new Error("Multi-tab not supported in Bun.WebView. Use engine: 'playwright'");
|
|
11559
|
+
},
|
|
11560
|
+
cookies: async () => [],
|
|
11561
|
+
addCookies: async (_) => {},
|
|
11562
|
+
clearCookies: async () => {},
|
|
11563
|
+
newCDPSession: async () => {
|
|
11564
|
+
throw new Error("CDP session via context not available in Bun.WebView. Use view.cdp() when shipped.");
|
|
11565
|
+
},
|
|
11566
|
+
route: async (_pattern, _handler) => {
|
|
11567
|
+
throw new Error("Network interception not supported in Bun.WebView. Use engine: 'cdp' or 'playwright'.");
|
|
11568
|
+
},
|
|
11569
|
+
unrouteAll: async () => {},
|
|
11570
|
+
pages: () => [],
|
|
11571
|
+
addInitScript: async (script) => {
|
|
11572
|
+
await this.addInitScript(script);
|
|
11573
|
+
}
|
|
11574
|
+
};
|
|
11575
|
+
}
|
|
11576
|
+
on(event, handler) {
|
|
11577
|
+
if (!this._eventListeners.has(event))
|
|
11578
|
+
this._eventListeners.set(event, []);
|
|
11579
|
+
this._eventListeners.get(event).push(handler);
|
|
11580
|
+
return this;
|
|
11581
|
+
}
|
|
11582
|
+
off(event, handler) {
|
|
11583
|
+
const listeners = this._eventListeners.get(event) ?? [];
|
|
11584
|
+
this._eventListeners.set(event, listeners.filter((l) => l !== handler));
|
|
11585
|
+
return this;
|
|
11586
|
+
}
|
|
11587
|
+
_emit(event, ...args) {
|
|
11588
|
+
for (const handler of this._eventListeners.get(event) ?? []) {
|
|
11589
|
+
try {
|
|
11590
|
+
handler(...args);
|
|
11591
|
+
} catch {}
|
|
11592
|
+
}
|
|
11593
|
+
}
|
|
11594
|
+
async pdf(_opts) {
|
|
11595
|
+
throw new Error("PDF generation not supported in Bun.WebView. Use engine: 'playwright'.");
|
|
11596
|
+
}
|
|
11597
|
+
coverage = {
|
|
11598
|
+
startJSCoverage: async () => {},
|
|
11599
|
+
stopJSCoverage: async () => [],
|
|
11600
|
+
startCSSCoverage: async () => {},
|
|
11601
|
+
stopCSSCoverage: async () => []
|
|
11602
|
+
};
|
|
11603
|
+
setSessionId(id) {
|
|
11604
|
+
this._sessionId = id;
|
|
11605
|
+
}
|
|
11606
|
+
getSessionId() {
|
|
11607
|
+
return this._sessionId;
|
|
11608
|
+
}
|
|
11609
|
+
getNativeView() {
|
|
11610
|
+
return this.view;
|
|
11611
|
+
}
|
|
11612
|
+
async close() {
|
|
11613
|
+
try {
|
|
11614
|
+
await this.view.close();
|
|
11615
|
+
} catch {}
|
|
11616
|
+
}
|
|
11617
|
+
[Symbol.asyncDispose]() {
|
|
11618
|
+
return this.close();
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
|
|
11153
11622
|
// src/engines/selector.ts
|
|
11154
11623
|
var ENGINE_MAP = {
|
|
11155
|
-
["scrape" /* SCRAPE */]: "
|
|
11156
|
-
["extract_links" /* EXTRACT_LINKS */]: "
|
|
11157
|
-
["status_check" /* STATUS_CHECK */]: "
|
|
11624
|
+
["scrape" /* SCRAPE */]: "bun",
|
|
11625
|
+
["extract_links" /* EXTRACT_LINKS */]: "bun",
|
|
11626
|
+
["status_check" /* STATUS_CHECK */]: "bun",
|
|
11627
|
+
["screenshot" /* SCREENSHOT */]: "bun",
|
|
11628
|
+
["spa_navigate" /* SPA_NAVIGATE */]: "bun",
|
|
11158
11629
|
["form_fill" /* FORM_FILL */]: "playwright",
|
|
11159
|
-
["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
|
|
11160
|
-
["screenshot" /* SCREENSHOT */]: "playwright",
|
|
11161
11630
|
["auth_flow" /* AUTH_FLOW */]: "playwright",
|
|
11162
11631
|
["multi_tab" /* MULTI_TAB */]: "playwright",
|
|
11163
11632
|
["record_replay" /* RECORD_REPLAY */]: "playwright",
|
|
@@ -11171,6 +11640,14 @@ function selectEngine(useCase, explicit) {
|
|
|
11171
11640
|
if (explicit && explicit !== "auto")
|
|
11172
11641
|
return explicit;
|
|
11173
11642
|
const preferred = ENGINE_MAP[useCase];
|
|
11643
|
+
if (preferred === "bun") {
|
|
11644
|
+
if (isBunWebViewAvailable())
|
|
11645
|
+
return "bun";
|
|
11646
|
+
if (useCase === "scrape" /* SCRAPE */ || useCase === "extract_links" /* EXTRACT_LINKS */ || useCase === "status_check" /* STATUS_CHECK */) {
|
|
11647
|
+
return isLightpandaAvailable() ? "lightpanda" : "playwright";
|
|
11648
|
+
}
|
|
11649
|
+
return "playwright";
|
|
11650
|
+
}
|
|
11174
11651
|
if (preferred === "lightpanda" && !isLightpandaAvailable()) {
|
|
11175
11652
|
return "playwright";
|
|
11176
11653
|
}
|
|
@@ -11462,12 +11939,30 @@ async function handleDialog(sessionId, action, promptText) {
|
|
|
11462
11939
|
|
|
11463
11940
|
// src/lib/session.ts
|
|
11464
11941
|
var handles = new Map;
|
|
11942
|
+
function createBunProxy(view) {
|
|
11943
|
+
return view;
|
|
11944
|
+
}
|
|
11465
11945
|
async function createSession2(opts = {}) {
|
|
11466
11946
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
11467
11947
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
11468
|
-
let browser;
|
|
11948
|
+
let browser = null;
|
|
11949
|
+
let bunView = null;
|
|
11469
11950
|
let page;
|
|
11470
|
-
if (resolvedEngine === "
|
|
11951
|
+
if (resolvedEngine === "bun") {
|
|
11952
|
+
if (!isBunWebViewAvailable()) {
|
|
11953
|
+
console.warn("[browser] Bun.WebView requested but not available \u2014 falling back to playwright. Run: bun upgrade --canary");
|
|
11954
|
+
browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
|
|
11955
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
11956
|
+
} else {
|
|
11957
|
+
bunView = new BunWebViewSession({
|
|
11958
|
+
width: opts.viewport?.width ?? 1280,
|
|
11959
|
+
height: opts.viewport?.height ?? 720,
|
|
11960
|
+
profile: opts.name ?? undefined
|
|
11961
|
+
});
|
|
11962
|
+
if (opts.stealth) {}
|
|
11963
|
+
page = createBunProxy(bunView);
|
|
11964
|
+
}
|
|
11965
|
+
} else if (resolvedEngine === "lightpanda") {
|
|
11471
11966
|
browser = await connectLightpanda();
|
|
11472
11967
|
const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
|
|
11473
11968
|
page = await context.newPage();
|
|
@@ -11477,12 +11972,9 @@ async function createSession2(opts = {}) {
|
|
|
11477
11972
|
viewport: opts.viewport,
|
|
11478
11973
|
userAgent: opts.userAgent
|
|
11479
11974
|
});
|
|
11480
|
-
page = await getPage(browser, {
|
|
11481
|
-
viewport: opts.viewport,
|
|
11482
|
-
userAgent: opts.userAgent
|
|
11483
|
-
});
|
|
11975
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
11484
11976
|
}
|
|
11485
|
-
|
|
11977
|
+
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
11486
11978
|
try {
|
|
11487
11979
|
return new URL(opts.startUrl).hostname;
|
|
11488
11980
|
} catch {
|
|
@@ -11490,35 +11982,57 @@ async function createSession2(opts = {}) {
|
|
|
11490
11982
|
}
|
|
11491
11983
|
})() : undefined);
|
|
11492
11984
|
const session = createSession({
|
|
11493
|
-
engine: resolvedEngine,
|
|
11985
|
+
engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
|
|
11494
11986
|
projectId: opts.projectId,
|
|
11495
11987
|
agentId: opts.agentId,
|
|
11496
11988
|
startUrl: opts.startUrl,
|
|
11497
11989
|
name: sessionName
|
|
11498
11990
|
});
|
|
11499
|
-
if (opts.stealth) {
|
|
11991
|
+
if (opts.stealth && !bunView) {
|
|
11500
11992
|
try {
|
|
11501
11993
|
await applyStealthPatches(page);
|
|
11502
11994
|
} catch {}
|
|
11503
11995
|
}
|
|
11504
11996
|
const cleanups = [];
|
|
11505
|
-
if (
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11997
|
+
if (!bunView) {
|
|
11998
|
+
if (opts.captureNetwork !== false) {
|
|
11999
|
+
try {
|
|
12000
|
+
cleanups.push(enableNetworkLogging(page, session.id));
|
|
12001
|
+
} catch {}
|
|
12002
|
+
}
|
|
12003
|
+
if (opts.captureConsole !== false) {
|
|
12004
|
+
try {
|
|
12005
|
+
cleanups.push(enableConsoleCapture(page, session.id));
|
|
12006
|
+
} catch {}
|
|
12007
|
+
}
|
|
11511
12008
|
try {
|
|
11512
|
-
cleanups.push(
|
|
12009
|
+
cleanups.push(setupDialogHandler(page, session.id));
|
|
11513
12010
|
} catch {}
|
|
12011
|
+
} else {
|
|
12012
|
+
if (opts.captureConsole !== false) {
|
|
12013
|
+
try {
|
|
12014
|
+
const { logConsoleMessage: logConsoleMessage2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
|
|
12015
|
+
await bunView.addInitScript(`
|
|
12016
|
+
(() => {
|
|
12017
|
+
const orig = { log: console.log, warn: console.warn, error: console.error, debug: console.debug, info: console.info };
|
|
12018
|
+
['log','warn','error','debug','info'].forEach(level => {
|
|
12019
|
+
console[level] = (...args) => {
|
|
12020
|
+
orig[level](...args);
|
|
12021
|
+
};
|
|
12022
|
+
});
|
|
12023
|
+
})()
|
|
12024
|
+
`);
|
|
12025
|
+
} catch {}
|
|
12026
|
+
}
|
|
11514
12027
|
}
|
|
11515
|
-
|
|
11516
|
-
cleanups.push(setupDialogHandler(page, session.id));
|
|
11517
|
-
} catch {}
|
|
11518
|
-
handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
12028
|
+
handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
11519
12029
|
if (opts.startUrl) {
|
|
11520
12030
|
try {
|
|
11521
|
-
|
|
12031
|
+
if (bunView) {
|
|
12032
|
+
await bunView.goto(opts.startUrl);
|
|
12033
|
+
} else {
|
|
12034
|
+
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
12035
|
+
}
|
|
11522
12036
|
} catch {}
|
|
11523
12037
|
}
|
|
11524
12038
|
return { session, page };
|
|
@@ -11528,13 +12042,23 @@ function getSessionPage(sessionId) {
|
|
|
11528
12042
|
if (!handle)
|
|
11529
12043
|
throw new SessionNotFoundError(sessionId);
|
|
11530
12044
|
try {
|
|
11531
|
-
handle.
|
|
12045
|
+
if (handle.bunView) {
|
|
12046
|
+
handle.bunView.url();
|
|
12047
|
+
} else {
|
|
12048
|
+
handle.page.url();
|
|
12049
|
+
}
|
|
11532
12050
|
} catch {
|
|
11533
12051
|
handles.delete(sessionId);
|
|
11534
12052
|
throw new SessionNotFoundError(sessionId);
|
|
11535
12053
|
}
|
|
11536
12054
|
return handle.page;
|
|
11537
12055
|
}
|
|
12056
|
+
function getSessionBunView(sessionId) {
|
|
12057
|
+
return handles.get(sessionId)?.bunView ?? null;
|
|
12058
|
+
}
|
|
12059
|
+
function isBunSession(sessionId) {
|
|
12060
|
+
return handles.get(sessionId)?.engine === "bun";
|
|
12061
|
+
}
|
|
11538
12062
|
function setSessionPage(sessionId, page) {
|
|
11539
12063
|
const handle = handles.get(sessionId);
|
|
11540
12064
|
if (!handle)
|
|
@@ -11549,12 +12073,19 @@ async function closeSession2(sessionId) {
|
|
|
11549
12073
|
cleanup();
|
|
11550
12074
|
} catch {}
|
|
11551
12075
|
}
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
12076
|
+
if (handle.bunView) {
|
|
12077
|
+
try {
|
|
12078
|
+
await handle.bunView.close();
|
|
12079
|
+
} catch {}
|
|
12080
|
+
} else {
|
|
12081
|
+
try {
|
|
12082
|
+
await handle.page.context().close();
|
|
12083
|
+
} catch {}
|
|
12084
|
+
try {
|
|
12085
|
+
if (handle.browser)
|
|
12086
|
+
await closeBrowser(handle.browser);
|
|
12087
|
+
} catch {}
|
|
12088
|
+
}
|
|
11558
12089
|
handles.delete(sessionId);
|
|
11559
12090
|
}
|
|
11560
12091
|
return closeSession(sessionId);
|
|
@@ -11890,9 +12421,19 @@ async function getLinks(page, baseUrl) {
|
|
|
11890
12421
|
}, baseUrl ?? page.url());
|
|
11891
12422
|
}
|
|
11892
12423
|
async function getTitle(page) {
|
|
12424
|
+
if (typeof page.getNativeView === "function") {
|
|
12425
|
+
const nativeView = page.getNativeView();
|
|
12426
|
+
const t = nativeView?.title;
|
|
12427
|
+
return typeof t === "string" && t ? t : "";
|
|
12428
|
+
}
|
|
11893
12429
|
return page.title();
|
|
11894
12430
|
}
|
|
11895
12431
|
async function getUrl(page) {
|
|
12432
|
+
if (typeof page.getNativeView === "function") {
|
|
12433
|
+
const nativeView = page.getNativeView();
|
|
12434
|
+
const u = nativeView?.url;
|
|
12435
|
+
return typeof u === "string" ? u : "";
|
|
12436
|
+
}
|
|
11896
12437
|
return page.url();
|
|
11897
12438
|
}
|
|
11898
12439
|
async function findElements(page, selector) {
|
|
@@ -11983,9 +12524,9 @@ async function getPageInfo(page) {
|
|
|
11983
12524
|
|
|
11984
12525
|
// src/lib/screenshot.ts
|
|
11985
12526
|
var import_sharp = __toESM(require_lib(), 1);
|
|
11986
|
-
import { join as
|
|
11987
|
-
import { mkdirSync as
|
|
11988
|
-
import { homedir as
|
|
12527
|
+
import { join as join3 } from "path";
|
|
12528
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
12529
|
+
import { homedir as homedir3 } from "os";
|
|
11989
12530
|
|
|
11990
12531
|
// src/db/gallery.ts
|
|
11991
12532
|
init_schema();
|
|
@@ -12131,13 +12672,13 @@ function getGalleryStats(projectId) {
|
|
|
12131
12672
|
|
|
12132
12673
|
// src/lib/screenshot.ts
|
|
12133
12674
|
function getDataDir2() {
|
|
12134
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
12675
|
+
return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
|
|
12135
12676
|
}
|
|
12136
12677
|
function getScreenshotDir(projectId) {
|
|
12137
|
-
const base =
|
|
12678
|
+
const base = join3(getDataDir2(), "screenshots");
|
|
12138
12679
|
const date = new Date().toISOString().split("T")[0];
|
|
12139
|
-
const dir = projectId ?
|
|
12140
|
-
|
|
12680
|
+
const dir = projectId ? join3(base, projectId, date) : join3(base, date);
|
|
12681
|
+
mkdirSync3(dir, { recursive: true });
|
|
12141
12682
|
return dir;
|
|
12142
12683
|
}
|
|
12143
12684
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -12152,7 +12693,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
12152
12693
|
}
|
|
12153
12694
|
}
|
|
12154
12695
|
async function generateThumbnail(raw, dir, stem) {
|
|
12155
|
-
const thumbPath =
|
|
12696
|
+
const thumbPath = join3(dir, `${stem}.thumb.webp`);
|
|
12156
12697
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
12157
12698
|
await Bun.write(thumbPath, thumbBuffer);
|
|
12158
12699
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -12171,11 +12712,20 @@ async function takeScreenshot(page, opts) {
|
|
|
12171
12712
|
type: "png"
|
|
12172
12713
|
};
|
|
12173
12714
|
let rawBuffer;
|
|
12715
|
+
const isBunView = typeof page.getNativeView === "function";
|
|
12174
12716
|
if (opts?.selector) {
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12717
|
+
if (isBunView) {
|
|
12718
|
+
const uint8 = await page.screenshot();
|
|
12719
|
+
rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
|
|
12720
|
+
} else {
|
|
12721
|
+
const el = await page.$(opts.selector);
|
|
12722
|
+
if (!el)
|
|
12723
|
+
throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
|
|
12724
|
+
rawBuffer = await el.screenshot(rawOpts);
|
|
12725
|
+
}
|
|
12726
|
+
} else if (isBunView) {
|
|
12727
|
+
const uint8 = await page.screenshot();
|
|
12728
|
+
rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
|
|
12179
12729
|
} else {
|
|
12180
12730
|
rawBuffer = await page.screenshot(rawOpts);
|
|
12181
12731
|
}
|
|
@@ -12200,7 +12750,7 @@ async function takeScreenshot(page, opts) {
|
|
|
12200
12750
|
const compressedSizeBytes = finalBuffer.length;
|
|
12201
12751
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
12202
12752
|
const ext = format;
|
|
12203
|
-
const screenshotPath = opts?.path ??
|
|
12753
|
+
const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
|
|
12204
12754
|
await Bun.write(screenshotPath, finalBuffer);
|
|
12205
12755
|
let thumbnailPath;
|
|
12206
12756
|
let thumbnailBase64;
|
|
@@ -12260,12 +12810,12 @@ async function takeScreenshot(page, opts) {
|
|
|
12260
12810
|
}
|
|
12261
12811
|
async function generatePDF(page, opts) {
|
|
12262
12812
|
try {
|
|
12263
|
-
const base =
|
|
12813
|
+
const base = join3(getDataDir2(), "pdfs");
|
|
12264
12814
|
const date = new Date().toISOString().split("T")[0];
|
|
12265
|
-
const dir = opts?.projectId ?
|
|
12266
|
-
|
|
12815
|
+
const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
|
|
12816
|
+
mkdirSync3(dir, { recursive: true });
|
|
12267
12817
|
const timestamp = Date.now();
|
|
12268
|
-
const pdfPath = opts?.path ??
|
|
12818
|
+
const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
|
|
12269
12819
|
const buffer = await page.pdf({
|
|
12270
12820
|
path: pdfPath,
|
|
12271
12821
|
format: opts?.format ?? "A4",
|
|
@@ -12788,16 +13338,16 @@ init_console_log();
|
|
|
12788
13338
|
|
|
12789
13339
|
// src/lib/downloads.ts
|
|
12790
13340
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
12791
|
-
import { join as
|
|
12792
|
-
import { mkdirSync as
|
|
12793
|
-
import { homedir as
|
|
13341
|
+
import { join as join4, basename, extname } from "path";
|
|
13342
|
+
import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
13343
|
+
import { homedir as homedir4 } from "os";
|
|
12794
13344
|
function getDataDir3() {
|
|
12795
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
13345
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
12796
13346
|
}
|
|
12797
13347
|
function getDownloadsDir(sessionId) {
|
|
12798
|
-
const base =
|
|
12799
|
-
const dir = sessionId ?
|
|
12800
|
-
|
|
13348
|
+
const base = join4(getDataDir3(), "downloads");
|
|
13349
|
+
const dir = sessionId ? join4(base, sessionId) : base;
|
|
13350
|
+
mkdirSync4(dir, { recursive: true });
|
|
12801
13351
|
return dir;
|
|
12802
13352
|
}
|
|
12803
13353
|
function metaPath(filePath) {
|
|
@@ -12809,7 +13359,7 @@ function saveToDownloads(buffer, filename, opts) {
|
|
|
12809
13359
|
const ext = extname(filename) || "";
|
|
12810
13360
|
const stem = basename(filename, ext);
|
|
12811
13361
|
const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
|
|
12812
|
-
const filePath =
|
|
13362
|
+
const filePath = join4(dir, uniqueName);
|
|
12813
13363
|
writeFileSync(filePath, buffer);
|
|
12814
13364
|
const meta = {
|
|
12815
13365
|
id,
|
|
@@ -12844,7 +13394,7 @@ function listDownloads(sessionId) {
|
|
|
12844
13394
|
for (const entry of entries) {
|
|
12845
13395
|
if (entry.endsWith(".meta.json"))
|
|
12846
13396
|
continue;
|
|
12847
|
-
const full =
|
|
13397
|
+
const full = join4(d, entry);
|
|
12848
13398
|
const stat = statSync(full);
|
|
12849
13399
|
if (stat.isDirectory()) {
|
|
12850
13400
|
scanDir(full);
|
|
@@ -12929,9 +13479,9 @@ function detectType(filename) {
|
|
|
12929
13479
|
|
|
12930
13480
|
// src/lib/gallery-diff.ts
|
|
12931
13481
|
var import_sharp2 = __toESM(require_lib(), 1);
|
|
12932
|
-
import { join as
|
|
12933
|
-
import { mkdirSync as
|
|
12934
|
-
import { homedir as
|
|
13482
|
+
import { join as join5 } from "path";
|
|
13483
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
13484
|
+
import { homedir as homedir5 } from "os";
|
|
12935
13485
|
async function diffImages(path1, path2) {
|
|
12936
13486
|
const img1 = import_sharp2.default(path1);
|
|
12937
13487
|
const img2 = import_sharp2.default(path2);
|
|
@@ -12962,10 +13512,10 @@ async function diffImages(path1, path2) {
|
|
|
12962
13512
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
12963
13513
|
}
|
|
12964
13514
|
}
|
|
12965
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
12966
|
-
const diffDir =
|
|
12967
|
-
|
|
12968
|
-
const diffPath =
|
|
13515
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
13516
|
+
const diffDir = join5(dataDir, "diffs");
|
|
13517
|
+
mkdirSync5(diffDir, { recursive: true });
|
|
13518
|
+
const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
|
|
12969
13519
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
12970
13520
|
await Bun.write(diffPath, diffImageBuffer);
|
|
12971
13521
|
return {
|
|
@@ -12981,9 +13531,9 @@ async function diffImages(path1, path2) {
|
|
|
12981
13531
|
init_snapshot();
|
|
12982
13532
|
|
|
12983
13533
|
// src/lib/files-integration.ts
|
|
12984
|
-
import { join as
|
|
12985
|
-
import { mkdirSync as
|
|
12986
|
-
import { homedir as
|
|
13534
|
+
import { join as join6 } from "path";
|
|
13535
|
+
import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
|
|
13536
|
+
import { homedir as homedir6 } from "os";
|
|
12987
13537
|
async function persistFile(localPath, opts) {
|
|
12988
13538
|
try {
|
|
12989
13539
|
const mod = await import("@hasna/files");
|
|
@@ -12992,12 +13542,12 @@ async function persistFile(localPath, opts) {
|
|
|
12992
13542
|
return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
|
|
12993
13543
|
}
|
|
12994
13544
|
} catch {}
|
|
12995
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
13545
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
12996
13546
|
const date = new Date().toISOString().split("T")[0];
|
|
12997
|
-
const dir =
|
|
12998
|
-
|
|
13547
|
+
const dir = join6(dataDir, "persistent", date);
|
|
13548
|
+
mkdirSync6(dir, { recursive: true });
|
|
12999
13549
|
const filename = localPath.split("/").pop() ?? "file";
|
|
13000
|
-
const targetPath =
|
|
13550
|
+
const targetPath = join6(dir, filename);
|
|
13001
13551
|
copyFileSync2(localPath, targetPath);
|
|
13002
13552
|
return {
|
|
13003
13553
|
id: `local-${Date.now()}`,
|
|
@@ -13089,23 +13639,23 @@ async function closeTab(page, index) {
|
|
|
13089
13639
|
}
|
|
13090
13640
|
|
|
13091
13641
|
// src/lib/profiles.ts
|
|
13092
|
-
import { mkdirSync as
|
|
13093
|
-
import { join as
|
|
13094
|
-
import { homedir as
|
|
13642
|
+
import { mkdirSync as mkdirSync7, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
13643
|
+
import { join as join7 } from "path";
|
|
13644
|
+
import { homedir as homedir7 } from "os";
|
|
13095
13645
|
function getProfilesDir() {
|
|
13096
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
13097
|
-
const dir =
|
|
13098
|
-
|
|
13646
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
|
|
13647
|
+
const dir = join7(dataDir, "profiles");
|
|
13648
|
+
mkdirSync7(dir, { recursive: true });
|
|
13099
13649
|
return dir;
|
|
13100
13650
|
}
|
|
13101
|
-
function
|
|
13102
|
-
return
|
|
13651
|
+
function getProfileDir2(name) {
|
|
13652
|
+
return join7(getProfilesDir(), name);
|
|
13103
13653
|
}
|
|
13104
13654
|
async function saveProfile(page, name) {
|
|
13105
|
-
const dir =
|
|
13106
|
-
|
|
13655
|
+
const dir = getProfileDir2(name);
|
|
13656
|
+
mkdirSync7(dir, { recursive: true });
|
|
13107
13657
|
const cookies = await page.context().cookies();
|
|
13108
|
-
writeFileSync2(
|
|
13658
|
+
writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
13109
13659
|
let localStorage2 = {};
|
|
13110
13660
|
try {
|
|
13111
13661
|
localStorage2 = await page.evaluate(() => {
|
|
@@ -13117,11 +13667,11 @@ async function saveProfile(page, name) {
|
|
|
13117
13667
|
return result;
|
|
13118
13668
|
});
|
|
13119
13669
|
} catch {}
|
|
13120
|
-
writeFileSync2(
|
|
13670
|
+
writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
13121
13671
|
const savedAt = new Date().toISOString();
|
|
13122
13672
|
const url = page.url();
|
|
13123
13673
|
const meta = { saved_at: savedAt, url };
|
|
13124
|
-
writeFileSync2(
|
|
13674
|
+
writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
13125
13675
|
return {
|
|
13126
13676
|
name,
|
|
13127
13677
|
saved_at: savedAt,
|
|
@@ -13131,13 +13681,13 @@ async function saveProfile(page, name) {
|
|
|
13131
13681
|
};
|
|
13132
13682
|
}
|
|
13133
13683
|
function loadProfile(name) {
|
|
13134
|
-
const dir =
|
|
13684
|
+
const dir = getProfileDir2(name);
|
|
13135
13685
|
if (!existsSync3(dir)) {
|
|
13136
13686
|
throw new Error(`Profile not found: ${name}`);
|
|
13137
13687
|
}
|
|
13138
|
-
const cookiesPath =
|
|
13139
|
-
const storagePath =
|
|
13140
|
-
const metaPath2 =
|
|
13688
|
+
const cookiesPath = join7(dir, "cookies.json");
|
|
13689
|
+
const storagePath = join7(dir, "storage.json");
|
|
13690
|
+
const metaPath2 = join7(dir, "meta.json");
|
|
13141
13691
|
const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
13142
13692
|
const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
13143
13693
|
let savedAt = new Date().toISOString();
|
|
@@ -13178,24 +13728,24 @@ function listProfiles() {
|
|
|
13178
13728
|
if (!entry.isDirectory())
|
|
13179
13729
|
continue;
|
|
13180
13730
|
const name = entry.name;
|
|
13181
|
-
const profileDir =
|
|
13731
|
+
const profileDir = join7(dir, name);
|
|
13182
13732
|
let savedAt = "";
|
|
13183
13733
|
let url;
|
|
13184
13734
|
let cookieCount = 0;
|
|
13185
13735
|
let storageKeyCount = 0;
|
|
13186
13736
|
try {
|
|
13187
|
-
const metaPath2 =
|
|
13737
|
+
const metaPath2 = join7(profileDir, "meta.json");
|
|
13188
13738
|
if (existsSync3(metaPath2)) {
|
|
13189
13739
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
13190
13740
|
savedAt = meta.saved_at ?? "";
|
|
13191
13741
|
url = meta.url;
|
|
13192
13742
|
}
|
|
13193
|
-
const cookiesPath =
|
|
13743
|
+
const cookiesPath = join7(profileDir, "cookies.json");
|
|
13194
13744
|
if (existsSync3(cookiesPath)) {
|
|
13195
13745
|
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
13196
13746
|
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
13197
13747
|
}
|
|
13198
|
-
const storagePath =
|
|
13748
|
+
const storagePath = join7(profileDir, "storage.json");
|
|
13199
13749
|
if (existsSync3(storagePath)) {
|
|
13200
13750
|
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
13201
13751
|
storageKeyCount = Object.keys(storage).length;
|
|
@@ -13212,7 +13762,7 @@ function listProfiles() {
|
|
|
13212
13762
|
return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
|
|
13213
13763
|
}
|
|
13214
13764
|
function deleteProfile(name) {
|
|
13215
|
-
const dir =
|
|
13765
|
+
const dir = getProfileDir2(name);
|
|
13216
13766
|
if (!existsSync3(dir))
|
|
13217
13767
|
return false;
|
|
13218
13768
|
try {
|
|
@@ -13224,7 +13774,7 @@ function deleteProfile(name) {
|
|
|
13224
13774
|
}
|
|
13225
13775
|
|
|
13226
13776
|
// src/mcp/index.ts
|
|
13227
|
-
var _pkg = JSON.parse(readFileSync3(
|
|
13777
|
+
var _pkg = JSON.parse(readFileSync3(join8(import.meta.dir, "../../package.json"), "utf8"));
|
|
13228
13778
|
var networkLogCleanup = new Map;
|
|
13229
13779
|
var consoleCaptureCleanup = new Map;
|
|
13230
13780
|
var harCaptures = new Map;
|
|
@@ -13244,7 +13794,7 @@ var server = new McpServer({
|
|
|
13244
13794
|
version: "0.0.1"
|
|
13245
13795
|
});
|
|
13246
13796
|
server.tool("browser_session_create", "Create a new browser session with the specified engine", {
|
|
13247
|
-
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto"),
|
|
13797
|
+
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
|
|
13248
13798
|
use_case: exports_external.string().optional(),
|
|
13249
13799
|
project_id: exports_external.string().optional(),
|
|
13250
13800
|
agent_id: exports_external.string().optional(),
|
|
@@ -13299,7 +13849,13 @@ server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto
|
|
|
13299
13849
|
}, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
|
|
13300
13850
|
try {
|
|
13301
13851
|
const page = getSessionPage(session_id);
|
|
13302
|
-
|
|
13852
|
+
if (isBunSession(session_id)) {
|
|
13853
|
+
const bunView = getSessionBunView(session_id);
|
|
13854
|
+
await bunView.goto(url, { timeout });
|
|
13855
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
13856
|
+
} else {
|
|
13857
|
+
await navigate(page, url, timeout);
|
|
13858
|
+
}
|
|
13303
13859
|
const title = await getTitle(page);
|
|
13304
13860
|
const current_url = await getUrl(page);
|
|
13305
13861
|
const redirected = current_url !== url && current_url !== url + "/" && url !== current_url.replace(/\/$/, "");
|
|
@@ -13340,6 +13896,9 @@ server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto
|
|
|
13340
13896
|
result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
13341
13897
|
} catch {}
|
|
13342
13898
|
}
|
|
13899
|
+
if (isBunSession(session_id) && auto_snapshot) {
|
|
13900
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
13901
|
+
}
|
|
13343
13902
|
if (auto_snapshot) {
|
|
13344
13903
|
try {
|
|
13345
13904
|
const snap = await takeSnapshot(page, session_id);
|
|
@@ -13847,7 +14406,7 @@ server.tool("browser_crawl", "Crawl a URL recursively and return discovered page
|
|
|
13847
14406
|
max_pages: exports_external.number().optional().default(50),
|
|
13848
14407
|
same_domain: exports_external.boolean().optional().default(true),
|
|
13849
14408
|
project_id: exports_external.string().optional(),
|
|
13850
|
-
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto")
|
|
14409
|
+
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto")
|
|
13851
14410
|
}, async ({ url, max_depth, max_pages, same_domain, project_id, engine }) => {
|
|
13852
14411
|
try {
|
|
13853
14412
|
const result = await crawl(url, {
|