@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/index.js
CHANGED
|
@@ -28,6 +28,79 @@ var __export = (target, all) => {
|
|
|
28
28
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
29
29
|
var __require = import.meta.require;
|
|
30
30
|
|
|
31
|
+
// src/types/index.ts
|
|
32
|
+
var UseCase, BrowserError, SessionNotFoundError, EngineNotAvailableError, NavigationError, ElementNotFoundError, RecordingNotFoundError, AgentNotFoundError, ProjectNotFoundError;
|
|
33
|
+
var init_types = __esm(() => {
|
|
34
|
+
((UseCase2) => {
|
|
35
|
+
UseCase2["SCRAPE"] = "scrape";
|
|
36
|
+
UseCase2["EXTRACT_LINKS"] = "extract_links";
|
|
37
|
+
UseCase2["STATUS_CHECK"] = "status_check";
|
|
38
|
+
UseCase2["FORM_FILL"] = "form_fill";
|
|
39
|
+
UseCase2["SPA_NAVIGATE"] = "spa_navigate";
|
|
40
|
+
UseCase2["SCREENSHOT"] = "screenshot";
|
|
41
|
+
UseCase2["AUTH_FLOW"] = "auth_flow";
|
|
42
|
+
UseCase2["MULTI_TAB"] = "multi_tab";
|
|
43
|
+
UseCase2["NETWORK_MONITOR"] = "network_monitor";
|
|
44
|
+
UseCase2["HAR_CAPTURE"] = "har_capture";
|
|
45
|
+
UseCase2["PERF_PROFILE"] = "perf_profile";
|
|
46
|
+
UseCase2["SCRIPT_INJECT"] = "script_inject";
|
|
47
|
+
UseCase2["COVERAGE"] = "coverage";
|
|
48
|
+
UseCase2["RECORD_REPLAY"] = "record_replay";
|
|
49
|
+
})(UseCase ||= {});
|
|
50
|
+
BrowserError = class BrowserError extends Error {
|
|
51
|
+
code;
|
|
52
|
+
retryable;
|
|
53
|
+
constructor(message, code = "BROWSER_ERROR", retryable = false) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.retryable = retryable;
|
|
57
|
+
this.name = "BrowserError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
SessionNotFoundError = class SessionNotFoundError extends BrowserError {
|
|
61
|
+
constructor(id) {
|
|
62
|
+
super(`Session not found: ${id}`, "SESSION_NOT_FOUND", false);
|
|
63
|
+
this.name = "SessionNotFoundError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
EngineNotAvailableError = class EngineNotAvailableError extends BrowserError {
|
|
67
|
+
constructor(engine, reason) {
|
|
68
|
+
super(`Engine '${engine}' is not available${reason ? `: ${reason}` : ""}`, "ENGINE_NOT_AVAILABLE", false);
|
|
69
|
+
this.name = "EngineNotAvailableError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
NavigationError = class NavigationError extends BrowserError {
|
|
73
|
+
constructor(url, reason) {
|
|
74
|
+
super(`Navigation to '${url}' failed${reason ? `: ${reason}` : ""}`, "NAVIGATION_ERROR", true);
|
|
75
|
+
this.name = "NavigationError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
ElementNotFoundError = class ElementNotFoundError extends BrowserError {
|
|
79
|
+
constructor(selector) {
|
|
80
|
+
super(`Element not found: ${selector}`, "ELEMENT_NOT_FOUND", false);
|
|
81
|
+
this.name = "ElementNotFoundError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
RecordingNotFoundError = class RecordingNotFoundError extends BrowserError {
|
|
85
|
+
constructor(id) {
|
|
86
|
+
super(`Recording not found: ${id}`, "RECORDING_NOT_FOUND", false);
|
|
87
|
+
this.name = "RecordingNotFoundError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
AgentNotFoundError = class AgentNotFoundError extends BrowserError {
|
|
91
|
+
constructor(id) {
|
|
92
|
+
super(`Agent not found: ${id}`, "AGENT_NOT_FOUND", false);
|
|
93
|
+
this.name = "AgentNotFoundError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
ProjectNotFoundError = class ProjectNotFoundError extends BrowserError {
|
|
97
|
+
constructor(id) {
|
|
98
|
+
super(`Project not found: ${id}`, "PROJECT_NOT_FOUND", false);
|
|
99
|
+
this.name = "ProjectNotFoundError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
31
104
|
// src/db/schema.ts
|
|
32
105
|
import { Database } from "bun:sqlite";
|
|
33
106
|
import { join } from "path";
|
|
@@ -243,6 +316,23 @@ function runMigrations(db) {
|
|
|
243
316
|
);
|
|
244
317
|
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
|
|
245
318
|
`
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
version: 6,
|
|
322
|
+
sql: `
|
|
323
|
+
CREATE TABLE IF NOT EXISTS auth_flows (
|
|
324
|
+
id TEXT PRIMARY KEY,
|
|
325
|
+
name TEXT NOT NULL UNIQUE,
|
|
326
|
+
domain TEXT NOT NULL,
|
|
327
|
+
recording_id TEXT REFERENCES recordings(id),
|
|
328
|
+
storage_state_path TEXT,
|
|
329
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
330
|
+
last_used TEXT
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
|
|
335
|
+
`
|
|
246
336
|
}
|
|
247
337
|
];
|
|
248
338
|
for (const m of migrations) {
|
|
@@ -291,6 +381,188 @@ var init_console_log = __esm(() => {
|
|
|
291
381
|
init_schema();
|
|
292
382
|
});
|
|
293
383
|
|
|
384
|
+
// src/engines/cdp.ts
|
|
385
|
+
var exports_cdp = {};
|
|
386
|
+
__export(exports_cdp, {
|
|
387
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
388
|
+
CDPClient: () => CDPClient
|
|
389
|
+
});
|
|
390
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
391
|
+
const { chromium: chromium2 } = await import("playwright");
|
|
392
|
+
try {
|
|
393
|
+
return await chromium2.connectOverCDP(cdpUrl);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
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);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
class CDPClient {
|
|
400
|
+
session;
|
|
401
|
+
networkEnabled = false;
|
|
402
|
+
performanceEnabled = false;
|
|
403
|
+
constructor(session) {
|
|
404
|
+
this.session = session;
|
|
405
|
+
}
|
|
406
|
+
static async fromPage(page) {
|
|
407
|
+
try {
|
|
408
|
+
const session = await page.context().newCDPSession(page);
|
|
409
|
+
return new CDPClient(session);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async send(method, params) {
|
|
415
|
+
try {
|
|
416
|
+
return await this.session.send(method, params);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
on(event, handler) {
|
|
422
|
+
this.session.on(event, handler);
|
|
423
|
+
}
|
|
424
|
+
off(event, handler) {
|
|
425
|
+
this.session.off(event, handler);
|
|
426
|
+
}
|
|
427
|
+
async enableNetwork() {
|
|
428
|
+
if (!this.networkEnabled) {
|
|
429
|
+
await this.send("Network.enable");
|
|
430
|
+
this.networkEnabled = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async enablePerformance() {
|
|
434
|
+
if (!this.performanceEnabled) {
|
|
435
|
+
await this.send("Performance.enable");
|
|
436
|
+
this.performanceEnabled = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async getPerformanceMetrics() {
|
|
440
|
+
await this.enablePerformance();
|
|
441
|
+
const result = await this.send("Performance.getMetrics");
|
|
442
|
+
const m = {};
|
|
443
|
+
for (const metric of result.metrics) {
|
|
444
|
+
m[metric.name] = metric.value;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
448
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
449
|
+
dom_interactive: m["DOMInteractive"],
|
|
450
|
+
dom_complete: m["DOMComplete"],
|
|
451
|
+
load_event: m["LoadEventEnd"]
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async startJSCoverage() {
|
|
455
|
+
await this.send("Profiler.enable");
|
|
456
|
+
await this.send("Debugger.enable");
|
|
457
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
458
|
+
callCount: false,
|
|
459
|
+
detailed: true
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async stopJSCoverage() {
|
|
463
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
464
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
465
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
466
|
+
url: r.url,
|
|
467
|
+
text: "",
|
|
468
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
async getCoverage() {
|
|
472
|
+
await this.startJSCoverage();
|
|
473
|
+
const js = await this.stopJSCoverage();
|
|
474
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
475
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
476
|
+
}
|
|
477
|
+
async captureHAREntries(page, handler) {
|
|
478
|
+
await this.enableNetwork();
|
|
479
|
+
const requestTimings = new Map;
|
|
480
|
+
const onRequest = (params) => {
|
|
481
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
482
|
+
};
|
|
483
|
+
const onResponse = (params) => {
|
|
484
|
+
const start = requestTimings.get(params.requestId);
|
|
485
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
486
|
+
handler({
|
|
487
|
+
method: "GET",
|
|
488
|
+
url: params.response.url,
|
|
489
|
+
status: params.response.status,
|
|
490
|
+
duration
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
494
|
+
this.on("Network.responseReceived", onResponse);
|
|
495
|
+
return () => {
|
|
496
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
497
|
+
this.off("Network.responseReceived", onResponse);
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
async detach() {
|
|
501
|
+
try {
|
|
502
|
+
await this.session.detach();
|
|
503
|
+
} catch {}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
var init_cdp = __esm(() => {
|
|
507
|
+
init_types();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// src/lib/storage-state.ts
|
|
511
|
+
var exports_storage_state = {};
|
|
512
|
+
__export(exports_storage_state, {
|
|
513
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
514
|
+
saveState: () => saveState,
|
|
515
|
+
loadStatePath: () => loadStatePath,
|
|
516
|
+
listStates: () => listStates,
|
|
517
|
+
deleteState: () => deleteState
|
|
518
|
+
});
|
|
519
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
520
|
+
import { join as join3 } from "path";
|
|
521
|
+
import { homedir as homedir3 } from "os";
|
|
522
|
+
function ensureDir() {
|
|
523
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
524
|
+
}
|
|
525
|
+
function statePath(name) {
|
|
526
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
527
|
+
}
|
|
528
|
+
async function saveState(context, name) {
|
|
529
|
+
ensureDir();
|
|
530
|
+
const path = statePath(name);
|
|
531
|
+
const state = await context.storageState({ path });
|
|
532
|
+
return path;
|
|
533
|
+
}
|
|
534
|
+
async function saveStateFromPage(page, name) {
|
|
535
|
+
return saveState(page.context(), name);
|
|
536
|
+
}
|
|
537
|
+
function loadStatePath(name) {
|
|
538
|
+
const path = statePath(name);
|
|
539
|
+
return existsSync(path) ? path : null;
|
|
540
|
+
}
|
|
541
|
+
function listStates() {
|
|
542
|
+
ensureDir();
|
|
543
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
544
|
+
const path = join3(STATES_DIR, f);
|
|
545
|
+
const stat = Bun.file(path);
|
|
546
|
+
return {
|
|
547
|
+
name: f.replace(".json", ""),
|
|
548
|
+
path,
|
|
549
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
550
|
+
};
|
|
551
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
552
|
+
}
|
|
553
|
+
function deleteState(name) {
|
|
554
|
+
const path = statePath(name);
|
|
555
|
+
if (existsSync(path)) {
|
|
556
|
+
unlinkSync(path);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
var STATES_DIR;
|
|
562
|
+
var init_storage_state = __esm(() => {
|
|
563
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
564
|
+
});
|
|
565
|
+
|
|
294
566
|
// node_modules/sharp/lib/is.js
|
|
295
567
|
var require_is = __commonJS((exports, module) => {
|
|
296
568
|
/*!
|
|
@@ -6686,90 +6958,13 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
6686
6958
|
module.exports = Sharp;
|
|
6687
6959
|
});
|
|
6688
6960
|
|
|
6689
|
-
// src/types/index.ts
|
|
6690
|
-
var UseCase;
|
|
6691
|
-
((UseCase2) => {
|
|
6692
|
-
UseCase2["SCRAPE"] = "scrape";
|
|
6693
|
-
UseCase2["EXTRACT_LINKS"] = "extract_links";
|
|
6694
|
-
UseCase2["STATUS_CHECK"] = "status_check";
|
|
6695
|
-
UseCase2["FORM_FILL"] = "form_fill";
|
|
6696
|
-
UseCase2["SPA_NAVIGATE"] = "spa_navigate";
|
|
6697
|
-
UseCase2["SCREENSHOT"] = "screenshot";
|
|
6698
|
-
UseCase2["AUTH_FLOW"] = "auth_flow";
|
|
6699
|
-
UseCase2["MULTI_TAB"] = "multi_tab";
|
|
6700
|
-
UseCase2["NETWORK_MONITOR"] = "network_monitor";
|
|
6701
|
-
UseCase2["HAR_CAPTURE"] = "har_capture";
|
|
6702
|
-
UseCase2["PERF_PROFILE"] = "perf_profile";
|
|
6703
|
-
UseCase2["SCRIPT_INJECT"] = "script_inject";
|
|
6704
|
-
UseCase2["COVERAGE"] = "coverage";
|
|
6705
|
-
UseCase2["RECORD_REPLAY"] = "record_replay";
|
|
6706
|
-
})(UseCase ||= {});
|
|
6707
|
-
|
|
6708
|
-
class BrowserError extends Error {
|
|
6709
|
-
code;
|
|
6710
|
-
retryable;
|
|
6711
|
-
constructor(message, code = "BROWSER_ERROR", retryable = false) {
|
|
6712
|
-
super(message);
|
|
6713
|
-
this.code = code;
|
|
6714
|
-
this.retryable = retryable;
|
|
6715
|
-
this.name = "BrowserError";
|
|
6716
|
-
}
|
|
6717
|
-
}
|
|
6718
|
-
|
|
6719
|
-
class SessionNotFoundError extends BrowserError {
|
|
6720
|
-
constructor(id) {
|
|
6721
|
-
super(`Session not found: ${id}`, "SESSION_NOT_FOUND", false);
|
|
6722
|
-
this.name = "SessionNotFoundError";
|
|
6723
|
-
}
|
|
6724
|
-
}
|
|
6725
|
-
|
|
6726
|
-
class EngineNotAvailableError extends BrowserError {
|
|
6727
|
-
constructor(engine, reason) {
|
|
6728
|
-
super(`Engine '${engine}' is not available${reason ? `: ${reason}` : ""}`, "ENGINE_NOT_AVAILABLE", false);
|
|
6729
|
-
this.name = "EngineNotAvailableError";
|
|
6730
|
-
}
|
|
6731
|
-
}
|
|
6732
|
-
|
|
6733
|
-
class NavigationError extends BrowserError {
|
|
6734
|
-
constructor(url, reason) {
|
|
6735
|
-
super(`Navigation to '${url}' failed${reason ? `: ${reason}` : ""}`, "NAVIGATION_ERROR", true);
|
|
6736
|
-
this.name = "NavigationError";
|
|
6737
|
-
}
|
|
6738
|
-
}
|
|
6739
|
-
|
|
6740
|
-
class ElementNotFoundError extends BrowserError {
|
|
6741
|
-
constructor(selector) {
|
|
6742
|
-
super(`Element not found: ${selector}`, "ELEMENT_NOT_FOUND", false);
|
|
6743
|
-
this.name = "ElementNotFoundError";
|
|
6744
|
-
}
|
|
6745
|
-
}
|
|
6746
|
-
|
|
6747
|
-
class RecordingNotFoundError extends BrowserError {
|
|
6748
|
-
constructor(id) {
|
|
6749
|
-
super(`Recording not found: ${id}`, "RECORDING_NOT_FOUND", false);
|
|
6750
|
-
this.name = "RecordingNotFoundError";
|
|
6751
|
-
}
|
|
6752
|
-
}
|
|
6753
|
-
|
|
6754
|
-
class AgentNotFoundError extends BrowserError {
|
|
6755
|
-
constructor(id) {
|
|
6756
|
-
super(`Agent not found: ${id}`, "AGENT_NOT_FOUND", false);
|
|
6757
|
-
this.name = "AgentNotFoundError";
|
|
6758
|
-
}
|
|
6759
|
-
}
|
|
6760
|
-
|
|
6761
|
-
class ProjectNotFoundError extends BrowserError {
|
|
6762
|
-
constructor(id) {
|
|
6763
|
-
super(`Project not found: ${id}`, "PROJECT_NOT_FOUND", false);
|
|
6764
|
-
this.name = "ProjectNotFoundError";
|
|
6765
|
-
}
|
|
6766
|
-
}
|
|
6767
|
-
|
|
6768
6961
|
// src/index.ts
|
|
6769
6962
|
init_schema();
|
|
6963
|
+
init_types();
|
|
6770
6964
|
|
|
6771
6965
|
// src/db/projects.ts
|
|
6772
6966
|
init_schema();
|
|
6967
|
+
init_types();
|
|
6773
6968
|
import { randomUUID } from "crypto";
|
|
6774
6969
|
function createProject(data) {
|
|
6775
6970
|
const db = getDatabase();
|
|
@@ -6827,6 +7022,7 @@ function deleteProject(id) {
|
|
|
6827
7022
|
}
|
|
6828
7023
|
// src/db/agents.ts
|
|
6829
7024
|
init_schema();
|
|
7025
|
+
init_types();
|
|
6830
7026
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6831
7027
|
function registerAgent(name, opts = {}) {
|
|
6832
7028
|
const db = getDatabase();
|
|
@@ -6907,6 +7103,7 @@ function cleanStaleAgents(thresholdMs) {
|
|
|
6907
7103
|
}
|
|
6908
7104
|
// src/db/sessions.ts
|
|
6909
7105
|
init_schema();
|
|
7106
|
+
init_types();
|
|
6910
7107
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6911
7108
|
function createSession(data) {
|
|
6912
7109
|
const db = getDatabase();
|
|
@@ -7039,6 +7236,7 @@ init_console_log();
|
|
|
7039
7236
|
|
|
7040
7237
|
// src/db/recordings.ts
|
|
7041
7238
|
init_schema();
|
|
7239
|
+
init_types();
|
|
7042
7240
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
7043
7241
|
function deserialize(row) {
|
|
7044
7242
|
return {
|
|
@@ -7153,6 +7351,7 @@ function cleanOldHeartbeats(olderThanMs) {
|
|
|
7153
7351
|
return result.changes;
|
|
7154
7352
|
}
|
|
7155
7353
|
// src/engines/playwright.ts
|
|
7354
|
+
init_types();
|
|
7156
7355
|
import { chromium } from "playwright";
|
|
7157
7356
|
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
7158
7357
|
async function launchPlaywright(options) {
|
|
@@ -7230,115 +7429,12 @@ class BrowserPool {
|
|
|
7230
7429
|
return this.pool.filter((e) => !e.inUse).length;
|
|
7231
7430
|
}
|
|
7232
7431
|
}
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
performanceEnabled = false;
|
|
7238
|
-
constructor(session) {
|
|
7239
|
-
this.session = session;
|
|
7240
|
-
}
|
|
7241
|
-
static async fromPage(page) {
|
|
7242
|
-
try {
|
|
7243
|
-
const session = await page.context().newCDPSession(page);
|
|
7244
|
-
return new CDPClient(session);
|
|
7245
|
-
} catch (err) {
|
|
7246
|
-
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
7247
|
-
}
|
|
7248
|
-
}
|
|
7249
|
-
async send(method, params) {
|
|
7250
|
-
try {
|
|
7251
|
-
return await this.session.send(method, params);
|
|
7252
|
-
} catch (err) {
|
|
7253
|
-
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
7254
|
-
}
|
|
7255
|
-
}
|
|
7256
|
-
on(event, handler) {
|
|
7257
|
-
this.session.on(event, handler);
|
|
7258
|
-
}
|
|
7259
|
-
off(event, handler) {
|
|
7260
|
-
this.session.off(event, handler);
|
|
7261
|
-
}
|
|
7262
|
-
async enableNetwork() {
|
|
7263
|
-
if (!this.networkEnabled) {
|
|
7264
|
-
await this.send("Network.enable");
|
|
7265
|
-
this.networkEnabled = true;
|
|
7266
|
-
}
|
|
7267
|
-
}
|
|
7268
|
-
async enablePerformance() {
|
|
7269
|
-
if (!this.performanceEnabled) {
|
|
7270
|
-
await this.send("Performance.enable");
|
|
7271
|
-
this.performanceEnabled = true;
|
|
7272
|
-
}
|
|
7273
|
-
}
|
|
7274
|
-
async getPerformanceMetrics() {
|
|
7275
|
-
await this.enablePerformance();
|
|
7276
|
-
const result = await this.send("Performance.getMetrics");
|
|
7277
|
-
const m = {};
|
|
7278
|
-
for (const metric of result.metrics) {
|
|
7279
|
-
m[metric.name] = metric.value;
|
|
7280
|
-
}
|
|
7281
|
-
return {
|
|
7282
|
-
js_heap_size_used: m["JSHeapUsedSize"],
|
|
7283
|
-
js_heap_size_total: m["JSHeapTotalSize"],
|
|
7284
|
-
dom_interactive: m["DOMInteractive"],
|
|
7285
|
-
dom_complete: m["DOMComplete"],
|
|
7286
|
-
load_event: m["LoadEventEnd"]
|
|
7287
|
-
};
|
|
7288
|
-
}
|
|
7289
|
-
async startJSCoverage() {
|
|
7290
|
-
await this.send("Profiler.enable");
|
|
7291
|
-
await this.send("Debugger.enable");
|
|
7292
|
-
await this.send("Profiler.startPreciseCoverage", {
|
|
7293
|
-
callCount: false,
|
|
7294
|
-
detailed: true
|
|
7295
|
-
});
|
|
7296
|
-
}
|
|
7297
|
-
async stopJSCoverage() {
|
|
7298
|
-
const result = await this.send("Profiler.takePreciseCoverage");
|
|
7299
|
-
await this.send("Profiler.stopPreciseCoverage");
|
|
7300
|
-
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
7301
|
-
url: r.url,
|
|
7302
|
-
text: "",
|
|
7303
|
-
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
7304
|
-
}));
|
|
7305
|
-
}
|
|
7306
|
-
async getCoverage() {
|
|
7307
|
-
await this.startJSCoverage();
|
|
7308
|
-
const js = await this.stopJSCoverage();
|
|
7309
|
-
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
7310
|
-
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
7311
|
-
}
|
|
7312
|
-
async captureHAREntries(page, handler) {
|
|
7313
|
-
await this.enableNetwork();
|
|
7314
|
-
const requestTimings = new Map;
|
|
7315
|
-
const onRequest = (params) => {
|
|
7316
|
-
requestTimings.set(params.requestId, params.timestamp);
|
|
7317
|
-
};
|
|
7318
|
-
const onResponse = (params) => {
|
|
7319
|
-
const start = requestTimings.get(params.requestId);
|
|
7320
|
-
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
7321
|
-
handler({
|
|
7322
|
-
method: "GET",
|
|
7323
|
-
url: params.response.url,
|
|
7324
|
-
status: params.response.status,
|
|
7325
|
-
duration
|
|
7326
|
-
});
|
|
7327
|
-
};
|
|
7328
|
-
this.on("Network.requestWillBeSent", onRequest);
|
|
7329
|
-
this.on("Network.responseReceived", onResponse);
|
|
7330
|
-
return () => {
|
|
7331
|
-
this.off("Network.requestWillBeSent", onRequest);
|
|
7332
|
-
this.off("Network.responseReceived", onResponse);
|
|
7333
|
-
};
|
|
7334
|
-
}
|
|
7335
|
-
async detach() {
|
|
7336
|
-
try {
|
|
7337
|
-
await this.session.detach();
|
|
7338
|
-
} catch {}
|
|
7339
|
-
}
|
|
7340
|
-
}
|
|
7432
|
+
|
|
7433
|
+
// src/index.ts
|
|
7434
|
+
init_cdp();
|
|
7435
|
+
|
|
7341
7436
|
// src/engines/lightpanda.ts
|
|
7437
|
+
init_types();
|
|
7342
7438
|
import { execSync, spawn } from "child_process";
|
|
7343
7439
|
import { chromium as chromium2 } from "playwright";
|
|
7344
7440
|
var DEFAULT_LIGHTPANDA_PORT = 9222;
|
|
@@ -7480,6 +7576,9 @@ class LightpandaPage {
|
|
|
7480
7576
|
await this.page.context().close();
|
|
7481
7577
|
}
|
|
7482
7578
|
}
|
|
7579
|
+
// src/engines/selector.ts
|
|
7580
|
+
init_types();
|
|
7581
|
+
|
|
7483
7582
|
// src/engines/bun-webview.ts
|
|
7484
7583
|
import { join as join2 } from "path";
|
|
7485
7584
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -7953,6 +8052,10 @@ function inferUseCase(label) {
|
|
|
7953
8052
|
};
|
|
7954
8053
|
return map[label.toLowerCase()] ?? "spa_navigate" /* SPA_NAVIGATE */;
|
|
7955
8054
|
}
|
|
8055
|
+
// src/lib/session.ts
|
|
8056
|
+
init_types();
|
|
8057
|
+
init_types();
|
|
8058
|
+
|
|
7956
8059
|
// src/lib/network.ts
|
|
7957
8060
|
function enableNetworkLogging(page, sessionId) {
|
|
7958
8061
|
const requestStart = new Map;
|
|
@@ -8229,6 +8332,37 @@ function createBunProxy(view) {
|
|
|
8229
8332
|
return view;
|
|
8230
8333
|
}
|
|
8231
8334
|
async function createSession2(opts = {}) {
|
|
8335
|
+
if (opts.cdpUrl) {
|
|
8336
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
8337
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
8338
|
+
const contexts = cdpBrowser.contexts();
|
|
8339
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
8340
|
+
const pages = context.pages();
|
|
8341
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
8342
|
+
const session2 = createSession({
|
|
8343
|
+
engine: "cdp",
|
|
8344
|
+
projectId: opts.projectId,
|
|
8345
|
+
agentId: opts.agentId,
|
|
8346
|
+
startUrl: page2.url(),
|
|
8347
|
+
name: opts.name ?? "attached"
|
|
8348
|
+
});
|
|
8349
|
+
const cleanups2 = [];
|
|
8350
|
+
if (opts.captureNetwork !== false) {
|
|
8351
|
+
try {
|
|
8352
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
8353
|
+
} catch {}
|
|
8354
|
+
}
|
|
8355
|
+
if (opts.captureConsole !== false) {
|
|
8356
|
+
try {
|
|
8357
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
8358
|
+
} catch {}
|
|
8359
|
+
}
|
|
8360
|
+
try {
|
|
8361
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
8362
|
+
} catch {}
|
|
8363
|
+
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 });
|
|
8364
|
+
return { session: session2, page: page2 };
|
|
8365
|
+
}
|
|
8232
8366
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
8233
8367
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
8234
8368
|
let browser = null;
|
|
@@ -8254,7 +8388,22 @@ async function createSession2(opts = {}) {
|
|
|
8254
8388
|
page = await context.newPage();
|
|
8255
8389
|
} else {
|
|
8256
8390
|
browser = await pool.acquire(opts.headless ?? true);
|
|
8257
|
-
|
|
8391
|
+
if (opts.storageState) {
|
|
8392
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
8393
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
8394
|
+
if (statePath2) {
|
|
8395
|
+
const context = await browser.newContext({
|
|
8396
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
8397
|
+
userAgent: opts.userAgent,
|
|
8398
|
+
storageState: statePath2
|
|
8399
|
+
});
|
|
8400
|
+
page = await context.newPage();
|
|
8401
|
+
} else {
|
|
8402
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8403
|
+
}
|
|
8404
|
+
} else {
|
|
8405
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8406
|
+
}
|
|
8258
8407
|
}
|
|
8259
8408
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
8260
8409
|
try {
|
|
@@ -8452,6 +8601,9 @@ function isAutoGallery(sessionId) {
|
|
|
8452
8601
|
function countActiveSessions2() {
|
|
8453
8602
|
return countActiveSessions();
|
|
8454
8603
|
}
|
|
8604
|
+
// src/lib/actions.ts
|
|
8605
|
+
init_types();
|
|
8606
|
+
|
|
8455
8607
|
// src/lib/snapshot.ts
|
|
8456
8608
|
var lastSnapshots = new Map;
|
|
8457
8609
|
var sessionRefMaps = new Map;
|
|
@@ -8465,6 +8617,66 @@ function getRefLocator(page, sessionId, ref) {
|
|
|
8465
8617
|
return page.getByRole(entry.role, { name: entry.name }).first();
|
|
8466
8618
|
}
|
|
8467
8619
|
|
|
8620
|
+
// src/lib/self-heal.ts
|
|
8621
|
+
async function healSelector(page, selector, sessionId) {
|
|
8622
|
+
const attempts = [];
|
|
8623
|
+
attempts.push(`selector: ${selector}`);
|
|
8624
|
+
try {
|
|
8625
|
+
const loc = page.locator(selector).first();
|
|
8626
|
+
if (await loc.count() > 0) {
|
|
8627
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
8628
|
+
}
|
|
8629
|
+
} catch {}
|
|
8630
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
8631
|
+
attempts.push(`text: "${selector}"`);
|
|
8632
|
+
try {
|
|
8633
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
8634
|
+
if (await loc.count() > 0) {
|
|
8635
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
8636
|
+
}
|
|
8637
|
+
} catch {}
|
|
8638
|
+
}
|
|
8639
|
+
const roleMap = {
|
|
8640
|
+
button: ["button", "submit", "reset"],
|
|
8641
|
+
link: ["a"],
|
|
8642
|
+
input: ["input", "textarea"],
|
|
8643
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
8644
|
+
};
|
|
8645
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
8646
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
8647
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
8648
|
+
try {
|
|
8649
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
8650
|
+
if (await loc.count() > 0) {
|
|
8651
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
8652
|
+
}
|
|
8653
|
+
} catch {}
|
|
8654
|
+
}
|
|
8655
|
+
if (selector.startsWith("#")) {
|
|
8656
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8657
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
8658
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
8659
|
+
try {
|
|
8660
|
+
const loc = page.locator(partialSel).first();
|
|
8661
|
+
if (await loc.count() > 0) {
|
|
8662
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
8663
|
+
}
|
|
8664
|
+
} catch {}
|
|
8665
|
+
}
|
|
8666
|
+
if (selector.startsWith(".")) {
|
|
8667
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8668
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
8669
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
8670
|
+
try {
|
|
8671
|
+
const loc = page.locator(partialSel).first();
|
|
8672
|
+
if (await loc.count() > 0) {
|
|
8673
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
8674
|
+
}
|
|
8675
|
+
} catch {}
|
|
8676
|
+
}
|
|
8677
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
8678
|
+
}
|
|
8679
|
+
|
|
8468
8680
|
// src/lib/actions.ts
|
|
8469
8681
|
async function click(page, selector, opts) {
|
|
8470
8682
|
try {
|
|
@@ -8474,11 +8686,22 @@ async function click(page, selector, opts) {
|
|
|
8474
8686
|
delay: opts?.delay,
|
|
8475
8687
|
timeout: opts?.timeout ?? 1e4
|
|
8476
8688
|
});
|
|
8477
|
-
|
|
8478
|
-
|
|
8689
|
+
return {};
|
|
8690
|
+
} catch (originalError) {
|
|
8691
|
+
if (opts?.selfHeal !== false) {
|
|
8692
|
+
const result = await healSelector(page, selector);
|
|
8693
|
+
if (result.found && result.locator) {
|
|
8694
|
+
await result.locator.click({
|
|
8695
|
+
button: opts?.button ?? "left",
|
|
8696
|
+
timeout: opts?.timeout ?? 1e4
|
|
8697
|
+
});
|
|
8698
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8479
8702
|
throw new ElementNotFoundError(selector);
|
|
8480
8703
|
}
|
|
8481
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
8704
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
8482
8705
|
}
|
|
8483
8706
|
}
|
|
8484
8707
|
async function type(page, selector, text, opts) {
|
|
@@ -8487,17 +8710,35 @@ async function type(page, selector, text, opts) {
|
|
|
8487
8710
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
8488
8711
|
}
|
|
8489
8712
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8490
|
-
|
|
8491
|
-
|
|
8713
|
+
return {};
|
|
8714
|
+
} catch (originalError) {
|
|
8715
|
+
if (opts?.selfHeal !== false) {
|
|
8716
|
+
const result = await healSelector(page, selector);
|
|
8717
|
+
if (result.found && result.locator) {
|
|
8718
|
+
if (opts?.clear)
|
|
8719
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
8720
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8721
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8722
|
+
}
|
|
8723
|
+
}
|
|
8724
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8492
8725
|
throw new ElementNotFoundError(selector);
|
|
8493
8726
|
}
|
|
8494
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
8727
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
8495
8728
|
}
|
|
8496
8729
|
}
|
|
8497
|
-
async function fill(page, selector, value, timeout = 1e4) {
|
|
8730
|
+
async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
|
|
8498
8731
|
try {
|
|
8499
8732
|
await page.fill(selector, value, { timeout });
|
|
8500
|
-
|
|
8733
|
+
return {};
|
|
8734
|
+
} catch (originalError) {
|
|
8735
|
+
if (selfHeal) {
|
|
8736
|
+
const result = await healSelector(page, selector);
|
|
8737
|
+
if (result.found && result.locator) {
|
|
8738
|
+
await result.locator.fill(value, { timeout });
|
|
8739
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8740
|
+
}
|
|
8741
|
+
}
|
|
8501
8742
|
throw new ElementNotFoundError(selector);
|
|
8502
8743
|
}
|
|
8503
8744
|
}
|
|
@@ -8623,12 +8864,39 @@ async function clickText(page, text, opts) {
|
|
|
8623
8864
|
}
|
|
8624
8865
|
}, { retries: opts?.retries ?? 1 });
|
|
8625
8866
|
}
|
|
8626
|
-
async function fillForm(page, fields, submitSelector) {
|
|
8867
|
+
async function fillForm(page, fields, submitSelector, selfHeal = true) {
|
|
8627
8868
|
let filled = 0;
|
|
8628
8869
|
const errors = [];
|
|
8870
|
+
const healedFields = [];
|
|
8629
8871
|
for (const [selector, value] of Object.entries(fields)) {
|
|
8630
8872
|
try {
|
|
8631
|
-
|
|
8873
|
+
let el = await page.$(selector);
|
|
8874
|
+
if (!el && selfHeal) {
|
|
8875
|
+
const result = await healSelector(page, selector);
|
|
8876
|
+
if (result.found && result.locator) {
|
|
8877
|
+
const handle = await result.locator.elementHandle();
|
|
8878
|
+
if (handle) {
|
|
8879
|
+
el = handle;
|
|
8880
|
+
healedFields.push(selector);
|
|
8881
|
+
const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
|
|
8882
|
+
const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
|
|
8883
|
+
if (tagName2 === "select") {
|
|
8884
|
+
await result.locator.selectOption(String(value));
|
|
8885
|
+
} else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
|
|
8886
|
+
if (Boolean(value))
|
|
8887
|
+
await result.locator.check();
|
|
8888
|
+
else
|
|
8889
|
+
await result.locator.uncheck();
|
|
8890
|
+
} else {
|
|
8891
|
+
await result.locator.fill(String(value));
|
|
8892
|
+
}
|
|
8893
|
+
filled++;
|
|
8894
|
+
continue;
|
|
8895
|
+
}
|
|
8896
|
+
}
|
|
8897
|
+
errors.push(`${selector}: element not found`);
|
|
8898
|
+
continue;
|
|
8899
|
+
}
|
|
8632
8900
|
if (!el) {
|
|
8633
8901
|
errors.push(`${selector}: element not found`);
|
|
8634
8902
|
continue;
|
|
@@ -8655,11 +8923,21 @@ async function fillForm(page, fields, submitSelector) {
|
|
|
8655
8923
|
if (submitSelector) {
|
|
8656
8924
|
try {
|
|
8657
8925
|
await page.click(submitSelector);
|
|
8658
|
-
} catch (
|
|
8659
|
-
|
|
8926
|
+
} catch (submitErr) {
|
|
8927
|
+
if (selfHeal) {
|
|
8928
|
+
const result = await healSelector(page, submitSelector);
|
|
8929
|
+
if (result.found && result.locator) {
|
|
8930
|
+
await result.locator.click();
|
|
8931
|
+
healedFields.push(submitSelector);
|
|
8932
|
+
} else {
|
|
8933
|
+
errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
8934
|
+
}
|
|
8935
|
+
} else {
|
|
8936
|
+
errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
8937
|
+
}
|
|
8660
8938
|
}
|
|
8661
8939
|
}
|
|
8662
|
-
return { filled, errors, fields_attempted: Object.keys(fields).length };
|
|
8940
|
+
return { filled, errors, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
|
|
8663
8941
|
}
|
|
8664
8942
|
async function waitForText(page, text, opts) {
|
|
8665
8943
|
const timeout = opts?.timeout ?? 1e4;
|
|
@@ -8939,6 +9217,7 @@ async function getPageInfo(page) {
|
|
|
8939
9217
|
};
|
|
8940
9218
|
}
|
|
8941
9219
|
// src/lib/performance.ts
|
|
9220
|
+
init_cdp();
|
|
8942
9221
|
async function getPerformanceMetrics(page) {
|
|
8943
9222
|
const navTiming = await page.evaluate(() => {
|
|
8944
9223
|
const t = performance.timing;
|
|
@@ -9026,10 +9305,11 @@ async function startCoverage(page) {
|
|
|
9026
9305
|
};
|
|
9027
9306
|
}
|
|
9028
9307
|
// src/lib/screenshot.ts
|
|
9308
|
+
init_types();
|
|
9029
9309
|
var import_sharp = __toESM(require_lib(), 1);
|
|
9030
|
-
import { join as
|
|
9031
|
-
import { mkdirSync as
|
|
9032
|
-
import { homedir as
|
|
9310
|
+
import { join as join4 } from "path";
|
|
9311
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
9312
|
+
import { homedir as homedir4 } from "os";
|
|
9033
9313
|
|
|
9034
9314
|
// src/db/gallery.ts
|
|
9035
9315
|
init_schema();
|
|
@@ -9075,13 +9355,13 @@ function getEntry(id) {
|
|
|
9075
9355
|
|
|
9076
9356
|
// src/lib/screenshot.ts
|
|
9077
9357
|
function getDataDir2() {
|
|
9078
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
9358
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
9079
9359
|
}
|
|
9080
9360
|
function getScreenshotDir(projectId) {
|
|
9081
|
-
const base =
|
|
9361
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
9082
9362
|
const date = new Date().toISOString().split("T")[0];
|
|
9083
|
-
const dir = projectId ?
|
|
9084
|
-
|
|
9363
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
9364
|
+
mkdirSync4(dir, { recursive: true });
|
|
9085
9365
|
return dir;
|
|
9086
9366
|
}
|
|
9087
9367
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -9096,7 +9376,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
9096
9376
|
}
|
|
9097
9377
|
}
|
|
9098
9378
|
async function generateThumbnail(raw, dir, stem) {
|
|
9099
|
-
const thumbPath =
|
|
9379
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
9100
9380
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
9101
9381
|
await Bun.write(thumbPath, thumbBuffer);
|
|
9102
9382
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -9153,7 +9433,7 @@ async function takeScreenshot(page, opts) {
|
|
|
9153
9433
|
const compressedSizeBytes = finalBuffer.length;
|
|
9154
9434
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
9155
9435
|
const ext = format;
|
|
9156
|
-
const screenshotPath = opts?.path ??
|
|
9436
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
9157
9437
|
await Bun.write(screenshotPath, finalBuffer);
|
|
9158
9438
|
let thumbnailPath;
|
|
9159
9439
|
let thumbnailBase64;
|
|
@@ -9213,12 +9493,12 @@ async function takeScreenshot(page, opts) {
|
|
|
9213
9493
|
}
|
|
9214
9494
|
async function generatePDF(page, opts) {
|
|
9215
9495
|
try {
|
|
9216
|
-
const base =
|
|
9496
|
+
const base = join4(getDataDir2(), "pdfs");
|
|
9217
9497
|
const date = new Date().toISOString().split("T")[0];
|
|
9218
|
-
const dir = opts?.projectId ?
|
|
9219
|
-
|
|
9498
|
+
const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
|
|
9499
|
+
mkdirSync4(dir, { recursive: true });
|
|
9220
9500
|
const timestamp = Date.now();
|
|
9221
|
-
const pdfPath = opts?.path ??
|
|
9501
|
+
const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
|
|
9222
9502
|
const buffer = await page.pdf({
|
|
9223
9503
|
path: pdfPath,
|
|
9224
9504
|
format: opts?.format ?? "A4",
|
|
@@ -9308,6 +9588,7 @@ async function getIndexedDB(page, dbName, storeName) {
|
|
|
9308
9588
|
}), [dbName, storeName]);
|
|
9309
9589
|
}
|
|
9310
9590
|
// src/lib/recorder.ts
|
|
9591
|
+
init_types();
|
|
9311
9592
|
var activeRecordings = new Map;
|
|
9312
9593
|
function startRecording(sessionId, name, startUrl) {
|
|
9313
9594
|
const steps = [];
|
|
@@ -9466,6 +9747,7 @@ function exportRecording(recordingId, format = "json") {
|
|
|
9466
9747
|
`);
|
|
9467
9748
|
}
|
|
9468
9749
|
// src/lib/crawler.ts
|
|
9750
|
+
init_types();
|
|
9469
9751
|
async function crawl(startUrl, opts = {}) {
|
|
9470
9752
|
const maxDepth = opts.maxDepth ?? 2;
|
|
9471
9753
|
const maxPages = opts.maxPages ?? 50;
|
|
@@ -9678,6 +9960,7 @@ export {
|
|
|
9678
9960
|
createCrawlResult,
|
|
9679
9961
|
crawl,
|
|
9680
9962
|
countActiveSessions2 as countActiveSessions,
|
|
9963
|
+
connectToExistingBrowser,
|
|
9681
9964
|
connectLightpanda,
|
|
9682
9965
|
closeSession2 as closeSession,
|
|
9683
9966
|
closePage,
|