@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/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,71 @@ 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
|
+
`
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
version: 7,
|
|
339
|
+
sql: `
|
|
340
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
341
|
+
id TEXT PRIMARY KEY,
|
|
342
|
+
name TEXT NOT NULL UNIQUE,
|
|
343
|
+
description TEXT,
|
|
344
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
345
|
+
start_url TEXT,
|
|
346
|
+
last_run TEXT,
|
|
347
|
+
last_heal TEXT,
|
|
348
|
+
heal_count INTEGER DEFAULT 0,
|
|
349
|
+
run_count INTEGER DEFAULT 0,
|
|
350
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
351
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
352
|
+
);
|
|
353
|
+
`
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
version: 8,
|
|
357
|
+
sql: `
|
|
358
|
+
CREATE TABLE IF NOT EXISTS datasets (
|
|
359
|
+
id TEXT PRIMARY KEY,
|
|
360
|
+
name TEXT NOT NULL UNIQUE,
|
|
361
|
+
source_url TEXT,
|
|
362
|
+
source_type TEXT NOT NULL DEFAULT 'page',
|
|
363
|
+
data TEXT NOT NULL DEFAULT '[]',
|
|
364
|
+
schema TEXT,
|
|
365
|
+
row_count INTEGER DEFAULT 0,
|
|
366
|
+
last_refresh TEXT,
|
|
367
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
368
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
CREATE TABLE IF NOT EXISTS api_endpoints (
|
|
372
|
+
id TEXT PRIMARY KEY,
|
|
373
|
+
session_id TEXT,
|
|
374
|
+
url TEXT NOT NULL,
|
|
375
|
+
method TEXT DEFAULT 'GET',
|
|
376
|
+
response_schema TEXT,
|
|
377
|
+
sample_response TEXT,
|
|
378
|
+
status_code INTEGER,
|
|
379
|
+
content_type TEXT,
|
|
380
|
+
discovered_at TEXT DEFAULT (datetime('now'))
|
|
381
|
+
);
|
|
382
|
+
CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
|
|
383
|
+
`
|
|
246
384
|
}
|
|
247
385
|
];
|
|
248
386
|
for (const m of migrations) {
|
|
@@ -291,6 +429,188 @@ var init_console_log = __esm(() => {
|
|
|
291
429
|
init_schema();
|
|
292
430
|
});
|
|
293
431
|
|
|
432
|
+
// src/engines/cdp.ts
|
|
433
|
+
var exports_cdp = {};
|
|
434
|
+
__export(exports_cdp, {
|
|
435
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
436
|
+
CDPClient: () => CDPClient
|
|
437
|
+
});
|
|
438
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
439
|
+
const { chromium: chromium2 } = await import("playwright");
|
|
440
|
+
try {
|
|
441
|
+
return await chromium2.connectOverCDP(cdpUrl);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
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);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
class CDPClient {
|
|
448
|
+
session;
|
|
449
|
+
networkEnabled = false;
|
|
450
|
+
performanceEnabled = false;
|
|
451
|
+
constructor(session) {
|
|
452
|
+
this.session = session;
|
|
453
|
+
}
|
|
454
|
+
static async fromPage(page) {
|
|
455
|
+
try {
|
|
456
|
+
const session = await page.context().newCDPSession(page);
|
|
457
|
+
return new CDPClient(session);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async send(method, params) {
|
|
463
|
+
try {
|
|
464
|
+
return await this.session.send(method, params);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
on(event, handler) {
|
|
470
|
+
this.session.on(event, handler);
|
|
471
|
+
}
|
|
472
|
+
off(event, handler) {
|
|
473
|
+
this.session.off(event, handler);
|
|
474
|
+
}
|
|
475
|
+
async enableNetwork() {
|
|
476
|
+
if (!this.networkEnabled) {
|
|
477
|
+
await this.send("Network.enable");
|
|
478
|
+
this.networkEnabled = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async enablePerformance() {
|
|
482
|
+
if (!this.performanceEnabled) {
|
|
483
|
+
await this.send("Performance.enable");
|
|
484
|
+
this.performanceEnabled = true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async getPerformanceMetrics() {
|
|
488
|
+
await this.enablePerformance();
|
|
489
|
+
const result = await this.send("Performance.getMetrics");
|
|
490
|
+
const m = {};
|
|
491
|
+
for (const metric of result.metrics) {
|
|
492
|
+
m[metric.name] = metric.value;
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
496
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
497
|
+
dom_interactive: m["DOMInteractive"],
|
|
498
|
+
dom_complete: m["DOMComplete"],
|
|
499
|
+
load_event: m["LoadEventEnd"]
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async startJSCoverage() {
|
|
503
|
+
await this.send("Profiler.enable");
|
|
504
|
+
await this.send("Debugger.enable");
|
|
505
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
506
|
+
callCount: false,
|
|
507
|
+
detailed: true
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async stopJSCoverage() {
|
|
511
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
512
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
513
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
514
|
+
url: r.url,
|
|
515
|
+
text: "",
|
|
516
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
async getCoverage() {
|
|
520
|
+
await this.startJSCoverage();
|
|
521
|
+
const js = await this.stopJSCoverage();
|
|
522
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
523
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
524
|
+
}
|
|
525
|
+
async captureHAREntries(page, handler) {
|
|
526
|
+
await this.enableNetwork();
|
|
527
|
+
const requestTimings = new Map;
|
|
528
|
+
const onRequest = (params) => {
|
|
529
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
530
|
+
};
|
|
531
|
+
const onResponse = (params) => {
|
|
532
|
+
const start = requestTimings.get(params.requestId);
|
|
533
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
534
|
+
handler({
|
|
535
|
+
method: "GET",
|
|
536
|
+
url: params.response.url,
|
|
537
|
+
status: params.response.status,
|
|
538
|
+
duration
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
542
|
+
this.on("Network.responseReceived", onResponse);
|
|
543
|
+
return () => {
|
|
544
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
545
|
+
this.off("Network.responseReceived", onResponse);
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
async detach() {
|
|
549
|
+
try {
|
|
550
|
+
await this.session.detach();
|
|
551
|
+
} catch {}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
var init_cdp = __esm(() => {
|
|
555
|
+
init_types();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// src/lib/storage-state.ts
|
|
559
|
+
var exports_storage_state = {};
|
|
560
|
+
__export(exports_storage_state, {
|
|
561
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
562
|
+
saveState: () => saveState,
|
|
563
|
+
loadStatePath: () => loadStatePath,
|
|
564
|
+
listStates: () => listStates,
|
|
565
|
+
deleteState: () => deleteState
|
|
566
|
+
});
|
|
567
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
568
|
+
import { join as join3 } from "path";
|
|
569
|
+
import { homedir as homedir3 } from "os";
|
|
570
|
+
function ensureDir() {
|
|
571
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
function statePath(name) {
|
|
574
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
575
|
+
}
|
|
576
|
+
async function saveState(context, name) {
|
|
577
|
+
ensureDir();
|
|
578
|
+
const path = statePath(name);
|
|
579
|
+
const state = await context.storageState({ path });
|
|
580
|
+
return path;
|
|
581
|
+
}
|
|
582
|
+
async function saveStateFromPage(page, name) {
|
|
583
|
+
return saveState(page.context(), name);
|
|
584
|
+
}
|
|
585
|
+
function loadStatePath(name) {
|
|
586
|
+
const path = statePath(name);
|
|
587
|
+
return existsSync(path) ? path : null;
|
|
588
|
+
}
|
|
589
|
+
function listStates() {
|
|
590
|
+
ensureDir();
|
|
591
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
592
|
+
const path = join3(STATES_DIR, f);
|
|
593
|
+
const stat = Bun.file(path);
|
|
594
|
+
return {
|
|
595
|
+
name: f.replace(".json", ""),
|
|
596
|
+
path,
|
|
597
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
598
|
+
};
|
|
599
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
600
|
+
}
|
|
601
|
+
function deleteState(name) {
|
|
602
|
+
const path = statePath(name);
|
|
603
|
+
if (existsSync(path)) {
|
|
604
|
+
unlinkSync(path);
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
var STATES_DIR;
|
|
610
|
+
var init_storage_state = __esm(() => {
|
|
611
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
612
|
+
});
|
|
613
|
+
|
|
294
614
|
// node_modules/sharp/lib/is.js
|
|
295
615
|
var require_is = __commonJS((exports, module) => {
|
|
296
616
|
/*!
|
|
@@ -6686,90 +7006,13 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
6686
7006
|
module.exports = Sharp;
|
|
6687
7007
|
});
|
|
6688
7008
|
|
|
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
7009
|
// src/index.ts
|
|
6769
7010
|
init_schema();
|
|
7011
|
+
init_types();
|
|
6770
7012
|
|
|
6771
7013
|
// src/db/projects.ts
|
|
6772
7014
|
init_schema();
|
|
7015
|
+
init_types();
|
|
6773
7016
|
import { randomUUID } from "crypto";
|
|
6774
7017
|
function createProject(data) {
|
|
6775
7018
|
const db = getDatabase();
|
|
@@ -6827,6 +7070,7 @@ function deleteProject(id) {
|
|
|
6827
7070
|
}
|
|
6828
7071
|
// src/db/agents.ts
|
|
6829
7072
|
init_schema();
|
|
7073
|
+
init_types();
|
|
6830
7074
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6831
7075
|
function registerAgent(name, opts = {}) {
|
|
6832
7076
|
const db = getDatabase();
|
|
@@ -6907,6 +7151,7 @@ function cleanStaleAgents(thresholdMs) {
|
|
|
6907
7151
|
}
|
|
6908
7152
|
// src/db/sessions.ts
|
|
6909
7153
|
init_schema();
|
|
7154
|
+
init_types();
|
|
6910
7155
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6911
7156
|
function createSession(data) {
|
|
6912
7157
|
const db = getDatabase();
|
|
@@ -7039,6 +7284,7 @@ init_console_log();
|
|
|
7039
7284
|
|
|
7040
7285
|
// src/db/recordings.ts
|
|
7041
7286
|
init_schema();
|
|
7287
|
+
init_types();
|
|
7042
7288
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
7043
7289
|
function deserialize(row) {
|
|
7044
7290
|
return {
|
|
@@ -7153,6 +7399,7 @@ function cleanOldHeartbeats(olderThanMs) {
|
|
|
7153
7399
|
return result.changes;
|
|
7154
7400
|
}
|
|
7155
7401
|
// src/engines/playwright.ts
|
|
7402
|
+
init_types();
|
|
7156
7403
|
import { chromium } from "playwright";
|
|
7157
7404
|
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
7158
7405
|
async function launchPlaywright(options) {
|
|
@@ -7230,115 +7477,12 @@ class BrowserPool {
|
|
|
7230
7477
|
return this.pool.filter((e) => !e.inUse).length;
|
|
7231
7478
|
}
|
|
7232
7479
|
}
|
|
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
|
-
}
|
|
7480
|
+
|
|
7481
|
+
// src/index.ts
|
|
7482
|
+
init_cdp();
|
|
7483
|
+
|
|
7341
7484
|
// src/engines/lightpanda.ts
|
|
7485
|
+
init_types();
|
|
7342
7486
|
import { execSync, spawn } from "child_process";
|
|
7343
7487
|
import { chromium as chromium2 } from "playwright";
|
|
7344
7488
|
var DEFAULT_LIGHTPANDA_PORT = 9222;
|
|
@@ -7480,6 +7624,9 @@ class LightpandaPage {
|
|
|
7480
7624
|
await this.page.context().close();
|
|
7481
7625
|
}
|
|
7482
7626
|
}
|
|
7627
|
+
// src/engines/selector.ts
|
|
7628
|
+
init_types();
|
|
7629
|
+
|
|
7483
7630
|
// src/engines/bun-webview.ts
|
|
7484
7631
|
import { join as join2 } from "path";
|
|
7485
7632
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -7953,6 +8100,10 @@ function inferUseCase(label) {
|
|
|
7953
8100
|
};
|
|
7954
8101
|
return map[label.toLowerCase()] ?? "spa_navigate" /* SPA_NAVIGATE */;
|
|
7955
8102
|
}
|
|
8103
|
+
// src/lib/session.ts
|
|
8104
|
+
init_types();
|
|
8105
|
+
init_types();
|
|
8106
|
+
|
|
7956
8107
|
// src/lib/network.ts
|
|
7957
8108
|
function enableNetworkLogging(page, sessionId) {
|
|
7958
8109
|
const requestStart = new Map;
|
|
@@ -8229,6 +8380,37 @@ function createBunProxy(view) {
|
|
|
8229
8380
|
return view;
|
|
8230
8381
|
}
|
|
8231
8382
|
async function createSession2(opts = {}) {
|
|
8383
|
+
if (opts.cdpUrl) {
|
|
8384
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
8385
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
8386
|
+
const contexts = cdpBrowser.contexts();
|
|
8387
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
8388
|
+
const pages = context.pages();
|
|
8389
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
8390
|
+
const session2 = createSession({
|
|
8391
|
+
engine: "cdp",
|
|
8392
|
+
projectId: opts.projectId,
|
|
8393
|
+
agentId: opts.agentId,
|
|
8394
|
+
startUrl: page2.url(),
|
|
8395
|
+
name: opts.name ?? "attached"
|
|
8396
|
+
});
|
|
8397
|
+
const cleanups2 = [];
|
|
8398
|
+
if (opts.captureNetwork !== false) {
|
|
8399
|
+
try {
|
|
8400
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
8401
|
+
} catch {}
|
|
8402
|
+
}
|
|
8403
|
+
if (opts.captureConsole !== false) {
|
|
8404
|
+
try {
|
|
8405
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
8406
|
+
} catch {}
|
|
8407
|
+
}
|
|
8408
|
+
try {
|
|
8409
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
8410
|
+
} catch {}
|
|
8411
|
+
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 });
|
|
8412
|
+
return { session: session2, page: page2 };
|
|
8413
|
+
}
|
|
8232
8414
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
8233
8415
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
8234
8416
|
let browser = null;
|
|
@@ -8254,7 +8436,22 @@ async function createSession2(opts = {}) {
|
|
|
8254
8436
|
page = await context.newPage();
|
|
8255
8437
|
} else {
|
|
8256
8438
|
browser = await pool.acquire(opts.headless ?? true);
|
|
8257
|
-
|
|
8439
|
+
if (opts.storageState) {
|
|
8440
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
8441
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
8442
|
+
if (statePath2) {
|
|
8443
|
+
const context = await browser.newContext({
|
|
8444
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
8445
|
+
userAgent: opts.userAgent,
|
|
8446
|
+
storageState: statePath2
|
|
8447
|
+
});
|
|
8448
|
+
page = await context.newPage();
|
|
8449
|
+
} else {
|
|
8450
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8451
|
+
}
|
|
8452
|
+
} else {
|
|
8453
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
8454
|
+
}
|
|
8258
8455
|
}
|
|
8259
8456
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
8260
8457
|
try {
|
|
@@ -8452,6 +8649,9 @@ function isAutoGallery(sessionId) {
|
|
|
8452
8649
|
function countActiveSessions2() {
|
|
8453
8650
|
return countActiveSessions();
|
|
8454
8651
|
}
|
|
8652
|
+
// src/lib/actions.ts
|
|
8653
|
+
init_types();
|
|
8654
|
+
|
|
8455
8655
|
// src/lib/snapshot.ts
|
|
8456
8656
|
var lastSnapshots = new Map;
|
|
8457
8657
|
var sessionRefMaps = new Map;
|
|
@@ -8465,6 +8665,66 @@ function getRefLocator(page, sessionId, ref) {
|
|
|
8465
8665
|
return page.getByRole(entry.role, { name: entry.name }).first();
|
|
8466
8666
|
}
|
|
8467
8667
|
|
|
8668
|
+
// src/lib/self-heal.ts
|
|
8669
|
+
async function healSelector(page, selector, sessionId) {
|
|
8670
|
+
const attempts = [];
|
|
8671
|
+
attempts.push(`selector: ${selector}`);
|
|
8672
|
+
try {
|
|
8673
|
+
const loc = page.locator(selector).first();
|
|
8674
|
+
if (await loc.count() > 0) {
|
|
8675
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
8676
|
+
}
|
|
8677
|
+
} catch {}
|
|
8678
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
8679
|
+
attempts.push(`text: "${selector}"`);
|
|
8680
|
+
try {
|
|
8681
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
8682
|
+
if (await loc.count() > 0) {
|
|
8683
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
8684
|
+
}
|
|
8685
|
+
} catch {}
|
|
8686
|
+
}
|
|
8687
|
+
const roleMap = {
|
|
8688
|
+
button: ["button", "submit", "reset"],
|
|
8689
|
+
link: ["a"],
|
|
8690
|
+
input: ["input", "textarea"],
|
|
8691
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
8692
|
+
};
|
|
8693
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
8694
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
8695
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
8696
|
+
try {
|
|
8697
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
8698
|
+
if (await loc.count() > 0) {
|
|
8699
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
8700
|
+
}
|
|
8701
|
+
} catch {}
|
|
8702
|
+
}
|
|
8703
|
+
if (selector.startsWith("#")) {
|
|
8704
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8705
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
8706
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
8707
|
+
try {
|
|
8708
|
+
const loc = page.locator(partialSel).first();
|
|
8709
|
+
if (await loc.count() > 0) {
|
|
8710
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
8711
|
+
}
|
|
8712
|
+
} catch {}
|
|
8713
|
+
}
|
|
8714
|
+
if (selector.startsWith(".")) {
|
|
8715
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
8716
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
8717
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
8718
|
+
try {
|
|
8719
|
+
const loc = page.locator(partialSel).first();
|
|
8720
|
+
if (await loc.count() > 0) {
|
|
8721
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
8722
|
+
}
|
|
8723
|
+
} catch {}
|
|
8724
|
+
}
|
|
8725
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
8726
|
+
}
|
|
8727
|
+
|
|
8468
8728
|
// src/lib/actions.ts
|
|
8469
8729
|
async function click(page, selector, opts) {
|
|
8470
8730
|
try {
|
|
@@ -8474,11 +8734,22 @@ async function click(page, selector, opts) {
|
|
|
8474
8734
|
delay: opts?.delay,
|
|
8475
8735
|
timeout: opts?.timeout ?? 1e4
|
|
8476
8736
|
});
|
|
8477
|
-
|
|
8478
|
-
|
|
8737
|
+
return {};
|
|
8738
|
+
} catch (originalError) {
|
|
8739
|
+
if (opts?.selfHeal !== false) {
|
|
8740
|
+
const result = await healSelector(page, selector);
|
|
8741
|
+
if (result.found && result.locator) {
|
|
8742
|
+
await result.locator.click({
|
|
8743
|
+
button: opts?.button ?? "left",
|
|
8744
|
+
timeout: opts?.timeout ?? 1e4
|
|
8745
|
+
});
|
|
8746
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8747
|
+
}
|
|
8748
|
+
}
|
|
8749
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8479
8750
|
throw new ElementNotFoundError(selector);
|
|
8480
8751
|
}
|
|
8481
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
8752
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
8482
8753
|
}
|
|
8483
8754
|
}
|
|
8484
8755
|
async function type(page, selector, text, opts) {
|
|
@@ -8487,17 +8758,35 @@ async function type(page, selector, text, opts) {
|
|
|
8487
8758
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
8488
8759
|
}
|
|
8489
8760
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8490
|
-
|
|
8491
|
-
|
|
8761
|
+
return {};
|
|
8762
|
+
} catch (originalError) {
|
|
8763
|
+
if (opts?.selfHeal !== false) {
|
|
8764
|
+
const result = await healSelector(page, selector);
|
|
8765
|
+
if (result.found && result.locator) {
|
|
8766
|
+
if (opts?.clear)
|
|
8767
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
8768
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8769
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
8772
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
8492
8773
|
throw new ElementNotFoundError(selector);
|
|
8493
8774
|
}
|
|
8494
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
8775
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
8495
8776
|
}
|
|
8496
8777
|
}
|
|
8497
|
-
async function fill(page, selector, value, timeout = 1e4) {
|
|
8778
|
+
async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
|
|
8498
8779
|
try {
|
|
8499
8780
|
await page.fill(selector, value, { timeout });
|
|
8500
|
-
|
|
8781
|
+
return {};
|
|
8782
|
+
} catch (originalError) {
|
|
8783
|
+
if (selfHeal) {
|
|
8784
|
+
const result = await healSelector(page, selector);
|
|
8785
|
+
if (result.found && result.locator) {
|
|
8786
|
+
await result.locator.fill(value, { timeout });
|
|
8787
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
8788
|
+
}
|
|
8789
|
+
}
|
|
8501
8790
|
throw new ElementNotFoundError(selector);
|
|
8502
8791
|
}
|
|
8503
8792
|
}
|
|
@@ -8623,12 +8912,39 @@ async function clickText(page, text, opts) {
|
|
|
8623
8912
|
}
|
|
8624
8913
|
}, { retries: opts?.retries ?? 1 });
|
|
8625
8914
|
}
|
|
8626
|
-
async function fillForm(page, fields, submitSelector) {
|
|
8915
|
+
async function fillForm(page, fields, submitSelector, selfHeal = true) {
|
|
8627
8916
|
let filled = 0;
|
|
8628
8917
|
const errors = [];
|
|
8918
|
+
const healedFields = [];
|
|
8629
8919
|
for (const [selector, value] of Object.entries(fields)) {
|
|
8630
8920
|
try {
|
|
8631
|
-
|
|
8921
|
+
let el = await page.$(selector);
|
|
8922
|
+
if (!el && selfHeal) {
|
|
8923
|
+
const result = await healSelector(page, selector);
|
|
8924
|
+
if (result.found && result.locator) {
|
|
8925
|
+
const handle = await result.locator.elementHandle();
|
|
8926
|
+
if (handle) {
|
|
8927
|
+
el = handle;
|
|
8928
|
+
healedFields.push(selector);
|
|
8929
|
+
const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
|
|
8930
|
+
const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
|
|
8931
|
+
if (tagName2 === "select") {
|
|
8932
|
+
await result.locator.selectOption(String(value));
|
|
8933
|
+
} else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
|
|
8934
|
+
if (Boolean(value))
|
|
8935
|
+
await result.locator.check();
|
|
8936
|
+
else
|
|
8937
|
+
await result.locator.uncheck();
|
|
8938
|
+
} else {
|
|
8939
|
+
await result.locator.fill(String(value));
|
|
8940
|
+
}
|
|
8941
|
+
filled++;
|
|
8942
|
+
continue;
|
|
8943
|
+
}
|
|
8944
|
+
}
|
|
8945
|
+
errors.push(`${selector}: element not found`);
|
|
8946
|
+
continue;
|
|
8947
|
+
}
|
|
8632
8948
|
if (!el) {
|
|
8633
8949
|
errors.push(`${selector}: element not found`);
|
|
8634
8950
|
continue;
|
|
@@ -8655,11 +8971,21 @@ async function fillForm(page, fields, submitSelector) {
|
|
|
8655
8971
|
if (submitSelector) {
|
|
8656
8972
|
try {
|
|
8657
8973
|
await page.click(submitSelector);
|
|
8658
|
-
} catch (
|
|
8659
|
-
|
|
8974
|
+
} catch (submitErr) {
|
|
8975
|
+
if (selfHeal) {
|
|
8976
|
+
const result = await healSelector(page, submitSelector);
|
|
8977
|
+
if (result.found && result.locator) {
|
|
8978
|
+
await result.locator.click();
|
|
8979
|
+
healedFields.push(submitSelector);
|
|
8980
|
+
} else {
|
|
8981
|
+
errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
8982
|
+
}
|
|
8983
|
+
} else {
|
|
8984
|
+
errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
8985
|
+
}
|
|
8660
8986
|
}
|
|
8661
8987
|
}
|
|
8662
|
-
return { filled, errors, fields_attempted: Object.keys(fields).length };
|
|
8988
|
+
return { filled, errors, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
|
|
8663
8989
|
}
|
|
8664
8990
|
async function waitForText(page, text, opts) {
|
|
8665
8991
|
const timeout = opts?.timeout ?? 1e4;
|
|
@@ -8939,6 +9265,7 @@ async function getPageInfo(page) {
|
|
|
8939
9265
|
};
|
|
8940
9266
|
}
|
|
8941
9267
|
// src/lib/performance.ts
|
|
9268
|
+
init_cdp();
|
|
8942
9269
|
async function getPerformanceMetrics(page) {
|
|
8943
9270
|
const navTiming = await page.evaluate(() => {
|
|
8944
9271
|
const t = performance.timing;
|
|
@@ -9026,10 +9353,11 @@ async function startCoverage(page) {
|
|
|
9026
9353
|
};
|
|
9027
9354
|
}
|
|
9028
9355
|
// src/lib/screenshot.ts
|
|
9356
|
+
init_types();
|
|
9029
9357
|
var import_sharp = __toESM(require_lib(), 1);
|
|
9030
|
-
import { join as
|
|
9031
|
-
import { mkdirSync as
|
|
9032
|
-
import { homedir as
|
|
9358
|
+
import { join as join4 } from "path";
|
|
9359
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
9360
|
+
import { homedir as homedir4 } from "os";
|
|
9033
9361
|
|
|
9034
9362
|
// src/db/gallery.ts
|
|
9035
9363
|
init_schema();
|
|
@@ -9075,13 +9403,13 @@ function getEntry(id) {
|
|
|
9075
9403
|
|
|
9076
9404
|
// src/lib/screenshot.ts
|
|
9077
9405
|
function getDataDir2() {
|
|
9078
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
9406
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
9079
9407
|
}
|
|
9080
9408
|
function getScreenshotDir(projectId) {
|
|
9081
|
-
const base =
|
|
9409
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
9082
9410
|
const date = new Date().toISOString().split("T")[0];
|
|
9083
|
-
const dir = projectId ?
|
|
9084
|
-
|
|
9411
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
9412
|
+
mkdirSync4(dir, { recursive: true });
|
|
9085
9413
|
return dir;
|
|
9086
9414
|
}
|
|
9087
9415
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -9096,7 +9424,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
9096
9424
|
}
|
|
9097
9425
|
}
|
|
9098
9426
|
async function generateThumbnail(raw, dir, stem) {
|
|
9099
|
-
const thumbPath =
|
|
9427
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
9100
9428
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
9101
9429
|
await Bun.write(thumbPath, thumbBuffer);
|
|
9102
9430
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -9153,7 +9481,7 @@ async function takeScreenshot(page, opts) {
|
|
|
9153
9481
|
const compressedSizeBytes = finalBuffer.length;
|
|
9154
9482
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
9155
9483
|
const ext = format;
|
|
9156
|
-
const screenshotPath = opts?.path ??
|
|
9484
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
9157
9485
|
await Bun.write(screenshotPath, finalBuffer);
|
|
9158
9486
|
let thumbnailPath;
|
|
9159
9487
|
let thumbnailBase64;
|
|
@@ -9213,12 +9541,12 @@ async function takeScreenshot(page, opts) {
|
|
|
9213
9541
|
}
|
|
9214
9542
|
async function generatePDF(page, opts) {
|
|
9215
9543
|
try {
|
|
9216
|
-
const base =
|
|
9544
|
+
const base = join4(getDataDir2(), "pdfs");
|
|
9217
9545
|
const date = new Date().toISOString().split("T")[0];
|
|
9218
|
-
const dir = opts?.projectId ?
|
|
9219
|
-
|
|
9546
|
+
const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
|
|
9547
|
+
mkdirSync4(dir, { recursive: true });
|
|
9220
9548
|
const timestamp = Date.now();
|
|
9221
|
-
const pdfPath = opts?.path ??
|
|
9549
|
+
const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
|
|
9222
9550
|
const buffer = await page.pdf({
|
|
9223
9551
|
path: pdfPath,
|
|
9224
9552
|
format: opts?.format ?? "A4",
|
|
@@ -9308,6 +9636,7 @@ async function getIndexedDB(page, dbName, storeName) {
|
|
|
9308
9636
|
}), [dbName, storeName]);
|
|
9309
9637
|
}
|
|
9310
9638
|
// src/lib/recorder.ts
|
|
9639
|
+
init_types();
|
|
9311
9640
|
var activeRecordings = new Map;
|
|
9312
9641
|
function startRecording(sessionId, name, startUrl) {
|
|
9313
9642
|
const steps = [];
|
|
@@ -9466,6 +9795,7 @@ function exportRecording(recordingId, format = "json") {
|
|
|
9466
9795
|
`);
|
|
9467
9796
|
}
|
|
9468
9797
|
// src/lib/crawler.ts
|
|
9798
|
+
init_types();
|
|
9469
9799
|
async function crawl(startUrl, opts = {}) {
|
|
9470
9800
|
const maxDepth = opts.maxDepth ?? 2;
|
|
9471
9801
|
const maxPages = opts.maxPages ?? 50;
|
|
@@ -9678,6 +10008,7 @@ export {
|
|
|
9678
10008
|
createCrawlResult,
|
|
9679
10009
|
crawl,
|
|
9680
10010
|
countActiveSessions2 as countActiveSessions,
|
|
10011
|
+
connectToExistingBrowser,
|
|
9681
10012
|
connectLightpanda,
|
|
9682
10013
|
closeSession2 as closeSession,
|
|
9683
10014
|
closePage,
|