@hasna/browser 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1254 -349
- package/dist/engines/cdp.d.ts +2 -1
- package/dist/engines/cdp.d.ts.map +1 -1
- package/dist/index.js +497 -214
- package/dist/lib/actions.d.ts +22 -4
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/auth-flow.d.ts +43 -0
- package/dist/lib/auth-flow.d.ts.map +1 -0
- package/dist/lib/sanitize.d.ts +21 -0
- package/dist/lib/sanitize.d.ts.map +1 -0
- package/dist/lib/self-heal.d.ts +18 -0
- package/dist/lib/self-heal.d.ts.map +1 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/storage-state.d.ts +15 -0
- package/dist/lib/storage-state.d.ts.map +1 -0
- package/dist/lib/vision-fallback.d.ts +29 -0
- package/dist/lib/vision-fallback.d.ts.map +1 -0
- package/dist/mcp/index.js +1360 -470
- package/dist/server/index.js +374 -158
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -291,6 +291,23 @@ function runMigrations(db) {
|
|
|
291
291
|
);
|
|
292
292
|
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
|
|
293
293
|
`
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
version: 6,
|
|
297
|
+
sql: `
|
|
298
|
+
CREATE TABLE IF NOT EXISTS auth_flows (
|
|
299
|
+
id TEXT PRIMARY KEY,
|
|
300
|
+
name TEXT NOT NULL UNIQUE,
|
|
301
|
+
domain TEXT NOT NULL,
|
|
302
|
+
recording_id TEXT REFERENCES recordings(id),
|
|
303
|
+
storage_state_path TEXT,
|
|
304
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
305
|
+
last_used TEXT
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
|
|
309
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
|
|
310
|
+
`
|
|
294
311
|
}
|
|
295
312
|
];
|
|
296
313
|
for (const m of migrations) {
|
|
@@ -339,6 +356,188 @@ var init_console_log = __esm(() => {
|
|
|
339
356
|
init_schema();
|
|
340
357
|
});
|
|
341
358
|
|
|
359
|
+
// src/engines/cdp.ts
|
|
360
|
+
var exports_cdp = {};
|
|
361
|
+
__export(exports_cdp, {
|
|
362
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
363
|
+
CDPClient: () => CDPClient
|
|
364
|
+
});
|
|
365
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
366
|
+
const { chromium: chromium3 } = await import("playwright");
|
|
367
|
+
try {
|
|
368
|
+
return await chromium3.connectOverCDP(cdpUrl);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
throw new BrowserError(`Failed to connect to browser at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}. Start Chrome with: google-chrome --remote-debugging-port=9222`, "CDP_CONNECT_FAILED", true);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
class CDPClient {
|
|
375
|
+
session;
|
|
376
|
+
networkEnabled = false;
|
|
377
|
+
performanceEnabled = false;
|
|
378
|
+
constructor(session) {
|
|
379
|
+
this.session = session;
|
|
380
|
+
}
|
|
381
|
+
static async fromPage(page) {
|
|
382
|
+
try {
|
|
383
|
+
const session = await page.context().newCDPSession(page);
|
|
384
|
+
return new CDPClient(session);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async send(method, params) {
|
|
390
|
+
try {
|
|
391
|
+
return await this.session.send(method, params);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
on(event, handler) {
|
|
397
|
+
this.session.on(event, handler);
|
|
398
|
+
}
|
|
399
|
+
off(event, handler) {
|
|
400
|
+
this.session.off(event, handler);
|
|
401
|
+
}
|
|
402
|
+
async enableNetwork() {
|
|
403
|
+
if (!this.networkEnabled) {
|
|
404
|
+
await this.send("Network.enable");
|
|
405
|
+
this.networkEnabled = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async enablePerformance() {
|
|
409
|
+
if (!this.performanceEnabled) {
|
|
410
|
+
await this.send("Performance.enable");
|
|
411
|
+
this.performanceEnabled = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async getPerformanceMetrics() {
|
|
415
|
+
await this.enablePerformance();
|
|
416
|
+
const result = await this.send("Performance.getMetrics");
|
|
417
|
+
const m = {};
|
|
418
|
+
for (const metric of result.metrics) {
|
|
419
|
+
m[metric.name] = metric.value;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
423
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
424
|
+
dom_interactive: m["DOMInteractive"],
|
|
425
|
+
dom_complete: m["DOMComplete"],
|
|
426
|
+
load_event: m["LoadEventEnd"]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async startJSCoverage() {
|
|
430
|
+
await this.send("Profiler.enable");
|
|
431
|
+
await this.send("Debugger.enable");
|
|
432
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
433
|
+
callCount: false,
|
|
434
|
+
detailed: true
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
async stopJSCoverage() {
|
|
438
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
439
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
440
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
441
|
+
url: r.url,
|
|
442
|
+
text: "",
|
|
443
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
async getCoverage() {
|
|
447
|
+
await this.startJSCoverage();
|
|
448
|
+
const js = await this.stopJSCoverage();
|
|
449
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
450
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
451
|
+
}
|
|
452
|
+
async captureHAREntries(page, handler) {
|
|
453
|
+
await this.enableNetwork();
|
|
454
|
+
const requestTimings = new Map;
|
|
455
|
+
const onRequest = (params) => {
|
|
456
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
457
|
+
};
|
|
458
|
+
const onResponse = (params) => {
|
|
459
|
+
const start = requestTimings.get(params.requestId);
|
|
460
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
461
|
+
handler({
|
|
462
|
+
method: "GET",
|
|
463
|
+
url: params.response.url,
|
|
464
|
+
status: params.response.status,
|
|
465
|
+
duration
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
469
|
+
this.on("Network.responseReceived", onResponse);
|
|
470
|
+
return () => {
|
|
471
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
472
|
+
this.off("Network.responseReceived", onResponse);
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async detach() {
|
|
476
|
+
try {
|
|
477
|
+
await this.session.detach();
|
|
478
|
+
} catch {}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
var init_cdp = __esm(() => {
|
|
482
|
+
init_types();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// src/lib/storage-state.ts
|
|
486
|
+
var exports_storage_state = {};
|
|
487
|
+
__export(exports_storage_state, {
|
|
488
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
489
|
+
saveState: () => saveState,
|
|
490
|
+
loadStatePath: () => loadStatePath,
|
|
491
|
+
listStates: () => listStates,
|
|
492
|
+
deleteState: () => deleteState
|
|
493
|
+
});
|
|
494
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
495
|
+
import { join as join3 } from "path";
|
|
496
|
+
import { homedir as homedir3 } from "os";
|
|
497
|
+
function ensureDir() {
|
|
498
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
499
|
+
}
|
|
500
|
+
function statePath(name) {
|
|
501
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
502
|
+
}
|
|
503
|
+
async function saveState(context, name) {
|
|
504
|
+
ensureDir();
|
|
505
|
+
const path = statePath(name);
|
|
506
|
+
const state = await context.storageState({ path });
|
|
507
|
+
return path;
|
|
508
|
+
}
|
|
509
|
+
async function saveStateFromPage(page, name) {
|
|
510
|
+
return saveState(page.context(), name);
|
|
511
|
+
}
|
|
512
|
+
function loadStatePath(name) {
|
|
513
|
+
const path = statePath(name);
|
|
514
|
+
return existsSync(path) ? path : null;
|
|
515
|
+
}
|
|
516
|
+
function listStates() {
|
|
517
|
+
ensureDir();
|
|
518
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
519
|
+
const path = join3(STATES_DIR, f);
|
|
520
|
+
const stat = Bun.file(path);
|
|
521
|
+
return {
|
|
522
|
+
name: f.replace(".json", ""),
|
|
523
|
+
path,
|
|
524
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
525
|
+
};
|
|
526
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
527
|
+
}
|
|
528
|
+
function deleteState(name) {
|
|
529
|
+
const path = statePath(name);
|
|
530
|
+
if (existsSync(path)) {
|
|
531
|
+
unlinkSync(path);
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
var STATES_DIR;
|
|
537
|
+
var init_storage_state = __esm(() => {
|
|
538
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
539
|
+
});
|
|
540
|
+
|
|
342
541
|
// node_modules/sharp/lib/is.js
|
|
343
542
|
var require_is = __commonJS((exports, module) => {
|
|
344
543
|
/*!
|
|
@@ -6933,8 +7132,8 @@ var init_snapshots = __esm(() => {
|
|
|
6933
7132
|
});
|
|
6934
7133
|
|
|
6935
7134
|
// src/server/index.ts
|
|
6936
|
-
import { join as
|
|
6937
|
-
import { existsSync as
|
|
7135
|
+
import { join as join7 } from "path";
|
|
7136
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6938
7137
|
|
|
6939
7138
|
// src/lib/session.ts
|
|
6940
7139
|
init_types();
|
|
@@ -7862,6 +8061,37 @@ function createBunProxy(view) {
|
|
|
7862
8061
|
return view;
|
|
7863
8062
|
}
|
|
7864
8063
|
async function createSession2(opts = {}) {
|
|
8064
|
+
if (opts.cdpUrl) {
|
|
8065
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
8066
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
8067
|
+
const contexts = cdpBrowser.contexts();
|
|
8068
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
8069
|
+
const pages = context.pages();
|
|
8070
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
8071
|
+
const session2 = createSession({
|
|
8072
|
+
engine: "cdp",
|
|
8073
|
+
projectId: opts.projectId,
|
|
8074
|
+
agentId: opts.agentId,
|
|
8075
|
+
startUrl: page2.url(),
|
|
8076
|
+
name: opts.name ?? "attached"
|
|
8077
|
+
});
|
|
8078
|
+
const cleanups2 = [];
|
|
8079
|
+
if (opts.captureNetwork !== false) {
|
|
8080
|
+
try {
|
|
8081
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
8082
|
+
} catch {}
|
|
8083
|
+
}
|
|
8084
|
+
if (opts.captureConsole !== false) {
|
|
8085
|
+
try {
|
|
8086
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
8087
|
+
} catch {}
|
|
8088
|
+
}
|
|
8089
|
+
try {
|
|
8090
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
8091
|
+
} catch {}
|
|
8092
|
+
handles.set(session2.id, { browser: cdpBrowser, bunView: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
|
|
8093
|
+
return { session: session2, page: page2 };
|
|
8094
|
+
}
|
|
7865
8095
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
7866
8096
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
7867
8097
|
let browser = null;
|
|
@@ -7887,7 +8117,22 @@ async function createSession2(opts = {}) {
|
|
|
7887
8117
|
page = await context.newPage();
|
|
7888
8118
|
} else {
|
|
7889
8119
|
browser = await pool.acquire(opts.headless ?? true);
|
|
7890
|
-
|
|
8120
|
+
if (opts.storageState) {
|
|
8121
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
8122
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
8123
|
+
if (statePath2) {
|
|
8124
|
+
const context = await browser.newContext({
|
|
8125
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
8126
|
+
userAgent: opts.userAgent,
|
|
8127
|
+
storageState: statePath2
|
|
8128
|
+
});
|
|
8129
|
+
page = await context.newPage();
|
|
8130
|
+
} else {
|
|
8131
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8132
|
+
}
|
|
8133
|
+
} else {
|
|
8134
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8135
|
+
}
|
|
7891
8136
|
}
|
|
7892
8137
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
7893
8138
|
try {
|
|
@@ -8003,6 +8248,66 @@ init_types();
|
|
|
8003
8248
|
var lastSnapshots = new Map;
|
|
8004
8249
|
var sessionRefMaps = new Map;
|
|
8005
8250
|
|
|
8251
|
+
// src/lib/self-heal.ts
|
|
8252
|
+
async function healSelector(page, selector, sessionId) {
|
|
8253
|
+
const attempts = [];
|
|
8254
|
+
attempts.push(`selector: ${selector}`);
|
|
8255
|
+
try {
|
|
8256
|
+
const loc = page.locator(selector).first();
|
|
8257
|
+
if (await loc.count() > 0) {
|
|
8258
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
8259
|
+
}
|
|
8260
|
+
} catch {}
|
|
8261
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
8262
|
+
attempts.push(`text: "${selector}"`);
|
|
8263
|
+
try {
|
|
8264
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
8265
|
+
if (await loc.count() > 0) {
|
|
8266
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
8267
|
+
}
|
|
8268
|
+
} catch {}
|
|
8269
|
+
}
|
|
8270
|
+
const roleMap = {
|
|
8271
|
+
button: ["button", "submit", "reset"],
|
|
8272
|
+
link: ["a"],
|
|
8273
|
+
input: ["input", "textarea"],
|
|
8274
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
8275
|
+
};
|
|
8276
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
8277
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
8278
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
8279
|
+
try {
|
|
8280
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
8281
|
+
if (await loc.count() > 0) {
|
|
8282
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
8283
|
+
}
|
|
8284
|
+
} catch {}
|
|
8285
|
+
}
|
|
8286
|
+
if (selector.startsWith("#")) {
|
|
8287
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8288
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
8289
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
8290
|
+
try {
|
|
8291
|
+
const loc = page.locator(partialSel).first();
|
|
8292
|
+
if (await loc.count() > 0) {
|
|
8293
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
8294
|
+
}
|
|
8295
|
+
} catch {}
|
|
8296
|
+
}
|
|
8297
|
+
if (selector.startsWith(".")) {
|
|
8298
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8299
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
8300
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
8301
|
+
try {
|
|
8302
|
+
const loc = page.locator(partialSel).first();
|
|
8303
|
+
if (await loc.count() > 0) {
|
|
8304
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
8305
|
+
}
|
|
8306
|
+
} catch {}
|
|
8307
|
+
}
|
|
8308
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
8309
|
+
}
|
|
8310
|
+
|
|
8006
8311
|
// src/lib/actions.ts
|
|
8007
8312
|
async function click(page, selector, opts) {
|
|
8008
8313
|
try {
|
|
@@ -8012,11 +8317,22 @@ async function click(page, selector, opts) {
|
|
|
8012
8317
|
delay: opts?.delay,
|
|
8013
8318
|
timeout: opts?.timeout ?? 1e4
|
|
8014
8319
|
});
|
|
8015
|
-
|
|
8016
|
-
|
|
8320
|
+
return {};
|
|
8321
|
+
} catch (originalError) {
|
|
8322
|
+
if (opts?.selfHeal !== false) {
|
|
8323
|
+
const result = await healSelector(page, selector);
|
|
8324
|
+
if (result.found && result.locator) {
|
|
8325
|
+
await result.locator.click({
|
|
8326
|
+
button: opts?.button ?? "left",
|
|
8327
|
+
timeout: opts?.timeout ?? 1e4
|
|
8328
|
+
});
|
|
8329
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8017
8333
|
throw new ElementNotFoundError(selector);
|
|
8018
8334
|
}
|
|
8019
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
8335
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
8020
8336
|
}
|
|
8021
8337
|
}
|
|
8022
8338
|
async function type(page, selector, text, opts) {
|
|
@@ -8025,11 +8341,21 @@ async function type(page, selector, text, opts) {
|
|
|
8025
8341
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
8026
8342
|
}
|
|
8027
8343
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8028
|
-
|
|
8029
|
-
|
|
8344
|
+
return {};
|
|
8345
|
+
} catch (originalError) {
|
|
8346
|
+
if (opts?.selfHeal !== false) {
|
|
8347
|
+
const result = await healSelector(page, selector);
|
|
8348
|
+
if (result.found && result.locator) {
|
|
8349
|
+
if (opts?.clear)
|
|
8350
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
8351
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8352
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8030
8356
|
throw new ElementNotFoundError(selector);
|
|
8031
8357
|
}
|
|
8032
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
8358
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
8033
8359
|
}
|
|
8034
8360
|
}
|
|
8035
8361
|
async function scroll(page, direction = "down", amount = 300) {
|
|
@@ -8129,9 +8455,9 @@ async function extract(page, opts = {}) {
|
|
|
8129
8455
|
// src/lib/screenshot.ts
|
|
8130
8456
|
init_types();
|
|
8131
8457
|
var import_sharp = __toESM(require_lib(), 1);
|
|
8132
|
-
import { join as
|
|
8133
|
-
import { mkdirSync as
|
|
8134
|
-
import { homedir as
|
|
8458
|
+
import { join as join4 } from "path";
|
|
8459
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
8460
|
+
import { homedir as homedir4 } from "os";
|
|
8135
8461
|
|
|
8136
8462
|
// src/db/gallery.ts
|
|
8137
8463
|
init_schema();
|
|
@@ -8261,13 +8587,13 @@ function getGalleryStats(projectId) {
|
|
|
8261
8587
|
|
|
8262
8588
|
// src/lib/screenshot.ts
|
|
8263
8589
|
function getDataDir2() {
|
|
8264
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
8590
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
8265
8591
|
}
|
|
8266
8592
|
function getScreenshotDir(projectId) {
|
|
8267
|
-
const base =
|
|
8593
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
8268
8594
|
const date = new Date().toISOString().split("T")[0];
|
|
8269
|
-
const dir = projectId ?
|
|
8270
|
-
|
|
8595
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
8596
|
+
mkdirSync4(dir, { recursive: true });
|
|
8271
8597
|
return dir;
|
|
8272
8598
|
}
|
|
8273
8599
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -8282,7 +8608,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
8282
8608
|
}
|
|
8283
8609
|
}
|
|
8284
8610
|
async function generateThumbnail(raw, dir, stem) {
|
|
8285
|
-
const thumbPath =
|
|
8611
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
8286
8612
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
8287
8613
|
await Bun.write(thumbPath, thumbBuffer);
|
|
8288
8614
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -8339,7 +8665,7 @@ async function takeScreenshot(page, opts) {
|
|
|
8339
8665
|
const compressedSizeBytes = finalBuffer.length;
|
|
8340
8666
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
8341
8667
|
const ext = format;
|
|
8342
|
-
const screenshotPath = opts?.path ??
|
|
8668
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
8343
8669
|
await Bun.write(screenshotPath, finalBuffer);
|
|
8344
8670
|
let thumbnailPath;
|
|
8345
8671
|
let thumbnailBase64;
|
|
@@ -8398,118 +8724,8 @@ async function takeScreenshot(page, opts) {
|
|
|
8398
8724
|
}
|
|
8399
8725
|
}
|
|
8400
8726
|
|
|
8401
|
-
// src/engines/cdp.ts
|
|
8402
|
-
init_types();
|
|
8403
|
-
|
|
8404
|
-
class CDPClient {
|
|
8405
|
-
session;
|
|
8406
|
-
networkEnabled = false;
|
|
8407
|
-
performanceEnabled = false;
|
|
8408
|
-
constructor(session) {
|
|
8409
|
-
this.session = session;
|
|
8410
|
-
}
|
|
8411
|
-
static async fromPage(page) {
|
|
8412
|
-
try {
|
|
8413
|
-
const session = await page.context().newCDPSession(page);
|
|
8414
|
-
return new CDPClient(session);
|
|
8415
|
-
} catch (err) {
|
|
8416
|
-
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
8417
|
-
}
|
|
8418
|
-
}
|
|
8419
|
-
async send(method, params) {
|
|
8420
|
-
try {
|
|
8421
|
-
return await this.session.send(method, params);
|
|
8422
|
-
} catch (err) {
|
|
8423
|
-
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
8424
|
-
}
|
|
8425
|
-
}
|
|
8426
|
-
on(event, handler) {
|
|
8427
|
-
this.session.on(event, handler);
|
|
8428
|
-
}
|
|
8429
|
-
off(event, handler) {
|
|
8430
|
-
this.session.off(event, handler);
|
|
8431
|
-
}
|
|
8432
|
-
async enableNetwork() {
|
|
8433
|
-
if (!this.networkEnabled) {
|
|
8434
|
-
await this.send("Network.enable");
|
|
8435
|
-
this.networkEnabled = true;
|
|
8436
|
-
}
|
|
8437
|
-
}
|
|
8438
|
-
async enablePerformance() {
|
|
8439
|
-
if (!this.performanceEnabled) {
|
|
8440
|
-
await this.send("Performance.enable");
|
|
8441
|
-
this.performanceEnabled = true;
|
|
8442
|
-
}
|
|
8443
|
-
}
|
|
8444
|
-
async getPerformanceMetrics() {
|
|
8445
|
-
await this.enablePerformance();
|
|
8446
|
-
const result = await this.send("Performance.getMetrics");
|
|
8447
|
-
const m = {};
|
|
8448
|
-
for (const metric of result.metrics) {
|
|
8449
|
-
m[metric.name] = metric.value;
|
|
8450
|
-
}
|
|
8451
|
-
return {
|
|
8452
|
-
js_heap_size_used: m["JSHeapUsedSize"],
|
|
8453
|
-
js_heap_size_total: m["JSHeapTotalSize"],
|
|
8454
|
-
dom_interactive: m["DOMInteractive"],
|
|
8455
|
-
dom_complete: m["DOMComplete"],
|
|
8456
|
-
load_event: m["LoadEventEnd"]
|
|
8457
|
-
};
|
|
8458
|
-
}
|
|
8459
|
-
async startJSCoverage() {
|
|
8460
|
-
await this.send("Profiler.enable");
|
|
8461
|
-
await this.send("Debugger.enable");
|
|
8462
|
-
await this.send("Profiler.startPreciseCoverage", {
|
|
8463
|
-
callCount: false,
|
|
8464
|
-
detailed: true
|
|
8465
|
-
});
|
|
8466
|
-
}
|
|
8467
|
-
async stopJSCoverage() {
|
|
8468
|
-
const result = await this.send("Profiler.takePreciseCoverage");
|
|
8469
|
-
await this.send("Profiler.stopPreciseCoverage");
|
|
8470
|
-
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
8471
|
-
url: r.url,
|
|
8472
|
-
text: "",
|
|
8473
|
-
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
8474
|
-
}));
|
|
8475
|
-
}
|
|
8476
|
-
async getCoverage() {
|
|
8477
|
-
await this.startJSCoverage();
|
|
8478
|
-
const js = await this.stopJSCoverage();
|
|
8479
|
-
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
8480
|
-
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
8481
|
-
}
|
|
8482
|
-
async captureHAREntries(page, handler) {
|
|
8483
|
-
await this.enableNetwork();
|
|
8484
|
-
const requestTimings = new Map;
|
|
8485
|
-
const onRequest = (params) => {
|
|
8486
|
-
requestTimings.set(params.requestId, params.timestamp);
|
|
8487
|
-
};
|
|
8488
|
-
const onResponse = (params) => {
|
|
8489
|
-
const start = requestTimings.get(params.requestId);
|
|
8490
|
-
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
8491
|
-
handler({
|
|
8492
|
-
method: "GET",
|
|
8493
|
-
url: params.response.url,
|
|
8494
|
-
status: params.response.status,
|
|
8495
|
-
duration
|
|
8496
|
-
});
|
|
8497
|
-
};
|
|
8498
|
-
this.on("Network.requestWillBeSent", onRequest);
|
|
8499
|
-
this.on("Network.responseReceived", onResponse);
|
|
8500
|
-
return () => {
|
|
8501
|
-
this.off("Network.requestWillBeSent", onRequest);
|
|
8502
|
-
this.off("Network.responseReceived", onResponse);
|
|
8503
|
-
};
|
|
8504
|
-
}
|
|
8505
|
-
async detach() {
|
|
8506
|
-
try {
|
|
8507
|
-
await this.session.detach();
|
|
8508
|
-
} catch {}
|
|
8509
|
-
}
|
|
8510
|
-
}
|
|
8511
|
-
|
|
8512
8727
|
// src/lib/performance.ts
|
|
8728
|
+
init_cdp();
|
|
8513
8729
|
async function getPerformanceMetrics(page) {
|
|
8514
8730
|
const navTiming = await page.evaluate(() => {
|
|
8515
8731
|
const t = performance.timing;
|
|
@@ -8742,16 +8958,16 @@ init_console_log();
|
|
|
8742
8958
|
init_recordings();
|
|
8743
8959
|
|
|
8744
8960
|
// src/lib/downloads.ts
|
|
8745
|
-
import { join as
|
|
8746
|
-
import { mkdirSync as
|
|
8747
|
-
import { homedir as
|
|
8961
|
+
import { join as join5, basename, extname } from "path";
|
|
8962
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
8963
|
+
import { homedir as homedir5 } from "os";
|
|
8748
8964
|
function getDataDir3() {
|
|
8749
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
8965
|
+
return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
8750
8966
|
}
|
|
8751
8967
|
function getDownloadsDir(sessionId) {
|
|
8752
|
-
const base =
|
|
8753
|
-
const dir = sessionId ?
|
|
8754
|
-
|
|
8968
|
+
const base = join5(getDataDir3(), "downloads");
|
|
8969
|
+
const dir = sessionId ? join5(base, sessionId) : base;
|
|
8970
|
+
mkdirSync5(dir, { recursive: true });
|
|
8755
8971
|
return dir;
|
|
8756
8972
|
}
|
|
8757
8973
|
function metaPath(filePath) {
|
|
@@ -8761,20 +8977,20 @@ function listDownloads(sessionId) {
|
|
|
8761
8977
|
const dir = getDownloadsDir(sessionId);
|
|
8762
8978
|
const results = [];
|
|
8763
8979
|
function scanDir(d) {
|
|
8764
|
-
if (!
|
|
8980
|
+
if (!existsSync2(d))
|
|
8765
8981
|
return;
|
|
8766
|
-
const entries =
|
|
8982
|
+
const entries = readdirSync2(d);
|
|
8767
8983
|
for (const entry of entries) {
|
|
8768
8984
|
if (entry.endsWith(".meta.json"))
|
|
8769
8985
|
continue;
|
|
8770
|
-
const full =
|
|
8986
|
+
const full = join5(d, entry);
|
|
8771
8987
|
const stat = statSync(full);
|
|
8772
8988
|
if (stat.isDirectory()) {
|
|
8773
8989
|
scanDir(full);
|
|
8774
8990
|
continue;
|
|
8775
8991
|
}
|
|
8776
8992
|
const mpath = metaPath(full);
|
|
8777
|
-
if (!
|
|
8993
|
+
if (!existsSync2(mpath))
|
|
8778
8994
|
continue;
|
|
8779
8995
|
try {
|
|
8780
8996
|
const meta = JSON.parse(readFileSync(mpath, "utf8"));
|
|
@@ -8804,9 +9020,9 @@ function deleteDownload(id, sessionId) {
|
|
|
8804
9020
|
if (!file)
|
|
8805
9021
|
return false;
|
|
8806
9022
|
try {
|
|
8807
|
-
|
|
8808
|
-
if (
|
|
8809
|
-
|
|
9023
|
+
unlinkSync2(file.path);
|
|
9024
|
+
if (existsSync2(file.meta_path))
|
|
9025
|
+
unlinkSync2(file.meta_path);
|
|
8810
9026
|
return true;
|
|
8811
9027
|
} catch {
|
|
8812
9028
|
return false;
|
|
@@ -8828,9 +9044,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
|
|
|
8828
9044
|
|
|
8829
9045
|
// src/lib/gallery-diff.ts
|
|
8830
9046
|
var import_sharp2 = __toESM(require_lib(), 1);
|
|
8831
|
-
import { join as
|
|
8832
|
-
import { mkdirSync as
|
|
8833
|
-
import { homedir as
|
|
9047
|
+
import { join as join6 } from "path";
|
|
9048
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
9049
|
+
import { homedir as homedir6 } from "os";
|
|
8834
9050
|
async function diffImages(path1, path2) {
|
|
8835
9051
|
const img1 = import_sharp2.default(path1);
|
|
8836
9052
|
const img2 = import_sharp2.default(path2);
|
|
@@ -8861,10 +9077,10 @@ async function diffImages(path1, path2) {
|
|
|
8861
9077
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
8862
9078
|
}
|
|
8863
9079
|
}
|
|
8864
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
8865
|
-
const diffDir =
|
|
8866
|
-
|
|
8867
|
-
const diffPath =
|
|
9080
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
9081
|
+
const diffDir = join6(dataDir, "diffs");
|
|
9082
|
+
mkdirSync6(diffDir, { recursive: true });
|
|
9083
|
+
const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
|
|
8868
9084
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
8869
9085
|
await Bun.write(diffPath, diffImageBuffer);
|
|
8870
9086
|
return {
|
|
@@ -9108,14 +9324,14 @@ var server = Bun.serve({
|
|
|
9108
9324
|
if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
|
|
9109
9325
|
const id = path.split("/")[3];
|
|
9110
9326
|
const entry = getEntry(id);
|
|
9111
|
-
if (!entry?.thumbnail_path || !
|
|
9327
|
+
if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
|
|
9112
9328
|
return notFound("Thumbnail not found");
|
|
9113
9329
|
return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
|
|
9114
9330
|
}
|
|
9115
9331
|
if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
|
|
9116
9332
|
const id = path.split("/")[3];
|
|
9117
9333
|
const entry = getEntry(id);
|
|
9118
|
-
if (!entry?.path || !
|
|
9334
|
+
if (!entry?.path || !existsSync4(entry.path))
|
|
9119
9335
|
return notFound("Image not found");
|
|
9120
9336
|
return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
|
|
9121
9337
|
}
|
|
@@ -9143,7 +9359,7 @@ var server = Bun.serve({
|
|
|
9143
9359
|
if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
|
|
9144
9360
|
const id = path.split("/")[3];
|
|
9145
9361
|
const file = getDownload(id);
|
|
9146
|
-
if (!file || !
|
|
9362
|
+
if (!file || !existsSync4(file.path))
|
|
9147
9363
|
return notFound("Download not found");
|
|
9148
9364
|
return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
|
|
9149
9365
|
}
|
|
@@ -9151,13 +9367,13 @@ var server = Bun.serve({
|
|
|
9151
9367
|
const id = path.split("/")[3];
|
|
9152
9368
|
return ok({ deleted: deleteDownload(id) });
|
|
9153
9369
|
}
|
|
9154
|
-
const dashboardDist =
|
|
9155
|
-
if (
|
|
9156
|
-
const filePath = path === "/" ?
|
|
9157
|
-
if (
|
|
9370
|
+
const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
|
|
9371
|
+
if (existsSync4(dashboardDist)) {
|
|
9372
|
+
const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
|
|
9373
|
+
if (existsSync4(filePath)) {
|
|
9158
9374
|
return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
|
|
9159
9375
|
}
|
|
9160
|
-
return new Response(Bun.file(
|
|
9376
|
+
return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
|
|
9161
9377
|
}
|
|
9162
9378
|
if (path === "/" || path === "") {
|
|
9163
9379
|
return new Response("@hasna/browser REST API running. Dashboard not built.", {
|