@hasna/browser 0.0.7 → 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 +2095 -609
- package/dist/db/sessions.d.ts +15 -0
- package/dist/db/sessions.d.ts.map +1 -1
- package/dist/db/timeline.d.ts +11 -0
- package/dist/db/timeline.d.ts.map +1 -0
- package/dist/engines/cdp.d.ts +2 -1
- package/dist/engines/cdp.d.ts.map +1 -1
- package/dist/engines/playwright.d.ts +1 -1
- package/dist/engines/playwright.d.ts.map +1 -1
- package/dist/index.js +614 -226
- 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 +7 -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 +2480 -1041
- package/dist/server/index.js +472 -172
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -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";
|
|
@@ -211,6 +284,55 @@ function runMigrations(db) {
|
|
|
211
284
|
CREATE INDEX IF NOT EXISTS idx_gallery_favorite ON gallery_entries(is_favorite);
|
|
212
285
|
CREATE INDEX IF NOT EXISTS idx_gallery_created ON gallery_entries(created_at);
|
|
213
286
|
`
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
version: 3,
|
|
290
|
+
sql: `
|
|
291
|
+
-- Session lock/claim for multi-agent ownership
|
|
292
|
+
ALTER TABLE sessions ADD COLUMN locked_by TEXT;
|
|
293
|
+
ALTER TABLE sessions ADD COLUMN locked_at TEXT;
|
|
294
|
+
`
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
version: 4,
|
|
298
|
+
sql: `
|
|
299
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
300
|
+
id TEXT PRIMARY KEY,
|
|
301
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
302
|
+
event_type TEXT NOT NULL,
|
|
303
|
+
details TEXT DEFAULT '{}',
|
|
304
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
305
|
+
);
|
|
306
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp);
|
|
307
|
+
`
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
version: 5,
|
|
311
|
+
sql: `
|
|
312
|
+
CREATE TABLE IF NOT EXISTS session_tags (
|
|
313
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
314
|
+
tag TEXT NOT NULL,
|
|
315
|
+
PRIMARY KEY (session_id, tag)
|
|
316
|
+
);
|
|
317
|
+
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
|
|
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
|
+
`
|
|
214
336
|
}
|
|
215
337
|
];
|
|
216
338
|
for (const m of migrations) {
|
|
@@ -259,6 +381,188 @@ var init_console_log = __esm(() => {
|
|
|
259
381
|
init_schema();
|
|
260
382
|
});
|
|
261
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
|
+
|
|
262
566
|
// node_modules/sharp/lib/is.js
|
|
263
567
|
var require_is = __commonJS((exports, module) => {
|
|
264
568
|
/*!
|
|
@@ -6654,90 +6958,13 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
6654
6958
|
module.exports = Sharp;
|
|
6655
6959
|
});
|
|
6656
6960
|
|
|
6657
|
-
// src/types/index.ts
|
|
6658
|
-
var UseCase;
|
|
6659
|
-
((UseCase2) => {
|
|
6660
|
-
UseCase2["SCRAPE"] = "scrape";
|
|
6661
|
-
UseCase2["EXTRACT_LINKS"] = "extract_links";
|
|
6662
|
-
UseCase2["STATUS_CHECK"] = "status_check";
|
|
6663
|
-
UseCase2["FORM_FILL"] = "form_fill";
|
|
6664
|
-
UseCase2["SPA_NAVIGATE"] = "spa_navigate";
|
|
6665
|
-
UseCase2["SCREENSHOT"] = "screenshot";
|
|
6666
|
-
UseCase2["AUTH_FLOW"] = "auth_flow";
|
|
6667
|
-
UseCase2["MULTI_TAB"] = "multi_tab";
|
|
6668
|
-
UseCase2["NETWORK_MONITOR"] = "network_monitor";
|
|
6669
|
-
UseCase2["HAR_CAPTURE"] = "har_capture";
|
|
6670
|
-
UseCase2["PERF_PROFILE"] = "perf_profile";
|
|
6671
|
-
UseCase2["SCRIPT_INJECT"] = "script_inject";
|
|
6672
|
-
UseCase2["COVERAGE"] = "coverage";
|
|
6673
|
-
UseCase2["RECORD_REPLAY"] = "record_replay";
|
|
6674
|
-
})(UseCase ||= {});
|
|
6675
|
-
|
|
6676
|
-
class BrowserError extends Error {
|
|
6677
|
-
code;
|
|
6678
|
-
retryable;
|
|
6679
|
-
constructor(message, code = "BROWSER_ERROR", retryable = false) {
|
|
6680
|
-
super(message);
|
|
6681
|
-
this.code = code;
|
|
6682
|
-
this.retryable = retryable;
|
|
6683
|
-
this.name = "BrowserError";
|
|
6684
|
-
}
|
|
6685
|
-
}
|
|
6686
|
-
|
|
6687
|
-
class SessionNotFoundError extends BrowserError {
|
|
6688
|
-
constructor(id) {
|
|
6689
|
-
super(`Session not found: ${id}`, "SESSION_NOT_FOUND", false);
|
|
6690
|
-
this.name = "SessionNotFoundError";
|
|
6691
|
-
}
|
|
6692
|
-
}
|
|
6693
|
-
|
|
6694
|
-
class EngineNotAvailableError extends BrowserError {
|
|
6695
|
-
constructor(engine, reason) {
|
|
6696
|
-
super(`Engine '${engine}' is not available${reason ? `: ${reason}` : ""}`, "ENGINE_NOT_AVAILABLE", false);
|
|
6697
|
-
this.name = "EngineNotAvailableError";
|
|
6698
|
-
}
|
|
6699
|
-
}
|
|
6700
|
-
|
|
6701
|
-
class NavigationError extends BrowserError {
|
|
6702
|
-
constructor(url, reason) {
|
|
6703
|
-
super(`Navigation to '${url}' failed${reason ? `: ${reason}` : ""}`, "NAVIGATION_ERROR", true);
|
|
6704
|
-
this.name = "NavigationError";
|
|
6705
|
-
}
|
|
6706
|
-
}
|
|
6707
|
-
|
|
6708
|
-
class ElementNotFoundError extends BrowserError {
|
|
6709
|
-
constructor(selector) {
|
|
6710
|
-
super(`Element not found: ${selector}`, "ELEMENT_NOT_FOUND", false);
|
|
6711
|
-
this.name = "ElementNotFoundError";
|
|
6712
|
-
}
|
|
6713
|
-
}
|
|
6714
|
-
|
|
6715
|
-
class RecordingNotFoundError extends BrowserError {
|
|
6716
|
-
constructor(id) {
|
|
6717
|
-
super(`Recording not found: ${id}`, "RECORDING_NOT_FOUND", false);
|
|
6718
|
-
this.name = "RecordingNotFoundError";
|
|
6719
|
-
}
|
|
6720
|
-
}
|
|
6721
|
-
|
|
6722
|
-
class AgentNotFoundError extends BrowserError {
|
|
6723
|
-
constructor(id) {
|
|
6724
|
-
super(`Agent not found: ${id}`, "AGENT_NOT_FOUND", false);
|
|
6725
|
-
this.name = "AgentNotFoundError";
|
|
6726
|
-
}
|
|
6727
|
-
}
|
|
6728
|
-
|
|
6729
|
-
class ProjectNotFoundError extends BrowserError {
|
|
6730
|
-
constructor(id) {
|
|
6731
|
-
super(`Project not found: ${id}`, "PROJECT_NOT_FOUND", false);
|
|
6732
|
-
this.name = "ProjectNotFoundError";
|
|
6733
|
-
}
|
|
6734
|
-
}
|
|
6735
|
-
|
|
6736
6961
|
// src/index.ts
|
|
6737
6962
|
init_schema();
|
|
6963
|
+
init_types();
|
|
6738
6964
|
|
|
6739
6965
|
// src/db/projects.ts
|
|
6740
6966
|
init_schema();
|
|
6967
|
+
init_types();
|
|
6741
6968
|
import { randomUUID } from "crypto";
|
|
6742
6969
|
function createProject(data) {
|
|
6743
6970
|
const db = getDatabase();
|
|
@@ -6795,6 +7022,7 @@ function deleteProject(id) {
|
|
|
6795
7022
|
}
|
|
6796
7023
|
// src/db/agents.ts
|
|
6797
7024
|
init_schema();
|
|
7025
|
+
init_types();
|
|
6798
7026
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6799
7027
|
function registerAgent(name, opts = {}) {
|
|
6800
7028
|
const db = getDatabase();
|
|
@@ -6875,6 +7103,7 @@ function cleanStaleAgents(thresholdMs) {
|
|
|
6875
7103
|
}
|
|
6876
7104
|
// src/db/sessions.ts
|
|
6877
7105
|
init_schema();
|
|
7106
|
+
init_types();
|
|
6878
7107
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6879
7108
|
function createSession(data) {
|
|
6880
7109
|
const db = getDatabase();
|
|
@@ -6927,8 +7156,24 @@ function updateSessionStatus(id, status) {
|
|
|
6927
7156
|
return getSession(id);
|
|
6928
7157
|
}
|
|
6929
7158
|
function closeSession(id) {
|
|
7159
|
+
const db = getDatabase();
|
|
7160
|
+
db.prepare("UPDATE sessions SET locked_by = NULL, locked_at = NULL WHERE id = ?").run(id);
|
|
6930
7161
|
return updateSessionStatus(id, "closed");
|
|
6931
7162
|
}
|
|
7163
|
+
function getActiveSessionForAgent(agentId) {
|
|
7164
|
+
const db = getDatabase();
|
|
7165
|
+
return db.query("SELECT * FROM sessions WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1").get(agentId) ?? null;
|
|
7166
|
+
}
|
|
7167
|
+
function getDefaultActiveSession() {
|
|
7168
|
+
const db = getDatabase();
|
|
7169
|
+
const rows = db.query("SELECT * FROM sessions WHERE status = 'active' ORDER BY created_at DESC LIMIT 2").all();
|
|
7170
|
+
return rows.length === 1 ? rows[0] : null;
|
|
7171
|
+
}
|
|
7172
|
+
function countActiveSessions() {
|
|
7173
|
+
const db = getDatabase();
|
|
7174
|
+
const row = db.query("SELECT COUNT(*) as count FROM sessions WHERE status = 'active'").get();
|
|
7175
|
+
return row?.count ?? 0;
|
|
7176
|
+
}
|
|
6932
7177
|
function deleteSession(id) {
|
|
6933
7178
|
const db = getDatabase();
|
|
6934
7179
|
db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
@@ -6991,6 +7236,7 @@ init_console_log();
|
|
|
6991
7236
|
|
|
6992
7237
|
// src/db/recordings.ts
|
|
6993
7238
|
init_schema();
|
|
7239
|
+
init_types();
|
|
6994
7240
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
6995
7241
|
function deserialize(row) {
|
|
6996
7242
|
return {
|
|
@@ -7105,6 +7351,7 @@ function cleanOldHeartbeats(olderThanMs) {
|
|
|
7105
7351
|
return result.changes;
|
|
7106
7352
|
}
|
|
7107
7353
|
// src/engines/playwright.ts
|
|
7354
|
+
init_types();
|
|
7108
7355
|
import { chromium } from "playwright";
|
|
7109
7356
|
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
7110
7357
|
async function launchPlaywright(options) {
|
|
@@ -7144,14 +7391,14 @@ class BrowserPool {
|
|
|
7144
7391
|
this.maxSize = maxSize;
|
|
7145
7392
|
this.options = options;
|
|
7146
7393
|
}
|
|
7147
|
-
async acquire() {
|
|
7394
|
+
async acquire(headless = true) {
|
|
7148
7395
|
const available = this.pool.find((e) => !e.inUse);
|
|
7149
7396
|
if (available) {
|
|
7150
7397
|
available.inUse = true;
|
|
7151
7398
|
return available.browser;
|
|
7152
7399
|
}
|
|
7153
7400
|
if (this.pool.length < this.maxSize) {
|
|
7154
|
-
const browser = await launchPlaywright(this.options);
|
|
7401
|
+
const browser = await launchPlaywright({ ...this.options, headless });
|
|
7155
7402
|
this.pool.push({ browser, inUse: true, createdAt: Date.now() });
|
|
7156
7403
|
return browser;
|
|
7157
7404
|
}
|
|
@@ -7182,115 +7429,12 @@ class BrowserPool {
|
|
|
7182
7429
|
return this.pool.filter((e) => !e.inUse).length;
|
|
7183
7430
|
}
|
|
7184
7431
|
}
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
performanceEnabled = false;
|
|
7190
|
-
constructor(session) {
|
|
7191
|
-
this.session = session;
|
|
7192
|
-
}
|
|
7193
|
-
static async fromPage(page) {
|
|
7194
|
-
try {
|
|
7195
|
-
const session = await page.context().newCDPSession(page);
|
|
7196
|
-
return new CDPClient(session);
|
|
7197
|
-
} catch (err) {
|
|
7198
|
-
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
7199
|
-
}
|
|
7200
|
-
}
|
|
7201
|
-
async send(method, params) {
|
|
7202
|
-
try {
|
|
7203
|
-
return await this.session.send(method, params);
|
|
7204
|
-
} catch (err) {
|
|
7205
|
-
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
7206
|
-
}
|
|
7207
|
-
}
|
|
7208
|
-
on(event, handler) {
|
|
7209
|
-
this.session.on(event, handler);
|
|
7210
|
-
}
|
|
7211
|
-
off(event, handler) {
|
|
7212
|
-
this.session.off(event, handler);
|
|
7213
|
-
}
|
|
7214
|
-
async enableNetwork() {
|
|
7215
|
-
if (!this.networkEnabled) {
|
|
7216
|
-
await this.send("Network.enable");
|
|
7217
|
-
this.networkEnabled = true;
|
|
7218
|
-
}
|
|
7219
|
-
}
|
|
7220
|
-
async enablePerformance() {
|
|
7221
|
-
if (!this.performanceEnabled) {
|
|
7222
|
-
await this.send("Performance.enable");
|
|
7223
|
-
this.performanceEnabled = true;
|
|
7224
|
-
}
|
|
7225
|
-
}
|
|
7226
|
-
async getPerformanceMetrics() {
|
|
7227
|
-
await this.enablePerformance();
|
|
7228
|
-
const result = await this.send("Performance.getMetrics");
|
|
7229
|
-
const m = {};
|
|
7230
|
-
for (const metric of result.metrics) {
|
|
7231
|
-
m[metric.name] = metric.value;
|
|
7232
|
-
}
|
|
7233
|
-
return {
|
|
7234
|
-
js_heap_size_used: m["JSHeapUsedSize"],
|
|
7235
|
-
js_heap_size_total: m["JSHeapTotalSize"],
|
|
7236
|
-
dom_interactive: m["DOMInteractive"],
|
|
7237
|
-
dom_complete: m["DOMComplete"],
|
|
7238
|
-
load_event: m["LoadEventEnd"]
|
|
7239
|
-
};
|
|
7240
|
-
}
|
|
7241
|
-
async startJSCoverage() {
|
|
7242
|
-
await this.send("Profiler.enable");
|
|
7243
|
-
await this.send("Debugger.enable");
|
|
7244
|
-
await this.send("Profiler.startPreciseCoverage", {
|
|
7245
|
-
callCount: false,
|
|
7246
|
-
detailed: true
|
|
7247
|
-
});
|
|
7248
|
-
}
|
|
7249
|
-
async stopJSCoverage() {
|
|
7250
|
-
const result = await this.send("Profiler.takePreciseCoverage");
|
|
7251
|
-
await this.send("Profiler.stopPreciseCoverage");
|
|
7252
|
-
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
7253
|
-
url: r.url,
|
|
7254
|
-
text: "",
|
|
7255
|
-
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
7256
|
-
}));
|
|
7257
|
-
}
|
|
7258
|
-
async getCoverage() {
|
|
7259
|
-
await this.startJSCoverage();
|
|
7260
|
-
const js = await this.stopJSCoverage();
|
|
7261
|
-
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
7262
|
-
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
7263
|
-
}
|
|
7264
|
-
async captureHAREntries(page, handler) {
|
|
7265
|
-
await this.enableNetwork();
|
|
7266
|
-
const requestTimings = new Map;
|
|
7267
|
-
const onRequest = (params) => {
|
|
7268
|
-
requestTimings.set(params.requestId, params.timestamp);
|
|
7269
|
-
};
|
|
7270
|
-
const onResponse = (params) => {
|
|
7271
|
-
const start = requestTimings.get(params.requestId);
|
|
7272
|
-
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
7273
|
-
handler({
|
|
7274
|
-
method: "GET",
|
|
7275
|
-
url: params.response.url,
|
|
7276
|
-
status: params.response.status,
|
|
7277
|
-
duration
|
|
7278
|
-
});
|
|
7279
|
-
};
|
|
7280
|
-
this.on("Network.requestWillBeSent", onRequest);
|
|
7281
|
-
this.on("Network.responseReceived", onResponse);
|
|
7282
|
-
return () => {
|
|
7283
|
-
this.off("Network.requestWillBeSent", onRequest);
|
|
7284
|
-
this.off("Network.responseReceived", onResponse);
|
|
7285
|
-
};
|
|
7286
|
-
}
|
|
7287
|
-
async detach() {
|
|
7288
|
-
try {
|
|
7289
|
-
await this.session.detach();
|
|
7290
|
-
} catch {}
|
|
7291
|
-
}
|
|
7292
|
-
}
|
|
7432
|
+
|
|
7433
|
+
// src/index.ts
|
|
7434
|
+
init_cdp();
|
|
7435
|
+
|
|
7293
7436
|
// src/engines/lightpanda.ts
|
|
7437
|
+
init_types();
|
|
7294
7438
|
import { execSync, spawn } from "child_process";
|
|
7295
7439
|
import { chromium as chromium2 } from "playwright";
|
|
7296
7440
|
var DEFAULT_LIGHTPANDA_PORT = 9222;
|
|
@@ -7432,6 +7576,9 @@ class LightpandaPage {
|
|
|
7432
7576
|
await this.page.context().close();
|
|
7433
7577
|
}
|
|
7434
7578
|
}
|
|
7579
|
+
// src/engines/selector.ts
|
|
7580
|
+
init_types();
|
|
7581
|
+
|
|
7435
7582
|
// src/engines/bun-webview.ts
|
|
7436
7583
|
import { join as join2 } from "path";
|
|
7437
7584
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -7905,6 +8052,10 @@ function inferUseCase(label) {
|
|
|
7905
8052
|
};
|
|
7906
8053
|
return map[label.toLowerCase()] ?? "spa_navigate" /* SPA_NAVIGATE */;
|
|
7907
8054
|
}
|
|
8055
|
+
// src/lib/session.ts
|
|
8056
|
+
init_types();
|
|
8057
|
+
init_types();
|
|
8058
|
+
|
|
7908
8059
|
// src/lib/network.ts
|
|
7909
8060
|
function enableNetworkLogging(page, sessionId) {
|
|
7910
8061
|
const requestStart = new Map;
|
|
@@ -8163,10 +8314,55 @@ function setupDialogHandler(page, sessionId) {
|
|
|
8163
8314
|
|
|
8164
8315
|
// src/lib/session.ts
|
|
8165
8316
|
var handles = new Map;
|
|
8317
|
+
var pool = new BrowserPool(5);
|
|
8318
|
+
var SESSION_TTL_MS = parseInt(process.env["SESSION_TTL_MINUTES"] ?? "10", 10) * 60000;
|
|
8319
|
+
var ttlInterval = setInterval(async () => {
|
|
8320
|
+
const now = Date.now();
|
|
8321
|
+
for (const [id, handle] of handles) {
|
|
8322
|
+
if (now - handle.lastActivity > SESSION_TTL_MS) {
|
|
8323
|
+
try {
|
|
8324
|
+
await closeSession2(id);
|
|
8325
|
+
} catch {}
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
}, 60000);
|
|
8329
|
+
if (ttlInterval.unref)
|
|
8330
|
+
ttlInterval.unref();
|
|
8166
8331
|
function createBunProxy(view) {
|
|
8167
8332
|
return view;
|
|
8168
8333
|
}
|
|
8169
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
|
+
}
|
|
8170
8366
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
8171
8367
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
8172
8368
|
let browser = null;
|
|
@@ -8191,12 +8387,23 @@ async function createSession2(opts = {}) {
|
|
|
8191
8387
|
const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
|
|
8192
8388
|
page = await context.newPage();
|
|
8193
8389
|
} else {
|
|
8194
|
-
browser = await
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8390
|
+
browser = await pool.acquire(opts.headless ?? true);
|
|
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
|
+
}
|
|
8200
8407
|
}
|
|
8201
8408
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
8202
8409
|
try {
|
|
@@ -8249,7 +8456,7 @@ async function createSession2(opts = {}) {
|
|
|
8249
8456
|
} catch {}
|
|
8250
8457
|
}
|
|
8251
8458
|
}
|
|
8252
|
-
handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
8459
|
+
handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
|
|
8253
8460
|
if (opts.startUrl) {
|
|
8254
8461
|
try {
|
|
8255
8462
|
if (bunView) {
|
|
@@ -8275,6 +8482,7 @@ function getSessionPage(sessionId) {
|
|
|
8275
8482
|
handles.delete(sessionId);
|
|
8276
8483
|
throw new SessionNotFoundError(sessionId);
|
|
8277
8484
|
}
|
|
8485
|
+
handle.lastActivity = Date.now();
|
|
8278
8486
|
return handle.page;
|
|
8279
8487
|
}
|
|
8280
8488
|
function getSessionBunView(sessionId) {
|
|
@@ -8322,10 +8530,8 @@ async function closeSession2(sessionId) {
|
|
|
8322
8530
|
try {
|
|
8323
8531
|
await handle.page.context().close();
|
|
8324
8532
|
} catch {}
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
await closeBrowser(handle.browser);
|
|
8328
|
-
} catch {}
|
|
8533
|
+
if (handle.browser)
|
|
8534
|
+
pool.release(handle.browser);
|
|
8329
8535
|
}
|
|
8330
8536
|
handles.delete(sessionId);
|
|
8331
8537
|
}
|
|
@@ -8341,6 +8547,7 @@ async function closeAllSessions() {
|
|
|
8341
8547
|
for (const [id] of handles) {
|
|
8342
8548
|
await closeSession2(id).catch(() => {});
|
|
8343
8549
|
}
|
|
8550
|
+
await pool.destroyAll();
|
|
8344
8551
|
}
|
|
8345
8552
|
function getSessionByName2(name) {
|
|
8346
8553
|
return getSessionByName(name);
|
|
@@ -8352,6 +8559,51 @@ function getTokenBudget(sessionId) {
|
|
|
8352
8559
|
const handle = handles.get(sessionId);
|
|
8353
8560
|
return handle ? handle.tokenBudget : null;
|
|
8354
8561
|
}
|
|
8562
|
+
function getActiveSessionForAgent2(agentId) {
|
|
8563
|
+
const session = getActiveSessionForAgent(agentId);
|
|
8564
|
+
if (!session)
|
|
8565
|
+
return null;
|
|
8566
|
+
const handle = handles.get(session.id);
|
|
8567
|
+
if (!handle)
|
|
8568
|
+
return null;
|
|
8569
|
+
try {
|
|
8570
|
+
if (handle.bunView)
|
|
8571
|
+
handle.bunView.url();
|
|
8572
|
+
else
|
|
8573
|
+
handle.page.url();
|
|
8574
|
+
} catch {
|
|
8575
|
+
handles.delete(session.id);
|
|
8576
|
+
return null;
|
|
8577
|
+
}
|
|
8578
|
+
return { session, page: handle.page };
|
|
8579
|
+
}
|
|
8580
|
+
function getDefaultSession() {
|
|
8581
|
+
const session = getDefaultActiveSession();
|
|
8582
|
+
if (!session)
|
|
8583
|
+
return null;
|
|
8584
|
+
const handle = handles.get(session.id);
|
|
8585
|
+
if (!handle)
|
|
8586
|
+
return null;
|
|
8587
|
+
try {
|
|
8588
|
+
if (handle.bunView)
|
|
8589
|
+
handle.bunView.url();
|
|
8590
|
+
else
|
|
8591
|
+
handle.page.url();
|
|
8592
|
+
} catch {
|
|
8593
|
+
handles.delete(session.id);
|
|
8594
|
+
return null;
|
|
8595
|
+
}
|
|
8596
|
+
return { session, page: handle.page };
|
|
8597
|
+
}
|
|
8598
|
+
function isAutoGallery(sessionId) {
|
|
8599
|
+
return handles.get(sessionId)?.autoGallery ?? false;
|
|
8600
|
+
}
|
|
8601
|
+
function countActiveSessions2() {
|
|
8602
|
+
return countActiveSessions();
|
|
8603
|
+
}
|
|
8604
|
+
// src/lib/actions.ts
|
|
8605
|
+
init_types();
|
|
8606
|
+
|
|
8355
8607
|
// src/lib/snapshot.ts
|
|
8356
8608
|
var lastSnapshots = new Map;
|
|
8357
8609
|
var sessionRefMaps = new Map;
|
|
@@ -8365,6 +8617,66 @@ function getRefLocator(page, sessionId, ref) {
|
|
|
8365
8617
|
return page.getByRole(entry.role, { name: entry.name }).first();
|
|
8366
8618
|
}
|
|
8367
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
|
+
|
|
8368
8680
|
// src/lib/actions.ts
|
|
8369
8681
|
async function click(page, selector, opts) {
|
|
8370
8682
|
try {
|
|
@@ -8374,11 +8686,22 @@ async function click(page, selector, opts) {
|
|
|
8374
8686
|
delay: opts?.delay,
|
|
8375
8687
|
timeout: opts?.timeout ?? 1e4
|
|
8376
8688
|
});
|
|
8377
|
-
|
|
8378
|
-
|
|
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")) {
|
|
8379
8702
|
throw new ElementNotFoundError(selector);
|
|
8380
8703
|
}
|
|
8381
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
8704
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
8382
8705
|
}
|
|
8383
8706
|
}
|
|
8384
8707
|
async function type(page, selector, text, opts) {
|
|
@@ -8387,17 +8710,35 @@ async function type(page, selector, text, opts) {
|
|
|
8387
8710
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
8388
8711
|
}
|
|
8389
8712
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8390
|
-
|
|
8391
|
-
|
|
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")) {
|
|
8392
8725
|
throw new ElementNotFoundError(selector);
|
|
8393
8726
|
}
|
|
8394
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
8727
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
8395
8728
|
}
|
|
8396
8729
|
}
|
|
8397
|
-
async function fill(page, selector, value, timeout = 1e4) {
|
|
8730
|
+
async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
|
|
8398
8731
|
try {
|
|
8399
8732
|
await page.fill(selector, value, { timeout });
|
|
8400
|
-
|
|
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
|
+
}
|
|
8401
8742
|
throw new ElementNotFoundError(selector);
|
|
8402
8743
|
}
|
|
8403
8744
|
}
|
|
@@ -8523,12 +8864,39 @@ async function clickText(page, text, opts) {
|
|
|
8523
8864
|
}
|
|
8524
8865
|
}, { retries: opts?.retries ?? 1 });
|
|
8525
8866
|
}
|
|
8526
|
-
async function fillForm(page, fields, submitSelector) {
|
|
8867
|
+
async function fillForm(page, fields, submitSelector, selfHeal = true) {
|
|
8527
8868
|
let filled = 0;
|
|
8528
8869
|
const errors = [];
|
|
8870
|
+
const healedFields = [];
|
|
8529
8871
|
for (const [selector, value] of Object.entries(fields)) {
|
|
8530
8872
|
try {
|
|
8531
|
-
|
|
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
|
+
}
|
|
8532
8900
|
if (!el) {
|
|
8533
8901
|
errors.push(`${selector}: element not found`);
|
|
8534
8902
|
continue;
|
|
@@ -8555,11 +8923,21 @@ async function fillForm(page, fields, submitSelector) {
|
|
|
8555
8923
|
if (submitSelector) {
|
|
8556
8924
|
try {
|
|
8557
8925
|
await page.click(submitSelector);
|
|
8558
|
-
} catch (
|
|
8559
|
-
|
|
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
|
+
}
|
|
8560
8938
|
}
|
|
8561
8939
|
}
|
|
8562
|
-
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 } : {} };
|
|
8563
8941
|
}
|
|
8564
8942
|
async function waitForText(page, text, opts) {
|
|
8565
8943
|
const timeout = opts?.timeout ?? 1e4;
|
|
@@ -8839,6 +9217,7 @@ async function getPageInfo(page) {
|
|
|
8839
9217
|
};
|
|
8840
9218
|
}
|
|
8841
9219
|
// src/lib/performance.ts
|
|
9220
|
+
init_cdp();
|
|
8842
9221
|
async function getPerformanceMetrics(page) {
|
|
8843
9222
|
const navTiming = await page.evaluate(() => {
|
|
8844
9223
|
const t = performance.timing;
|
|
@@ -8926,10 +9305,11 @@ async function startCoverage(page) {
|
|
|
8926
9305
|
};
|
|
8927
9306
|
}
|
|
8928
9307
|
// src/lib/screenshot.ts
|
|
9308
|
+
init_types();
|
|
8929
9309
|
var import_sharp = __toESM(require_lib(), 1);
|
|
8930
|
-
import { join as
|
|
8931
|
-
import { mkdirSync as
|
|
8932
|
-
import { homedir as
|
|
9310
|
+
import { join as join4 } from "path";
|
|
9311
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
9312
|
+
import { homedir as homedir4 } from "os";
|
|
8933
9313
|
|
|
8934
9314
|
// src/db/gallery.ts
|
|
8935
9315
|
init_schema();
|
|
@@ -8975,13 +9355,13 @@ function getEntry(id) {
|
|
|
8975
9355
|
|
|
8976
9356
|
// src/lib/screenshot.ts
|
|
8977
9357
|
function getDataDir2() {
|
|
8978
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
9358
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
8979
9359
|
}
|
|
8980
9360
|
function getScreenshotDir(projectId) {
|
|
8981
|
-
const base =
|
|
9361
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
8982
9362
|
const date = new Date().toISOString().split("T")[0];
|
|
8983
|
-
const dir = projectId ?
|
|
8984
|
-
|
|
9363
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
9364
|
+
mkdirSync4(dir, { recursive: true });
|
|
8985
9365
|
return dir;
|
|
8986
9366
|
}
|
|
8987
9367
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -8996,7 +9376,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
8996
9376
|
}
|
|
8997
9377
|
}
|
|
8998
9378
|
async function generateThumbnail(raw, dir, stem) {
|
|
8999
|
-
const thumbPath =
|
|
9379
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
9000
9380
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
9001
9381
|
await Bun.write(thumbPath, thumbBuffer);
|
|
9002
9382
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -9053,7 +9433,7 @@ async function takeScreenshot(page, opts) {
|
|
|
9053
9433
|
const compressedSizeBytes = finalBuffer.length;
|
|
9054
9434
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
9055
9435
|
const ext = format;
|
|
9056
|
-
const screenshotPath = opts?.path ??
|
|
9436
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
9057
9437
|
await Bun.write(screenshotPath, finalBuffer);
|
|
9058
9438
|
let thumbnailPath;
|
|
9059
9439
|
let thumbnailBase64;
|
|
@@ -9113,12 +9493,12 @@ async function takeScreenshot(page, opts) {
|
|
|
9113
9493
|
}
|
|
9114
9494
|
async function generatePDF(page, opts) {
|
|
9115
9495
|
try {
|
|
9116
|
-
const base =
|
|
9496
|
+
const base = join4(getDataDir2(), "pdfs");
|
|
9117
9497
|
const date = new Date().toISOString().split("T")[0];
|
|
9118
|
-
const dir = opts?.projectId ?
|
|
9119
|
-
|
|
9498
|
+
const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
|
|
9499
|
+
mkdirSync4(dir, { recursive: true });
|
|
9120
9500
|
const timestamp = Date.now();
|
|
9121
|
-
const pdfPath = opts?.path ??
|
|
9501
|
+
const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
|
|
9122
9502
|
const buffer = await page.pdf({
|
|
9123
9503
|
path: pdfPath,
|
|
9124
9504
|
format: opts?.format ?? "A4",
|
|
@@ -9208,6 +9588,7 @@ async function getIndexedDB(page, dbName, storeName) {
|
|
|
9208
9588
|
}), [dbName, storeName]);
|
|
9209
9589
|
}
|
|
9210
9590
|
// src/lib/recorder.ts
|
|
9591
|
+
init_types();
|
|
9211
9592
|
var activeRecordings = new Map;
|
|
9212
9593
|
function startRecording(sessionId, name, startUrl) {
|
|
9213
9594
|
const steps = [];
|
|
@@ -9366,6 +9747,7 @@ function exportRecording(recordingId, format = "json") {
|
|
|
9366
9747
|
`);
|
|
9367
9748
|
}
|
|
9368
9749
|
// src/lib/crawler.ts
|
|
9750
|
+
init_types();
|
|
9369
9751
|
async function crawl(startUrl, opts = {}) {
|
|
9370
9752
|
const maxDepth = opts.maxDepth ?? 2;
|
|
9371
9753
|
const maxPages = opts.maxPages ?? 50;
|
|
@@ -9491,6 +9873,7 @@ export {
|
|
|
9491
9873
|
isLightpandaAvailable,
|
|
9492
9874
|
isEngineAvailable,
|
|
9493
9875
|
isBunSession,
|
|
9876
|
+
isAutoGallery,
|
|
9494
9877
|
isAgentStale,
|
|
9495
9878
|
inferUseCase,
|
|
9496
9879
|
hoverRef,
|
|
@@ -9529,6 +9912,7 @@ export {
|
|
|
9529
9912
|
getLastHeartbeat,
|
|
9530
9913
|
getIndexedDB,
|
|
9531
9914
|
getHTML,
|
|
9915
|
+
getDefaultSession,
|
|
9532
9916
|
getDatabase,
|
|
9533
9917
|
getDataDir,
|
|
9534
9918
|
getCrawlResult,
|
|
@@ -9539,6 +9923,7 @@ export {
|
|
|
9539
9923
|
getAgentByName,
|
|
9540
9924
|
getAgent,
|
|
9541
9925
|
getActiveSessions,
|
|
9926
|
+
getActiveSessionForAgent2 as getActiveSessionForAgent,
|
|
9542
9927
|
getActiveAgents,
|
|
9543
9928
|
generatePDF,
|
|
9544
9929
|
findElements,
|
|
@@ -9574,6 +9959,8 @@ export {
|
|
|
9574
9959
|
createProject,
|
|
9575
9960
|
createCrawlResult,
|
|
9576
9961
|
crawl,
|
|
9962
|
+
countActiveSessions2 as countActiveSessions,
|
|
9963
|
+
connectToExistingBrowser,
|
|
9577
9964
|
connectLightpanda,
|
|
9578
9965
|
closeSession2 as closeSession,
|
|
9579
9966
|
closePage,
|
|
@@ -9593,6 +9980,7 @@ export {
|
|
|
9593
9980
|
checkRef,
|
|
9594
9981
|
checkBox,
|
|
9595
9982
|
capturePageErrors,
|
|
9983
|
+
pool as browserPool,
|
|
9596
9984
|
attachPageListeners,
|
|
9597
9985
|
addInterceptRule,
|
|
9598
9986
|
UseCase,
|