@hasna/browser 0.0.9 → 0.1.1
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 +2090 -364
- package/dist/engines/cdp.d.ts +2 -1
- package/dist/engines/cdp.d.ts.map +1 -1
- package/dist/index.js +545 -214
- package/dist/lib/actions.d.ts +22 -4
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/api-detector.d.ts +17 -0
- package/dist/lib/api-detector.d.ts.map +1 -0
- package/dist/lib/auth-flow.d.ts +43 -0
- package/dist/lib/auth-flow.d.ts.map +1 -0
- package/dist/lib/datasets.d.ts +33 -0
- package/dist/lib/datasets.d.ts.map +1 -0
- package/dist/lib/deep-performance.d.ts +49 -0
- package/dist/lib/deep-performance.d.ts.map +1 -0
- package/dist/lib/env-detector.d.ts +12 -0
- package/dist/lib/env-detector.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/structured-extract.d.ts +26 -0
- package/dist/lib/structured-extract.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/lib/workflows.d.ts +46 -0
- package/dist/lib/workflows.d.ts.map +1 -0
- package/dist/mcp/index.js +2196 -485
- package/dist/server/index.js +422 -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,71 @@ 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
|
+
`
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
version: 7,
|
|
314
|
+
sql: `
|
|
315
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
316
|
+
id TEXT PRIMARY KEY,
|
|
317
|
+
name TEXT NOT NULL UNIQUE,
|
|
318
|
+
description TEXT,
|
|
319
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
320
|
+
start_url TEXT,
|
|
321
|
+
last_run TEXT,
|
|
322
|
+
last_heal TEXT,
|
|
323
|
+
heal_count INTEGER DEFAULT 0,
|
|
324
|
+
run_count INTEGER DEFAULT 0,
|
|
325
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
326
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
327
|
+
);
|
|
328
|
+
`
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
version: 8,
|
|
332
|
+
sql: `
|
|
333
|
+
CREATE TABLE IF NOT EXISTS datasets (
|
|
334
|
+
id TEXT PRIMARY KEY,
|
|
335
|
+
name TEXT NOT NULL UNIQUE,
|
|
336
|
+
source_url TEXT,
|
|
337
|
+
source_type TEXT NOT NULL DEFAULT 'page',
|
|
338
|
+
data TEXT NOT NULL DEFAULT '[]',
|
|
339
|
+
schema TEXT,
|
|
340
|
+
row_count INTEGER DEFAULT 0,
|
|
341
|
+
last_refresh TEXT,
|
|
342
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
343
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
CREATE TABLE IF NOT EXISTS api_endpoints (
|
|
347
|
+
id TEXT PRIMARY KEY,
|
|
348
|
+
session_id TEXT,
|
|
349
|
+
url TEXT NOT NULL,
|
|
350
|
+
method TEXT DEFAULT 'GET',
|
|
351
|
+
response_schema TEXT,
|
|
352
|
+
sample_response TEXT,
|
|
353
|
+
status_code INTEGER,
|
|
354
|
+
content_type TEXT,
|
|
355
|
+
discovered_at TEXT DEFAULT (datetime('now'))
|
|
356
|
+
);
|
|
357
|
+
CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
|
|
358
|
+
`
|
|
294
359
|
}
|
|
295
360
|
];
|
|
296
361
|
for (const m of migrations) {
|
|
@@ -339,6 +404,188 @@ var init_console_log = __esm(() => {
|
|
|
339
404
|
init_schema();
|
|
340
405
|
});
|
|
341
406
|
|
|
407
|
+
// src/engines/cdp.ts
|
|
408
|
+
var exports_cdp = {};
|
|
409
|
+
__export(exports_cdp, {
|
|
410
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
411
|
+
CDPClient: () => CDPClient
|
|
412
|
+
});
|
|
413
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
414
|
+
const { chromium: chromium3 } = await import("playwright");
|
|
415
|
+
try {
|
|
416
|
+
return await chromium3.connectOverCDP(cdpUrl);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
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);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
class CDPClient {
|
|
423
|
+
session;
|
|
424
|
+
networkEnabled = false;
|
|
425
|
+
performanceEnabled = false;
|
|
426
|
+
constructor(session) {
|
|
427
|
+
this.session = session;
|
|
428
|
+
}
|
|
429
|
+
static async fromPage(page) {
|
|
430
|
+
try {
|
|
431
|
+
const session = await page.context().newCDPSession(page);
|
|
432
|
+
return new CDPClient(session);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async send(method, params) {
|
|
438
|
+
try {
|
|
439
|
+
return await this.session.send(method, params);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
on(event, handler) {
|
|
445
|
+
this.session.on(event, handler);
|
|
446
|
+
}
|
|
447
|
+
off(event, handler) {
|
|
448
|
+
this.session.off(event, handler);
|
|
449
|
+
}
|
|
450
|
+
async enableNetwork() {
|
|
451
|
+
if (!this.networkEnabled) {
|
|
452
|
+
await this.send("Network.enable");
|
|
453
|
+
this.networkEnabled = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async enablePerformance() {
|
|
457
|
+
if (!this.performanceEnabled) {
|
|
458
|
+
await this.send("Performance.enable");
|
|
459
|
+
this.performanceEnabled = true;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async getPerformanceMetrics() {
|
|
463
|
+
await this.enablePerformance();
|
|
464
|
+
const result = await this.send("Performance.getMetrics");
|
|
465
|
+
const m = {};
|
|
466
|
+
for (const metric of result.metrics) {
|
|
467
|
+
m[metric.name] = metric.value;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
471
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
472
|
+
dom_interactive: m["DOMInteractive"],
|
|
473
|
+
dom_complete: m["DOMComplete"],
|
|
474
|
+
load_event: m["LoadEventEnd"]
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
async startJSCoverage() {
|
|
478
|
+
await this.send("Profiler.enable");
|
|
479
|
+
await this.send("Debugger.enable");
|
|
480
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
481
|
+
callCount: false,
|
|
482
|
+
detailed: true
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async stopJSCoverage() {
|
|
486
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
487
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
488
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
489
|
+
url: r.url,
|
|
490
|
+
text: "",
|
|
491
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
async getCoverage() {
|
|
495
|
+
await this.startJSCoverage();
|
|
496
|
+
const js = await this.stopJSCoverage();
|
|
497
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
498
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
499
|
+
}
|
|
500
|
+
async captureHAREntries(page, handler) {
|
|
501
|
+
await this.enableNetwork();
|
|
502
|
+
const requestTimings = new Map;
|
|
503
|
+
const onRequest = (params) => {
|
|
504
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
505
|
+
};
|
|
506
|
+
const onResponse = (params) => {
|
|
507
|
+
const start = requestTimings.get(params.requestId);
|
|
508
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
509
|
+
handler({
|
|
510
|
+
method: "GET",
|
|
511
|
+
url: params.response.url,
|
|
512
|
+
status: params.response.status,
|
|
513
|
+
duration
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
517
|
+
this.on("Network.responseReceived", onResponse);
|
|
518
|
+
return () => {
|
|
519
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
520
|
+
this.off("Network.responseReceived", onResponse);
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async detach() {
|
|
524
|
+
try {
|
|
525
|
+
await this.session.detach();
|
|
526
|
+
} catch {}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
var init_cdp = __esm(() => {
|
|
530
|
+
init_types();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// src/lib/storage-state.ts
|
|
534
|
+
var exports_storage_state = {};
|
|
535
|
+
__export(exports_storage_state, {
|
|
536
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
537
|
+
saveState: () => saveState,
|
|
538
|
+
loadStatePath: () => loadStatePath,
|
|
539
|
+
listStates: () => listStates,
|
|
540
|
+
deleteState: () => deleteState
|
|
541
|
+
});
|
|
542
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
543
|
+
import { join as join3 } from "path";
|
|
544
|
+
import { homedir as homedir3 } from "os";
|
|
545
|
+
function ensureDir() {
|
|
546
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
547
|
+
}
|
|
548
|
+
function statePath(name) {
|
|
549
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
550
|
+
}
|
|
551
|
+
async function saveState(context, name) {
|
|
552
|
+
ensureDir();
|
|
553
|
+
const path = statePath(name);
|
|
554
|
+
const state = await context.storageState({ path });
|
|
555
|
+
return path;
|
|
556
|
+
}
|
|
557
|
+
async function saveStateFromPage(page, name) {
|
|
558
|
+
return saveState(page.context(), name);
|
|
559
|
+
}
|
|
560
|
+
function loadStatePath(name) {
|
|
561
|
+
const path = statePath(name);
|
|
562
|
+
return existsSync(path) ? path : null;
|
|
563
|
+
}
|
|
564
|
+
function listStates() {
|
|
565
|
+
ensureDir();
|
|
566
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
567
|
+
const path = join3(STATES_DIR, f);
|
|
568
|
+
const stat = Bun.file(path);
|
|
569
|
+
return {
|
|
570
|
+
name: f.replace(".json", ""),
|
|
571
|
+
path,
|
|
572
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
573
|
+
};
|
|
574
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
575
|
+
}
|
|
576
|
+
function deleteState(name) {
|
|
577
|
+
const path = statePath(name);
|
|
578
|
+
if (existsSync(path)) {
|
|
579
|
+
unlinkSync(path);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
var STATES_DIR;
|
|
585
|
+
var init_storage_state = __esm(() => {
|
|
586
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
587
|
+
});
|
|
588
|
+
|
|
342
589
|
// node_modules/sharp/lib/is.js
|
|
343
590
|
var require_is = __commonJS((exports, module) => {
|
|
344
591
|
/*!
|
|
@@ -6933,8 +7180,8 @@ var init_snapshots = __esm(() => {
|
|
|
6933
7180
|
});
|
|
6934
7181
|
|
|
6935
7182
|
// src/server/index.ts
|
|
6936
|
-
import { join as
|
|
6937
|
-
import { existsSync as
|
|
7183
|
+
import { join as join7 } from "path";
|
|
7184
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6938
7185
|
|
|
6939
7186
|
// src/lib/session.ts
|
|
6940
7187
|
init_types();
|
|
@@ -7862,6 +8109,37 @@ function createBunProxy(view) {
|
|
|
7862
8109
|
return view;
|
|
7863
8110
|
}
|
|
7864
8111
|
async function createSession2(opts = {}) {
|
|
8112
|
+
if (opts.cdpUrl) {
|
|
8113
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
8114
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
8115
|
+
const contexts = cdpBrowser.contexts();
|
|
8116
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
8117
|
+
const pages = context.pages();
|
|
8118
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
8119
|
+
const session2 = createSession({
|
|
8120
|
+
engine: "cdp",
|
|
8121
|
+
projectId: opts.projectId,
|
|
8122
|
+
agentId: opts.agentId,
|
|
8123
|
+
startUrl: page2.url(),
|
|
8124
|
+
name: opts.name ?? "attached"
|
|
8125
|
+
});
|
|
8126
|
+
const cleanups2 = [];
|
|
8127
|
+
if (opts.captureNetwork !== false) {
|
|
8128
|
+
try {
|
|
8129
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
8130
|
+
} catch {}
|
|
8131
|
+
}
|
|
8132
|
+
if (opts.captureConsole !== false) {
|
|
8133
|
+
try {
|
|
8134
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
8135
|
+
} catch {}
|
|
8136
|
+
}
|
|
8137
|
+
try {
|
|
8138
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
8139
|
+
} catch {}
|
|
8140
|
+
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 });
|
|
8141
|
+
return { session: session2, page: page2 };
|
|
8142
|
+
}
|
|
7865
8143
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
7866
8144
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
7867
8145
|
let browser = null;
|
|
@@ -7887,7 +8165,22 @@ async function createSession2(opts = {}) {
|
|
|
7887
8165
|
page = await context.newPage();
|
|
7888
8166
|
} else {
|
|
7889
8167
|
browser = await pool.acquire(opts.headless ?? true);
|
|
7890
|
-
|
|
8168
|
+
if (opts.storageState) {
|
|
8169
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
8170
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
8171
|
+
if (statePath2) {
|
|
8172
|
+
const context = await browser.newContext({
|
|
8173
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
8174
|
+
userAgent: opts.userAgent,
|
|
8175
|
+
storageState: statePath2
|
|
8176
|
+
});
|
|
8177
|
+
page = await context.newPage();
|
|
8178
|
+
} else {
|
|
8179
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8180
|
+
}
|
|
8181
|
+
} else {
|
|
8182
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8183
|
+
}
|
|
7891
8184
|
}
|
|
7892
8185
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
7893
8186
|
try {
|
|
@@ -8003,6 +8296,66 @@ init_types();
|
|
|
8003
8296
|
var lastSnapshots = new Map;
|
|
8004
8297
|
var sessionRefMaps = new Map;
|
|
8005
8298
|
|
|
8299
|
+
// src/lib/self-heal.ts
|
|
8300
|
+
async function healSelector(page, selector, sessionId) {
|
|
8301
|
+
const attempts = [];
|
|
8302
|
+
attempts.push(`selector: ${selector}`);
|
|
8303
|
+
try {
|
|
8304
|
+
const loc = page.locator(selector).first();
|
|
8305
|
+
if (await loc.count() > 0) {
|
|
8306
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
8307
|
+
}
|
|
8308
|
+
} catch {}
|
|
8309
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
8310
|
+
attempts.push(`text: "${selector}"`);
|
|
8311
|
+
try {
|
|
8312
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
8313
|
+
if (await loc.count() > 0) {
|
|
8314
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
8315
|
+
}
|
|
8316
|
+
} catch {}
|
|
8317
|
+
}
|
|
8318
|
+
const roleMap = {
|
|
8319
|
+
button: ["button", "submit", "reset"],
|
|
8320
|
+
link: ["a"],
|
|
8321
|
+
input: ["input", "textarea"],
|
|
8322
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
8323
|
+
};
|
|
8324
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
8325
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
8326
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
8327
|
+
try {
|
|
8328
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
8329
|
+
if (await loc.count() > 0) {
|
|
8330
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
8331
|
+
}
|
|
8332
|
+
} catch {}
|
|
8333
|
+
}
|
|
8334
|
+
if (selector.startsWith("#")) {
|
|
8335
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8336
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
8337
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
8338
|
+
try {
|
|
8339
|
+
const loc = page.locator(partialSel).first();
|
|
8340
|
+
if (await loc.count() > 0) {
|
|
8341
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
8342
|
+
}
|
|
8343
|
+
} catch {}
|
|
8344
|
+
}
|
|
8345
|
+
if (selector.startsWith(".")) {
|
|
8346
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8347
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
8348
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
8349
|
+
try {
|
|
8350
|
+
const loc = page.locator(partialSel).first();
|
|
8351
|
+
if (await loc.count() > 0) {
|
|
8352
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
8353
|
+
}
|
|
8354
|
+
} catch {}
|
|
8355
|
+
}
|
|
8356
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
8357
|
+
}
|
|
8358
|
+
|
|
8006
8359
|
// src/lib/actions.ts
|
|
8007
8360
|
async function click(page, selector, opts) {
|
|
8008
8361
|
try {
|
|
@@ -8012,11 +8365,22 @@ async function click(page, selector, opts) {
|
|
|
8012
8365
|
delay: opts?.delay,
|
|
8013
8366
|
timeout: opts?.timeout ?? 1e4
|
|
8014
8367
|
});
|
|
8015
|
-
|
|
8016
|
-
|
|
8368
|
+
return {};
|
|
8369
|
+
} catch (originalError) {
|
|
8370
|
+
if (opts?.selfHeal !== false) {
|
|
8371
|
+
const result = await healSelector(page, selector);
|
|
8372
|
+
if (result.found && result.locator) {
|
|
8373
|
+
await result.locator.click({
|
|
8374
|
+
button: opts?.button ?? "left",
|
|
8375
|
+
timeout: opts?.timeout ?? 1e4
|
|
8376
|
+
});
|
|
8377
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8017
8381
|
throw new ElementNotFoundError(selector);
|
|
8018
8382
|
}
|
|
8019
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
8383
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
8020
8384
|
}
|
|
8021
8385
|
}
|
|
8022
8386
|
async function type(page, selector, text, opts) {
|
|
@@ -8025,11 +8389,21 @@ async function type(page, selector, text, opts) {
|
|
|
8025
8389
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
8026
8390
|
}
|
|
8027
8391
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8028
|
-
|
|
8029
|
-
|
|
8392
|
+
return {};
|
|
8393
|
+
} catch (originalError) {
|
|
8394
|
+
if (opts?.selfHeal !== false) {
|
|
8395
|
+
const result = await healSelector(page, selector);
|
|
8396
|
+
if (result.found && result.locator) {
|
|
8397
|
+
if (opts?.clear)
|
|
8398
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
8399
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8400
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8030
8404
|
throw new ElementNotFoundError(selector);
|
|
8031
8405
|
}
|
|
8032
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
8406
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
8033
8407
|
}
|
|
8034
8408
|
}
|
|
8035
8409
|
async function scroll(page, direction = "down", amount = 300) {
|
|
@@ -8129,9 +8503,9 @@ async function extract(page, opts = {}) {
|
|
|
8129
8503
|
// src/lib/screenshot.ts
|
|
8130
8504
|
init_types();
|
|
8131
8505
|
var import_sharp = __toESM(require_lib(), 1);
|
|
8132
|
-
import { join as
|
|
8133
|
-
import { mkdirSync as
|
|
8134
|
-
import { homedir as
|
|
8506
|
+
import { join as join4 } from "path";
|
|
8507
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
8508
|
+
import { homedir as homedir4 } from "os";
|
|
8135
8509
|
|
|
8136
8510
|
// src/db/gallery.ts
|
|
8137
8511
|
init_schema();
|
|
@@ -8261,13 +8635,13 @@ function getGalleryStats(projectId) {
|
|
|
8261
8635
|
|
|
8262
8636
|
// src/lib/screenshot.ts
|
|
8263
8637
|
function getDataDir2() {
|
|
8264
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
8638
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
8265
8639
|
}
|
|
8266
8640
|
function getScreenshotDir(projectId) {
|
|
8267
|
-
const base =
|
|
8641
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
8268
8642
|
const date = new Date().toISOString().split("T")[0];
|
|
8269
|
-
const dir = projectId ?
|
|
8270
|
-
|
|
8643
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
8644
|
+
mkdirSync4(dir, { recursive: true });
|
|
8271
8645
|
return dir;
|
|
8272
8646
|
}
|
|
8273
8647
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -8282,7 +8656,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
8282
8656
|
}
|
|
8283
8657
|
}
|
|
8284
8658
|
async function generateThumbnail(raw, dir, stem) {
|
|
8285
|
-
const thumbPath =
|
|
8659
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
8286
8660
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
8287
8661
|
await Bun.write(thumbPath, thumbBuffer);
|
|
8288
8662
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -8339,7 +8713,7 @@ async function takeScreenshot(page, opts) {
|
|
|
8339
8713
|
const compressedSizeBytes = finalBuffer.length;
|
|
8340
8714
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
8341
8715
|
const ext = format;
|
|
8342
|
-
const screenshotPath = opts?.path ??
|
|
8716
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
8343
8717
|
await Bun.write(screenshotPath, finalBuffer);
|
|
8344
8718
|
let thumbnailPath;
|
|
8345
8719
|
let thumbnailBase64;
|
|
@@ -8398,118 +8772,8 @@ async function takeScreenshot(page, opts) {
|
|
|
8398
8772
|
}
|
|
8399
8773
|
}
|
|
8400
8774
|
|
|
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
8775
|
// src/lib/performance.ts
|
|
8776
|
+
init_cdp();
|
|
8513
8777
|
async function getPerformanceMetrics(page) {
|
|
8514
8778
|
const navTiming = await page.evaluate(() => {
|
|
8515
8779
|
const t = performance.timing;
|
|
@@ -8742,16 +9006,16 @@ init_console_log();
|
|
|
8742
9006
|
init_recordings();
|
|
8743
9007
|
|
|
8744
9008
|
// src/lib/downloads.ts
|
|
8745
|
-
import { join as
|
|
8746
|
-
import { mkdirSync as
|
|
8747
|
-
import { homedir as
|
|
9009
|
+
import { join as join5, basename, extname } from "path";
|
|
9010
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
9011
|
+
import { homedir as homedir5 } from "os";
|
|
8748
9012
|
function getDataDir3() {
|
|
8749
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
9013
|
+
return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
8750
9014
|
}
|
|
8751
9015
|
function getDownloadsDir(sessionId) {
|
|
8752
|
-
const base =
|
|
8753
|
-
const dir = sessionId ?
|
|
8754
|
-
|
|
9016
|
+
const base = join5(getDataDir3(), "downloads");
|
|
9017
|
+
const dir = sessionId ? join5(base, sessionId) : base;
|
|
9018
|
+
mkdirSync5(dir, { recursive: true });
|
|
8755
9019
|
return dir;
|
|
8756
9020
|
}
|
|
8757
9021
|
function metaPath(filePath) {
|
|
@@ -8761,20 +9025,20 @@ function listDownloads(sessionId) {
|
|
|
8761
9025
|
const dir = getDownloadsDir(sessionId);
|
|
8762
9026
|
const results = [];
|
|
8763
9027
|
function scanDir(d) {
|
|
8764
|
-
if (!
|
|
9028
|
+
if (!existsSync2(d))
|
|
8765
9029
|
return;
|
|
8766
|
-
const entries =
|
|
9030
|
+
const entries = readdirSync2(d);
|
|
8767
9031
|
for (const entry of entries) {
|
|
8768
9032
|
if (entry.endsWith(".meta.json"))
|
|
8769
9033
|
continue;
|
|
8770
|
-
const full =
|
|
9034
|
+
const full = join5(d, entry);
|
|
8771
9035
|
const stat = statSync(full);
|
|
8772
9036
|
if (stat.isDirectory()) {
|
|
8773
9037
|
scanDir(full);
|
|
8774
9038
|
continue;
|
|
8775
9039
|
}
|
|
8776
9040
|
const mpath = metaPath(full);
|
|
8777
|
-
if (!
|
|
9041
|
+
if (!existsSync2(mpath))
|
|
8778
9042
|
continue;
|
|
8779
9043
|
try {
|
|
8780
9044
|
const meta = JSON.parse(readFileSync(mpath, "utf8"));
|
|
@@ -8804,9 +9068,9 @@ function deleteDownload(id, sessionId) {
|
|
|
8804
9068
|
if (!file)
|
|
8805
9069
|
return false;
|
|
8806
9070
|
try {
|
|
8807
|
-
|
|
8808
|
-
if (
|
|
8809
|
-
|
|
9071
|
+
unlinkSync2(file.path);
|
|
9072
|
+
if (existsSync2(file.meta_path))
|
|
9073
|
+
unlinkSync2(file.meta_path);
|
|
8810
9074
|
return true;
|
|
8811
9075
|
} catch {
|
|
8812
9076
|
return false;
|
|
@@ -8828,9 +9092,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
|
|
|
8828
9092
|
|
|
8829
9093
|
// src/lib/gallery-diff.ts
|
|
8830
9094
|
var import_sharp2 = __toESM(require_lib(), 1);
|
|
8831
|
-
import { join as
|
|
8832
|
-
import { mkdirSync as
|
|
8833
|
-
import { homedir as
|
|
9095
|
+
import { join as join6 } from "path";
|
|
9096
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
9097
|
+
import { homedir as homedir6 } from "os";
|
|
8834
9098
|
async function diffImages(path1, path2) {
|
|
8835
9099
|
const img1 = import_sharp2.default(path1);
|
|
8836
9100
|
const img2 = import_sharp2.default(path2);
|
|
@@ -8861,10 +9125,10 @@ async function diffImages(path1, path2) {
|
|
|
8861
9125
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
8862
9126
|
}
|
|
8863
9127
|
}
|
|
8864
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
8865
|
-
const diffDir =
|
|
8866
|
-
|
|
8867
|
-
const diffPath =
|
|
9128
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
9129
|
+
const diffDir = join6(dataDir, "diffs");
|
|
9130
|
+
mkdirSync6(diffDir, { recursive: true });
|
|
9131
|
+
const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
|
|
8868
9132
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
8869
9133
|
await Bun.write(diffPath, diffImageBuffer);
|
|
8870
9134
|
return {
|
|
@@ -9108,14 +9372,14 @@ var server = Bun.serve({
|
|
|
9108
9372
|
if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
|
|
9109
9373
|
const id = path.split("/")[3];
|
|
9110
9374
|
const entry = getEntry(id);
|
|
9111
|
-
if (!entry?.thumbnail_path || !
|
|
9375
|
+
if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
|
|
9112
9376
|
return notFound("Thumbnail not found");
|
|
9113
9377
|
return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
|
|
9114
9378
|
}
|
|
9115
9379
|
if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
|
|
9116
9380
|
const id = path.split("/")[3];
|
|
9117
9381
|
const entry = getEntry(id);
|
|
9118
|
-
if (!entry?.path || !
|
|
9382
|
+
if (!entry?.path || !existsSync4(entry.path))
|
|
9119
9383
|
return notFound("Image not found");
|
|
9120
9384
|
return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
|
|
9121
9385
|
}
|
|
@@ -9143,7 +9407,7 @@ var server = Bun.serve({
|
|
|
9143
9407
|
if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
|
|
9144
9408
|
const id = path.split("/")[3];
|
|
9145
9409
|
const file = getDownload(id);
|
|
9146
|
-
if (!file || !
|
|
9410
|
+
if (!file || !existsSync4(file.path))
|
|
9147
9411
|
return notFound("Download not found");
|
|
9148
9412
|
return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
|
|
9149
9413
|
}
|
|
@@ -9151,13 +9415,13 @@ var server = Bun.serve({
|
|
|
9151
9415
|
const id = path.split("/")[3];
|
|
9152
9416
|
return ok({ deleted: deleteDownload(id) });
|
|
9153
9417
|
}
|
|
9154
|
-
const dashboardDist =
|
|
9155
|
-
if (
|
|
9156
|
-
const filePath = path === "/" ?
|
|
9157
|
-
if (
|
|
9418
|
+
const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
|
|
9419
|
+
if (existsSync4(dashboardDist)) {
|
|
9420
|
+
const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
|
|
9421
|
+
if (existsSync4(filePath)) {
|
|
9158
9422
|
return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
|
|
9159
9423
|
}
|
|
9160
|
-
return new Response(Bun.file(
|
|
9424
|
+
return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
|
|
9161
9425
|
}
|
|
9162
9426
|
if (path === "/" || path === "") {
|
|
9163
9427
|
return new Response("@hasna/browser REST API running. Dashboard not built.", {
|