@hasna/browser 0.0.2 → 0.0.4
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 +1576 -289
- package/dist/db/sessions.d.ts.map +1 -1
- package/dist/index.js +418 -157
- package/dist/lib/actions-ref.test.d.ts +2 -0
- package/dist/lib/actions-ref.test.d.ts.map +1 -0
- package/dist/lib/actions.d.ts +12 -0
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/annotate.d.ts +18 -0
- package/dist/lib/annotate.d.ts.map +1 -0
- package/dist/lib/annotate.test.d.ts +2 -0
- package/dist/lib/annotate.test.d.ts.map +1 -0
- package/dist/lib/dialogs.d.ts +15 -0
- package/dist/lib/dialogs.d.ts.map +1 -0
- package/dist/lib/profiles.d.ts +23 -0
- package/dist/lib/profiles.d.ts.map +1 -0
- package/dist/lib/screenshot-v4.test.d.ts +2 -0
- package/dist/lib/screenshot-v4.test.d.ts.map +1 -0
- package/dist/lib/screenshot.d.ts.map +1 -1
- package/dist/lib/session-v3.test.d.ts +2 -0
- package/dist/lib/session-v3.test.d.ts.map +1 -0
- package/dist/lib/session.d.ts +5 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/snapshot-diff.test.d.ts +2 -0
- package/dist/lib/snapshot-diff.test.d.ts.map +1 -0
- package/dist/lib/snapshot.d.ts +33 -0
- package/dist/lib/snapshot.d.ts.map +1 -0
- package/dist/lib/snapshot.test.d.ts +2 -0
- package/dist/lib/snapshot.test.d.ts.map +1 -0
- package/dist/lib/stealth.d.ts +5 -0
- package/dist/lib/stealth.d.ts.map +1 -0
- package/dist/lib/stealth.test.d.ts +2 -0
- package/dist/lib/stealth.test.d.ts.map +1 -0
- package/dist/lib/tabs.d.ts +18 -0
- package/dist/lib/tabs.d.ts.map +1 -0
- package/dist/mcp/index.js +1591 -312
- package/dist/mcp/v4.test.d.ts +2 -0
- package/dist/mcp/v4.test.d.ts.map +1 -0
- package/dist/server/index.js +340 -173
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6827,7 +6827,14 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
6827
6827
|
function createSession(data) {
|
|
6828
6828
|
const db = getDatabase();
|
|
6829
6829
|
const id = randomUUID3();
|
|
6830
|
-
|
|
6830
|
+
let name = data.name ?? null;
|
|
6831
|
+
if (name) {
|
|
6832
|
+
const existing = db.query("SELECT id FROM sessions WHERE name = ?").get(name);
|
|
6833
|
+
if (existing) {
|
|
6834
|
+
name = `${name}-${id.slice(0, 6)}`;
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
db.prepare("INSERT INTO sessions (id, engine, project_id, agent_id, start_url, name) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.engine, data.projectId ?? null, data.agentId ?? null, data.startUrl ?? null, name);
|
|
6831
6838
|
return getSession(id);
|
|
6832
6839
|
}
|
|
6833
6840
|
function getSessionByName(name) {
|
|
@@ -7451,6 +7458,261 @@ function inferUseCase(label) {
|
|
|
7451
7458
|
};
|
|
7452
7459
|
return map[label.toLowerCase()] ?? "spa_navigate" /* SPA_NAVIGATE */;
|
|
7453
7460
|
}
|
|
7461
|
+
// src/lib/network.ts
|
|
7462
|
+
function enableNetworkLogging(page, sessionId) {
|
|
7463
|
+
const requestStart = new Map;
|
|
7464
|
+
const onRequest = (req) => {
|
|
7465
|
+
requestStart.set(req.url(), Date.now());
|
|
7466
|
+
};
|
|
7467
|
+
const onResponse = (res) => {
|
|
7468
|
+
const start = requestStart.get(res.url()) ?? Date.now();
|
|
7469
|
+
const duration = Date.now() - start;
|
|
7470
|
+
const req = res.request();
|
|
7471
|
+
try {
|
|
7472
|
+
logRequest({
|
|
7473
|
+
session_id: sessionId,
|
|
7474
|
+
method: req.method(),
|
|
7475
|
+
url: res.url(),
|
|
7476
|
+
status_code: res.status(),
|
|
7477
|
+
request_headers: JSON.stringify(req.headers()),
|
|
7478
|
+
response_headers: JSON.stringify(res.headers()),
|
|
7479
|
+
body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
|
|
7480
|
+
duration_ms: duration,
|
|
7481
|
+
resource_type: req.resourceType()
|
|
7482
|
+
});
|
|
7483
|
+
} catch {}
|
|
7484
|
+
};
|
|
7485
|
+
page.on("request", onRequest);
|
|
7486
|
+
page.on("response", onResponse);
|
|
7487
|
+
return () => {
|
|
7488
|
+
page.off("request", onRequest);
|
|
7489
|
+
page.off("response", onResponse);
|
|
7490
|
+
};
|
|
7491
|
+
}
|
|
7492
|
+
async function addInterceptRule(page, rule) {
|
|
7493
|
+
await page.route(rule.pattern, async (route) => {
|
|
7494
|
+
if (rule.action === "block") {
|
|
7495
|
+
await route.abort();
|
|
7496
|
+
} else if (rule.action === "modify" && rule.response) {
|
|
7497
|
+
await route.fulfill({
|
|
7498
|
+
status: rule.response.status,
|
|
7499
|
+
body: rule.response.body,
|
|
7500
|
+
headers: rule.response.headers
|
|
7501
|
+
});
|
|
7502
|
+
} else {
|
|
7503
|
+
await route.continue();
|
|
7504
|
+
}
|
|
7505
|
+
});
|
|
7506
|
+
}
|
|
7507
|
+
async function clearInterceptRules(page) {
|
|
7508
|
+
await page.unrouteAll();
|
|
7509
|
+
}
|
|
7510
|
+
function startHAR(page) {
|
|
7511
|
+
const entries = [];
|
|
7512
|
+
const requestStart = new Map;
|
|
7513
|
+
const onRequest = (req) => {
|
|
7514
|
+
requestStart.set(req.url() + req.method(), {
|
|
7515
|
+
time: Date.now(),
|
|
7516
|
+
method: req.method(),
|
|
7517
|
+
headers: req.headers(),
|
|
7518
|
+
postData: req.postData() ?? undefined
|
|
7519
|
+
});
|
|
7520
|
+
};
|
|
7521
|
+
const onResponse = async (res) => {
|
|
7522
|
+
const key = res.url() + res.request().method();
|
|
7523
|
+
const start = requestStart.get(key);
|
|
7524
|
+
if (!start)
|
|
7525
|
+
return;
|
|
7526
|
+
const duration = Date.now() - start.time;
|
|
7527
|
+
const entry = {
|
|
7528
|
+
startedDateTime: new Date(start.time).toISOString(),
|
|
7529
|
+
time: duration,
|
|
7530
|
+
request: {
|
|
7531
|
+
method: start.method,
|
|
7532
|
+
url: res.url(),
|
|
7533
|
+
headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
|
|
7534
|
+
postData: start.postData ? { text: start.postData } : undefined
|
|
7535
|
+
},
|
|
7536
|
+
response: {
|
|
7537
|
+
status: res.status(),
|
|
7538
|
+
statusText: res.statusText(),
|
|
7539
|
+
headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
|
|
7540
|
+
content: {
|
|
7541
|
+
size: parseInt(res.headers()["content-length"] ?? "0") || 0,
|
|
7542
|
+
mimeType: res.headers()["content-type"] ?? "application/octet-stream"
|
|
7543
|
+
}
|
|
7544
|
+
},
|
|
7545
|
+
timings: { send: 0, wait: duration, receive: 0 }
|
|
7546
|
+
};
|
|
7547
|
+
entries.push(entry);
|
|
7548
|
+
requestStart.delete(key);
|
|
7549
|
+
};
|
|
7550
|
+
page.on("request", onRequest);
|
|
7551
|
+
page.on("response", onResponse);
|
|
7552
|
+
return {
|
|
7553
|
+
entries,
|
|
7554
|
+
stop: () => {
|
|
7555
|
+
page.off("request", onRequest);
|
|
7556
|
+
page.off("response", onResponse);
|
|
7557
|
+
return {
|
|
7558
|
+
log: {
|
|
7559
|
+
version: "1.2",
|
|
7560
|
+
creator: { name: "@hasna/browser", version: "0.0.1" },
|
|
7561
|
+
entries
|
|
7562
|
+
}
|
|
7563
|
+
};
|
|
7564
|
+
}
|
|
7565
|
+
};
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7568
|
+
// src/lib/console.ts
|
|
7569
|
+
function enableConsoleCapture(page, sessionId) {
|
|
7570
|
+
const onConsole = (msg) => {
|
|
7571
|
+
const levelMap = {
|
|
7572
|
+
log: "log",
|
|
7573
|
+
warn: "warn",
|
|
7574
|
+
error: "error",
|
|
7575
|
+
debug: "debug",
|
|
7576
|
+
info: "info",
|
|
7577
|
+
warning: "warn"
|
|
7578
|
+
};
|
|
7579
|
+
const level = levelMap[msg.type()] ?? "log";
|
|
7580
|
+
const location = msg.location();
|
|
7581
|
+
try {
|
|
7582
|
+
logConsoleMessage({
|
|
7583
|
+
session_id: sessionId,
|
|
7584
|
+
level,
|
|
7585
|
+
message: msg.text(),
|
|
7586
|
+
source: location.url || undefined,
|
|
7587
|
+
line_number: location.lineNumber || undefined
|
|
7588
|
+
});
|
|
7589
|
+
} catch {}
|
|
7590
|
+
};
|
|
7591
|
+
page.on("console", onConsole);
|
|
7592
|
+
return () => page.off("console", onConsole);
|
|
7593
|
+
}
|
|
7594
|
+
async function capturePageErrors(page, sessionId) {
|
|
7595
|
+
const onError = (err) => {
|
|
7596
|
+
try {
|
|
7597
|
+
logConsoleMessage({
|
|
7598
|
+
session_id: sessionId,
|
|
7599
|
+
level: "error",
|
|
7600
|
+
message: `[Page Error] ${err.message}`,
|
|
7601
|
+
source: err.stack?.split(`
|
|
7602
|
+
`)[1]?.trim()
|
|
7603
|
+
});
|
|
7604
|
+
} catch {}
|
|
7605
|
+
};
|
|
7606
|
+
page.on("pageerror", onError);
|
|
7607
|
+
return () => page.off("pageerror", onError);
|
|
7608
|
+
}
|
|
7609
|
+
|
|
7610
|
+
// src/lib/stealth.ts
|
|
7611
|
+
var REALISTIC_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
|
|
7612
|
+
var STEALTH_SCRIPT = `
|
|
7613
|
+
// \u2500\u2500 1. Remove navigator.webdriver flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7614
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
7615
|
+
get: () => false,
|
|
7616
|
+
configurable: true,
|
|
7617
|
+
});
|
|
7618
|
+
|
|
7619
|
+
// \u2500\u2500 2. Override navigator.plugins to show typical Chrome plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7620
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
7621
|
+
get: () => {
|
|
7622
|
+
const plugins = [
|
|
7623
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
|
|
7624
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
|
|
7625
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
|
|
7626
|
+
];
|
|
7627
|
+
// Mimic PluginArray interface
|
|
7628
|
+
const pluginArray = Object.create(PluginArray.prototype);
|
|
7629
|
+
plugins.forEach((p, i) => {
|
|
7630
|
+
const plugin = Object.create(Plugin.prototype);
|
|
7631
|
+
Object.defineProperties(plugin, {
|
|
7632
|
+
name: { value: p.name, enumerable: true },
|
|
7633
|
+
filename: { value: p.filename, enumerable: true },
|
|
7634
|
+
description: { value: p.description, enumerable: true },
|
|
7635
|
+
length: { value: p.length, enumerable: true },
|
|
7636
|
+
});
|
|
7637
|
+
pluginArray[i] = plugin;
|
|
7638
|
+
});
|
|
7639
|
+
Object.defineProperty(pluginArray, 'length', { value: plugins.length });
|
|
7640
|
+
pluginArray.item = (i) => pluginArray[i] || null;
|
|
7641
|
+
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
|
|
7642
|
+
pluginArray.refresh = () => {};
|
|
7643
|
+
return pluginArray;
|
|
7644
|
+
},
|
|
7645
|
+
configurable: true,
|
|
7646
|
+
});
|
|
7647
|
+
|
|
7648
|
+
// \u2500\u2500 3. Override navigator.languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7649
|
+
Object.defineProperty(navigator, 'languages', {
|
|
7650
|
+
get: () => ['en-US', 'en'],
|
|
7651
|
+
configurable: true,
|
|
7652
|
+
});
|
|
7653
|
+
|
|
7654
|
+
// \u2500\u2500 4. Override chrome.runtime to appear like real Chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7655
|
+
if (!window.chrome) {
|
|
7656
|
+
window.chrome = {};
|
|
7657
|
+
}
|
|
7658
|
+
if (!window.chrome.runtime) {
|
|
7659
|
+
window.chrome.runtime = {
|
|
7660
|
+
connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {} }; },
|
|
7661
|
+
sendMessage: function() {},
|
|
7662
|
+
onMessage: { addListener: function() {}, removeListener: function() {} },
|
|
7663
|
+
id: undefined,
|
|
7664
|
+
};
|
|
7665
|
+
}
|
|
7666
|
+
`;
|
|
7667
|
+
async function applyStealthPatches(page) {
|
|
7668
|
+
await page.context().addInitScript(STEALTH_SCRIPT);
|
|
7669
|
+
await page.context().setExtraHTTPHeaders({
|
|
7670
|
+
"User-Agent": REALISTIC_USER_AGENT
|
|
7671
|
+
});
|
|
7672
|
+
}
|
|
7673
|
+
|
|
7674
|
+
// src/lib/dialogs.ts
|
|
7675
|
+
var pendingDialogs = new Map;
|
|
7676
|
+
var AUTO_DISMISS_MS = 5000;
|
|
7677
|
+
function setupDialogHandler(page, sessionId) {
|
|
7678
|
+
const onDialog = (dialog) => {
|
|
7679
|
+
const info = {
|
|
7680
|
+
type: dialog.type(),
|
|
7681
|
+
message: dialog.message(),
|
|
7682
|
+
default_value: dialog.defaultValue(),
|
|
7683
|
+
timestamp: new Date().toISOString()
|
|
7684
|
+
};
|
|
7685
|
+
const autoTimer = setTimeout(() => {
|
|
7686
|
+
try {
|
|
7687
|
+
dialog.dismiss().catch(() => {});
|
|
7688
|
+
} catch {}
|
|
7689
|
+
const list = pendingDialogs.get(sessionId);
|
|
7690
|
+
if (list) {
|
|
7691
|
+
const idx = list.findIndex((p) => p.dialog === dialog);
|
|
7692
|
+
if (idx >= 0)
|
|
7693
|
+
list.splice(idx, 1);
|
|
7694
|
+
if (list.length === 0)
|
|
7695
|
+
pendingDialogs.delete(sessionId);
|
|
7696
|
+
}
|
|
7697
|
+
}, AUTO_DISMISS_MS);
|
|
7698
|
+
const pending = { dialog, info, autoTimer };
|
|
7699
|
+
if (!pendingDialogs.has(sessionId)) {
|
|
7700
|
+
pendingDialogs.set(sessionId, []);
|
|
7701
|
+
}
|
|
7702
|
+
pendingDialogs.get(sessionId).push(pending);
|
|
7703
|
+
};
|
|
7704
|
+
page.on("dialog", onDialog);
|
|
7705
|
+
return () => {
|
|
7706
|
+
page.off("dialog", onDialog);
|
|
7707
|
+
const list = pendingDialogs.get(sessionId);
|
|
7708
|
+
if (list) {
|
|
7709
|
+
for (const p of list)
|
|
7710
|
+
clearTimeout(p.autoTimer);
|
|
7711
|
+
pendingDialogs.delete(sessionId);
|
|
7712
|
+
}
|
|
7713
|
+
};
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7454
7716
|
// src/lib/session.ts
|
|
7455
7717
|
var handles = new Map;
|
|
7456
7718
|
async function createSession2(opts = {}) {
|
|
@@ -7473,17 +7735,44 @@ async function createSession2(opts = {}) {
|
|
|
7473
7735
|
userAgent: opts.userAgent
|
|
7474
7736
|
});
|
|
7475
7737
|
}
|
|
7738
|
+
let sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
7739
|
+
try {
|
|
7740
|
+
return new URL(opts.startUrl).hostname;
|
|
7741
|
+
} catch {
|
|
7742
|
+
return;
|
|
7743
|
+
}
|
|
7744
|
+
})() : undefined);
|
|
7476
7745
|
const session = createSession({
|
|
7477
7746
|
engine: resolvedEngine,
|
|
7478
7747
|
projectId: opts.projectId,
|
|
7479
7748
|
agentId: opts.agentId,
|
|
7480
|
-
startUrl: opts.startUrl
|
|
7749
|
+
startUrl: opts.startUrl,
|
|
7750
|
+
name: sessionName
|
|
7481
7751
|
});
|
|
7482
|
-
|
|
7752
|
+
if (opts.stealth) {
|
|
7753
|
+
try {
|
|
7754
|
+
await applyStealthPatches(page);
|
|
7755
|
+
} catch {}
|
|
7756
|
+
}
|
|
7757
|
+
const cleanups = [];
|
|
7758
|
+
if (opts.captureNetwork !== false) {
|
|
7759
|
+
try {
|
|
7760
|
+
cleanups.push(enableNetworkLogging(page, session.id));
|
|
7761
|
+
} catch {}
|
|
7762
|
+
}
|
|
7763
|
+
if (opts.captureConsole !== false) {
|
|
7764
|
+
try {
|
|
7765
|
+
cleanups.push(enableConsoleCapture(page, session.id));
|
|
7766
|
+
} catch {}
|
|
7767
|
+
}
|
|
7768
|
+
try {
|
|
7769
|
+
cleanups.push(setupDialogHandler(page, session.id));
|
|
7770
|
+
} catch {}
|
|
7771
|
+
handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
7483
7772
|
if (opts.startUrl) {
|
|
7484
7773
|
try {
|
|
7485
7774
|
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
7486
|
-
} catch
|
|
7775
|
+
} catch {}
|
|
7487
7776
|
}
|
|
7488
7777
|
return { session, page };
|
|
7489
7778
|
}
|
|
@@ -7491,6 +7780,12 @@ function getSessionPage(sessionId) {
|
|
|
7491
7780
|
const handle = handles.get(sessionId);
|
|
7492
7781
|
if (!handle)
|
|
7493
7782
|
throw new SessionNotFoundError(sessionId);
|
|
7783
|
+
try {
|
|
7784
|
+
handle.page.url();
|
|
7785
|
+
} catch {
|
|
7786
|
+
handles.delete(sessionId);
|
|
7787
|
+
throw new SessionNotFoundError(sessionId);
|
|
7788
|
+
}
|
|
7494
7789
|
return handle.page;
|
|
7495
7790
|
}
|
|
7496
7791
|
function getSessionBrowser(sessionId) {
|
|
@@ -7508,9 +7803,20 @@ function getSessionEngine(sessionId) {
|
|
|
7508
7803
|
function hasActiveHandle(sessionId) {
|
|
7509
7804
|
return handles.has(sessionId);
|
|
7510
7805
|
}
|
|
7806
|
+
function setSessionPage(sessionId, page) {
|
|
7807
|
+
const handle = handles.get(sessionId);
|
|
7808
|
+
if (!handle)
|
|
7809
|
+
throw new SessionNotFoundError(sessionId);
|
|
7810
|
+
handle.page = page;
|
|
7811
|
+
}
|
|
7511
7812
|
async function closeSession2(sessionId) {
|
|
7512
7813
|
const handle = handles.get(sessionId);
|
|
7513
7814
|
if (handle) {
|
|
7815
|
+
for (const cleanup of handle.cleanups) {
|
|
7816
|
+
try {
|
|
7817
|
+
cleanup();
|
|
7818
|
+
} catch {}
|
|
7819
|
+
}
|
|
7514
7820
|
try {
|
|
7515
7821
|
await handle.page.context().close();
|
|
7516
7822
|
} catch {}
|
|
@@ -7538,6 +7844,23 @@ function getSessionByName2(name) {
|
|
|
7538
7844
|
function renameSession2(id, name) {
|
|
7539
7845
|
return renameSession(id, name);
|
|
7540
7846
|
}
|
|
7847
|
+
function getTokenBudget(sessionId) {
|
|
7848
|
+
const handle = handles.get(sessionId);
|
|
7849
|
+
return handle ? handle.tokenBudget : null;
|
|
7850
|
+
}
|
|
7851
|
+
// src/lib/snapshot.ts
|
|
7852
|
+
var lastSnapshots = new Map;
|
|
7853
|
+
var sessionRefMaps = new Map;
|
|
7854
|
+
function getRefLocator(page, sessionId, ref) {
|
|
7855
|
+
const refMap = sessionRefMaps.get(sessionId);
|
|
7856
|
+
if (!refMap)
|
|
7857
|
+
throw new Error(`No snapshot taken for session ${sessionId}. Call browser_snapshot first.`);
|
|
7858
|
+
const entry = refMap.get(ref);
|
|
7859
|
+
if (!entry)
|
|
7860
|
+
throw new Error(`Ref ${ref} not found. Available refs: ${[...refMap.keys()].slice(0, 20).join(", ")}`);
|
|
7861
|
+
return page.getByRole(entry.role, { name: entry.name }).first();
|
|
7862
|
+
}
|
|
7863
|
+
|
|
7541
7864
|
// src/lib/actions.ts
|
|
7542
7865
|
async function click(page, selector, opts) {
|
|
7543
7866
|
try {
|
|
@@ -7780,6 +8103,73 @@ function stopWatch(watchId) {
|
|
|
7780
8103
|
activeWatches.delete(watchId);
|
|
7781
8104
|
}
|
|
7782
8105
|
}
|
|
8106
|
+
async function clickRef(page, sessionId, ref, opts) {
|
|
8107
|
+
try {
|
|
8108
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
8109
|
+
await locator.click({ timeout: opts?.timeout ?? 1e4 });
|
|
8110
|
+
} catch (err) {
|
|
8111
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
8112
|
+
throw new ElementNotFoundError(ref);
|
|
8113
|
+
if (err instanceof Error && err.message.includes("No snapshot"))
|
|
8114
|
+
throw new BrowserError(err.message, "NO_SNAPSHOT");
|
|
8115
|
+
throw new BrowserError(`clickRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CLICK_REF_FAILED");
|
|
8116
|
+
}
|
|
8117
|
+
}
|
|
8118
|
+
async function typeRef(page, sessionId, ref, text, opts) {
|
|
8119
|
+
try {
|
|
8120
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
8121
|
+
if (opts?.clear)
|
|
8122
|
+
await locator.fill("", { timeout: opts.timeout ?? 1e4 });
|
|
8123
|
+
await locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
8124
|
+
} catch (err) {
|
|
8125
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
8126
|
+
throw new ElementNotFoundError(ref);
|
|
8127
|
+
throw new BrowserError(`typeRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "TYPE_REF_FAILED");
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
async function fillRef(page, sessionId, ref, value, timeout = 1e4) {
|
|
8131
|
+
try {
|
|
8132
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
8133
|
+
await locator.fill(value, { timeout });
|
|
8134
|
+
} catch (err) {
|
|
8135
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
8136
|
+
throw new ElementNotFoundError(ref);
|
|
8137
|
+
throw new BrowserError(`fillRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "FILL_REF_FAILED");
|
|
8138
|
+
}
|
|
8139
|
+
}
|
|
8140
|
+
async function selectRef(page, sessionId, ref, value, timeout = 1e4) {
|
|
8141
|
+
try {
|
|
8142
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
8143
|
+
return await locator.selectOption(value, { timeout });
|
|
8144
|
+
} catch (err) {
|
|
8145
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
8146
|
+
throw new ElementNotFoundError(ref);
|
|
8147
|
+
throw new BrowserError(`selectRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "SELECT_REF_FAILED");
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
async function checkRef(page, sessionId, ref, checked, timeout = 1e4) {
|
|
8151
|
+
try {
|
|
8152
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
8153
|
+
if (checked)
|
|
8154
|
+
await locator.check({ timeout });
|
|
8155
|
+
else
|
|
8156
|
+
await locator.uncheck({ timeout });
|
|
8157
|
+
} catch (err) {
|
|
8158
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
8159
|
+
throw new ElementNotFoundError(ref);
|
|
8160
|
+
throw new BrowserError(`checkRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CHECK_REF_FAILED");
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
async function hoverRef(page, sessionId, ref, timeout = 1e4) {
|
|
8164
|
+
try {
|
|
8165
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
8166
|
+
await locator.hover({ timeout });
|
|
8167
|
+
} catch (err) {
|
|
8168
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
8169
|
+
throw new ElementNotFoundError(ref);
|
|
8170
|
+
throw new BrowserError(`hoverRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "HOVER_REF_FAILED");
|
|
8171
|
+
}
|
|
8172
|
+
}
|
|
7783
8173
|
// src/lib/extractor.ts
|
|
7784
8174
|
async function getText(page, selector) {
|
|
7785
8175
|
if (selector) {
|
|
@@ -7934,112 +8324,6 @@ async function getPageInfo(page) {
|
|
|
7934
8324
|
viewport
|
|
7935
8325
|
};
|
|
7936
8326
|
}
|
|
7937
|
-
// src/lib/network.ts
|
|
7938
|
-
function enableNetworkLogging(page, sessionId) {
|
|
7939
|
-
const requestStart = new Map;
|
|
7940
|
-
const onRequest = (req) => {
|
|
7941
|
-
requestStart.set(req.url(), Date.now());
|
|
7942
|
-
};
|
|
7943
|
-
const onResponse = (res) => {
|
|
7944
|
-
const start = requestStart.get(res.url()) ?? Date.now();
|
|
7945
|
-
const duration = Date.now() - start;
|
|
7946
|
-
const req = res.request();
|
|
7947
|
-
try {
|
|
7948
|
-
logRequest({
|
|
7949
|
-
session_id: sessionId,
|
|
7950
|
-
method: req.method(),
|
|
7951
|
-
url: res.url(),
|
|
7952
|
-
status_code: res.status(),
|
|
7953
|
-
request_headers: JSON.stringify(req.headers()),
|
|
7954
|
-
response_headers: JSON.stringify(res.headers()),
|
|
7955
|
-
body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
|
|
7956
|
-
duration_ms: duration,
|
|
7957
|
-
resource_type: req.resourceType()
|
|
7958
|
-
});
|
|
7959
|
-
} catch {}
|
|
7960
|
-
};
|
|
7961
|
-
page.on("request", onRequest);
|
|
7962
|
-
page.on("response", onResponse);
|
|
7963
|
-
return () => {
|
|
7964
|
-
page.off("request", onRequest);
|
|
7965
|
-
page.off("response", onResponse);
|
|
7966
|
-
};
|
|
7967
|
-
}
|
|
7968
|
-
async function addInterceptRule(page, rule) {
|
|
7969
|
-
await page.route(rule.pattern, async (route) => {
|
|
7970
|
-
if (rule.action === "block") {
|
|
7971
|
-
await route.abort();
|
|
7972
|
-
} else if (rule.action === "modify" && rule.response) {
|
|
7973
|
-
await route.fulfill({
|
|
7974
|
-
status: rule.response.status,
|
|
7975
|
-
body: rule.response.body,
|
|
7976
|
-
headers: rule.response.headers
|
|
7977
|
-
});
|
|
7978
|
-
} else {
|
|
7979
|
-
await route.continue();
|
|
7980
|
-
}
|
|
7981
|
-
});
|
|
7982
|
-
}
|
|
7983
|
-
async function clearInterceptRules(page) {
|
|
7984
|
-
await page.unrouteAll();
|
|
7985
|
-
}
|
|
7986
|
-
function startHAR(page) {
|
|
7987
|
-
const entries = [];
|
|
7988
|
-
const requestStart = new Map;
|
|
7989
|
-
const onRequest = (req) => {
|
|
7990
|
-
requestStart.set(req.url() + req.method(), {
|
|
7991
|
-
time: Date.now(),
|
|
7992
|
-
method: req.method(),
|
|
7993
|
-
headers: req.headers(),
|
|
7994
|
-
postData: req.postData() ?? undefined
|
|
7995
|
-
});
|
|
7996
|
-
};
|
|
7997
|
-
const onResponse = async (res) => {
|
|
7998
|
-
const key = res.url() + res.request().method();
|
|
7999
|
-
const start = requestStart.get(key);
|
|
8000
|
-
if (!start)
|
|
8001
|
-
return;
|
|
8002
|
-
const duration = Date.now() - start.time;
|
|
8003
|
-
const entry = {
|
|
8004
|
-
startedDateTime: new Date(start.time).toISOString(),
|
|
8005
|
-
time: duration,
|
|
8006
|
-
request: {
|
|
8007
|
-
method: start.method,
|
|
8008
|
-
url: res.url(),
|
|
8009
|
-
headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
|
|
8010
|
-
postData: start.postData ? { text: start.postData } : undefined
|
|
8011
|
-
},
|
|
8012
|
-
response: {
|
|
8013
|
-
status: res.status(),
|
|
8014
|
-
statusText: res.statusText(),
|
|
8015
|
-
headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
|
|
8016
|
-
content: {
|
|
8017
|
-
size: parseInt(res.headers()["content-length"] ?? "0") || 0,
|
|
8018
|
-
mimeType: res.headers()["content-type"] ?? "application/octet-stream"
|
|
8019
|
-
}
|
|
8020
|
-
},
|
|
8021
|
-
timings: { send: 0, wait: duration, receive: 0 }
|
|
8022
|
-
};
|
|
8023
|
-
entries.push(entry);
|
|
8024
|
-
requestStart.delete(key);
|
|
8025
|
-
};
|
|
8026
|
-
page.on("request", onRequest);
|
|
8027
|
-
page.on("response", onResponse);
|
|
8028
|
-
return {
|
|
8029
|
-
entries,
|
|
8030
|
-
stop: () => {
|
|
8031
|
-
page.off("request", onRequest);
|
|
8032
|
-
page.off("response", onResponse);
|
|
8033
|
-
return {
|
|
8034
|
-
log: {
|
|
8035
|
-
version: "1.2",
|
|
8036
|
-
creator: { name: "@hasna/browser", version: "0.0.1" },
|
|
8037
|
-
entries
|
|
8038
|
-
}
|
|
8039
|
-
};
|
|
8040
|
-
}
|
|
8041
|
-
};
|
|
8042
|
-
}
|
|
8043
8327
|
// src/lib/performance.ts
|
|
8044
8328
|
async function getPerformanceMetrics(page) {
|
|
8045
8329
|
const navTiming = await page.evaluate(() => {
|
|
@@ -8127,47 +8411,6 @@ async function startCoverage(page) {
|
|
|
8127
8411
|
}
|
|
8128
8412
|
};
|
|
8129
8413
|
}
|
|
8130
|
-
// src/lib/console.ts
|
|
8131
|
-
function enableConsoleCapture(page, sessionId) {
|
|
8132
|
-
const onConsole = (msg) => {
|
|
8133
|
-
const levelMap = {
|
|
8134
|
-
log: "log",
|
|
8135
|
-
warn: "warn",
|
|
8136
|
-
error: "error",
|
|
8137
|
-
debug: "debug",
|
|
8138
|
-
info: "info",
|
|
8139
|
-
warning: "warn"
|
|
8140
|
-
};
|
|
8141
|
-
const level = levelMap[msg.type()] ?? "log";
|
|
8142
|
-
const location = msg.location();
|
|
8143
|
-
try {
|
|
8144
|
-
logConsoleMessage({
|
|
8145
|
-
session_id: sessionId,
|
|
8146
|
-
level,
|
|
8147
|
-
message: msg.text(),
|
|
8148
|
-
source: location.url || undefined,
|
|
8149
|
-
line_number: location.lineNumber || undefined
|
|
8150
|
-
});
|
|
8151
|
-
} catch {}
|
|
8152
|
-
};
|
|
8153
|
-
page.on("console", onConsole);
|
|
8154
|
-
return () => page.off("console", onConsole);
|
|
8155
|
-
}
|
|
8156
|
-
async function capturePageErrors(page, sessionId) {
|
|
8157
|
-
const onError = (err) => {
|
|
8158
|
-
try {
|
|
8159
|
-
logConsoleMessage({
|
|
8160
|
-
session_id: sessionId,
|
|
8161
|
-
level: "error",
|
|
8162
|
-
message: `[Page Error] ${err.message}`,
|
|
8163
|
-
source: err.stack?.split(`
|
|
8164
|
-
`)[1]?.trim()
|
|
8165
|
-
});
|
|
8166
|
-
} catch {}
|
|
8167
|
-
};
|
|
8168
|
-
page.on("pageerror", onError);
|
|
8169
|
-
return () => page.off("pageerror", onError);
|
|
8170
|
-
}
|
|
8171
8414
|
// src/lib/screenshot.ts
|
|
8172
8415
|
var import_sharp = __toESM(require_lib(), 1);
|
|
8173
8416
|
import { join as join2 } from "path";
|
|
@@ -8267,11 +8510,20 @@ async function takeScreenshot(page, opts) {
|
|
|
8267
8510
|
}
|
|
8268
8511
|
const originalSizeBytes = rawBuffer.length;
|
|
8269
8512
|
let finalBuffer;
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8513
|
+
let compressed = true;
|
|
8514
|
+
let fallback = false;
|
|
8515
|
+
try {
|
|
8516
|
+
if (compress && format !== "png") {
|
|
8517
|
+
finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
|
|
8518
|
+
} else if (compress && format === "png") {
|
|
8519
|
+
finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
|
|
8520
|
+
} else {
|
|
8521
|
+
finalBuffer = rawBuffer;
|
|
8522
|
+
compressed = false;
|
|
8523
|
+
}
|
|
8524
|
+
} catch (sharpErr) {
|
|
8525
|
+
fallback = true;
|
|
8526
|
+
compressed = false;
|
|
8275
8527
|
finalBuffer = rawBuffer;
|
|
8276
8528
|
}
|
|
8277
8529
|
const compressedSizeBytes = finalBuffer.length;
|
|
@@ -8299,7 +8551,8 @@ async function takeScreenshot(page, opts) {
|
|
|
8299
8551
|
compressed_size_bytes: compressedSizeBytes,
|
|
8300
8552
|
compression_ratio: compressionRatio,
|
|
8301
8553
|
thumbnail_path: thumbnailPath,
|
|
8302
|
-
thumbnail_base64: thumbnailBase64
|
|
8554
|
+
thumbnail_base64: thumbnailBase64,
|
|
8555
|
+
...fallback ? { fallback: true, compressed: false } : {}
|
|
8303
8556
|
};
|
|
8304
8557
|
if (opts?.track !== false) {
|
|
8305
8558
|
try {
|
|
@@ -8673,6 +8926,7 @@ export {
|
|
|
8673
8926
|
updateRecording,
|
|
8674
8927
|
updateProject,
|
|
8675
8928
|
updateAgent,
|
|
8929
|
+
typeRef,
|
|
8676
8930
|
type,
|
|
8677
8931
|
takeScreenshot,
|
|
8678
8932
|
stopWatch,
|
|
@@ -8682,8 +8936,10 @@ export {
|
|
|
8682
8936
|
startHAR,
|
|
8683
8937
|
startCoverage,
|
|
8684
8938
|
setSessionStorage,
|
|
8939
|
+
setSessionPage,
|
|
8685
8940
|
setLocalStorage,
|
|
8686
8941
|
setCookie,
|
|
8942
|
+
selectRef,
|
|
8687
8943
|
selectOption,
|
|
8688
8944
|
selectEngine,
|
|
8689
8945
|
scrollTo,
|
|
@@ -8712,6 +8968,7 @@ export {
|
|
|
8712
8968
|
isEngineAvailable,
|
|
8713
8969
|
isAgentStale,
|
|
8714
8970
|
inferUseCase,
|
|
8971
|
+
hoverRef,
|
|
8715
8972
|
hover,
|
|
8716
8973
|
heartbeat2 as heartbeat,
|
|
8717
8974
|
hasActiveHandle,
|
|
@@ -8719,6 +8976,7 @@ export {
|
|
|
8719
8976
|
goBack,
|
|
8720
8977
|
getWatchChanges,
|
|
8721
8978
|
getUrl,
|
|
8979
|
+
getTokenBudget,
|
|
8722
8980
|
getTitle,
|
|
8723
8981
|
getTimingEntries,
|
|
8724
8982
|
getText,
|
|
@@ -8758,6 +9016,7 @@ export {
|
|
|
8758
9016
|
getActiveAgents,
|
|
8759
9017
|
generatePDF,
|
|
8760
9018
|
findElements,
|
|
9019
|
+
fillRef,
|
|
8761
9020
|
fillForm,
|
|
8762
9021
|
fill,
|
|
8763
9022
|
extractTable,
|
|
@@ -8795,6 +9054,7 @@ export {
|
|
|
8795
9054
|
closeBrowser,
|
|
8796
9055
|
closeAllSessions,
|
|
8797
9056
|
clickText,
|
|
9057
|
+
clickRef,
|
|
8798
9058
|
click,
|
|
8799
9059
|
clearSessionStorage,
|
|
8800
9060
|
clearNetworkLog,
|
|
@@ -8804,6 +9064,7 @@ export {
|
|
|
8804
9064
|
clearConsoleLog,
|
|
8805
9065
|
cleanStaleAgents,
|
|
8806
9066
|
cleanOldHeartbeats,
|
|
9067
|
+
checkRef,
|
|
8807
9068
|
checkBox,
|
|
8808
9069
|
capturePageErrors,
|
|
8809
9070
|
attachPageListeners,
|