browserclaw 0.7.1 → 0.8.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/README.md +3 -1
- package/dist/index.cjs +488 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +210 -3
- package/dist/index.d.ts +210 -3
- package/dist/index.js +490 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1198,8 +1198,13 @@ function isWebSocketUrl(url) {
|
|
|
1198
1198
|
function isLoopbackHost(hostname) {
|
|
1199
1199
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1200
1200
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1201
|
+
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1202
|
+
function hasProxyEnvConfigured(env = process.env) {
|
|
1203
|
+
for (const key of PROXY_ENV_KEYS) {
|
|
1204
|
+
const value = env[key];
|
|
1205
|
+
if (typeof value === "string" && value.trim().length > 0) return true;
|
|
1206
|
+
}
|
|
1207
|
+
return false;
|
|
1203
1208
|
}
|
|
1204
1209
|
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
1205
1210
|
const ws = new URL(wsUrl);
|
|
@@ -1292,6 +1297,16 @@ async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
1292
1297
|
clearTimeout(t);
|
|
1293
1298
|
}
|
|
1294
1299
|
}
|
|
1300
|
+
var COMMON_CDP_PORTS = [9222, 9223, 9224, 9225, 9226, 9229];
|
|
1301
|
+
async function discoverChromeCdpUrl(timeoutMs = 500) {
|
|
1302
|
+
const results = await Promise.all(
|
|
1303
|
+
COMMON_CDP_PORTS.map(async (port) => {
|
|
1304
|
+
const url = `http://127.0.0.1:${String(port)}`;
|
|
1305
|
+
return await isChromeReachable(url, timeoutMs) ? url : null;
|
|
1306
|
+
})
|
|
1307
|
+
);
|
|
1308
|
+
return results.find((url) => url !== null) ?? null;
|
|
1309
|
+
}
|
|
1295
1310
|
async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
1296
1311
|
if (isWebSocketUrl(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs);
|
|
1297
1312
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
@@ -1912,6 +1927,43 @@ function ensurePageState(page) {
|
|
|
1912
1927
|
rec.ok = false;
|
|
1913
1928
|
}
|
|
1914
1929
|
});
|
|
1930
|
+
page.on("dialog", (dialog) => {
|
|
1931
|
+
if (state.armIdDialog > 0) return;
|
|
1932
|
+
if (state.dialogHandler) {
|
|
1933
|
+
let handled = false;
|
|
1934
|
+
const event = {
|
|
1935
|
+
type: dialog.type(),
|
|
1936
|
+
message: dialog.message(),
|
|
1937
|
+
defaultValue: dialog.defaultValue(),
|
|
1938
|
+
accept: (promptText) => {
|
|
1939
|
+
handled = true;
|
|
1940
|
+
return dialog.accept(promptText);
|
|
1941
|
+
},
|
|
1942
|
+
dismiss: () => {
|
|
1943
|
+
handled = true;
|
|
1944
|
+
return dialog.dismiss();
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
Promise.resolve(state.dialogHandler(event)).then(() => {
|
|
1948
|
+
if (!handled) {
|
|
1949
|
+
dialog.dismiss().catch((err) => {
|
|
1950
|
+
console.warn(`[browserclaw] Failed to auto-dismiss dialog: ${err instanceof Error ? err.message : String(err)}`);
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
}).catch((err) => {
|
|
1954
|
+
console.warn(`[browserclaw] onDialog handler error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1955
|
+
if (!handled) {
|
|
1956
|
+
dialog.dismiss().catch((dismissErr) => {
|
|
1957
|
+
console.warn(`[browserclaw] Failed to dismiss dialog after handler error: ${dismissErr instanceof Error ? dismissErr.message : String(dismissErr)}`);
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
dialog.dismiss().catch((err) => {
|
|
1964
|
+
console.warn(`[browserclaw] Failed to dismiss dialog: ${err instanceof Error ? err.message : String(err)}`);
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1915
1967
|
page.on("close", () => {
|
|
1916
1968
|
pageStates.delete(page);
|
|
1917
1969
|
observedPages.delete(page);
|
|
@@ -1919,6 +1971,11 @@ function ensurePageState(page) {
|
|
|
1919
1971
|
}
|
|
1920
1972
|
return state;
|
|
1921
1973
|
}
|
|
1974
|
+
async function setDialogHandler(opts) {
|
|
1975
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1976
|
+
const state = ensurePageState(page);
|
|
1977
|
+
state.dialogHandler = opts.handler;
|
|
1978
|
+
}
|
|
1922
1979
|
function applyStealthToPage(page) {
|
|
1923
1980
|
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1924
1981
|
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
@@ -2351,48 +2408,52 @@ async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
|
2351
2408
|
}
|
|
2352
2409
|
var BROWSER_EVALUATOR = new Function(
|
|
2353
2410
|
"args",
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
var candidate
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2411
|
+
[
|
|
2412
|
+
'"use strict";',
|
|
2413
|
+
"var fnBody = args.fnBody, timeoutMs = args.timeoutMs;",
|
|
2414
|
+
"try {",
|
|
2415
|
+
" var candidate;",
|
|
2416
|
+
' try { candidate = eval("(" + fnBody + ")"); }',
|
|
2417
|
+
" catch (_) { candidate = (0, eval)(fnBody); }",
|
|
2418
|
+
' var result = typeof candidate === "function" ? candidate() : candidate;',
|
|
2419
|
+
' if (result && typeof result.then === "function") {',
|
|
2420
|
+
" return Promise.race([",
|
|
2421
|
+
" result,",
|
|
2422
|
+
" new Promise(function(_, reject) {",
|
|
2423
|
+
' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
|
|
2424
|
+
" })",
|
|
2425
|
+
" ]);",
|
|
2426
|
+
" }",
|
|
2427
|
+
" return result;",
|
|
2428
|
+
"} catch (err) {",
|
|
2429
|
+
' throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));',
|
|
2430
|
+
"}"
|
|
2431
|
+
].join("\n")
|
|
2373
2432
|
);
|
|
2374
2433
|
var ELEMENT_EVALUATOR = new Function(
|
|
2375
2434
|
"el",
|
|
2376
2435
|
"args",
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
var candidate
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2436
|
+
[
|
|
2437
|
+
'"use strict";',
|
|
2438
|
+
"var fnBody = args.fnBody, timeoutMs = args.timeoutMs;",
|
|
2439
|
+
"try {",
|
|
2440
|
+
" var candidate;",
|
|
2441
|
+
' try { candidate = eval("(" + fnBody + ")"); }',
|
|
2442
|
+
" catch (_) { candidate = (0, eval)(fnBody); }",
|
|
2443
|
+
' var result = typeof candidate === "function" ? candidate(el) : candidate;',
|
|
2444
|
+
' if (result && typeof result.then === "function") {',
|
|
2445
|
+
" return Promise.race([",
|
|
2446
|
+
" result,",
|
|
2447
|
+
" new Promise(function(_, reject) {",
|
|
2448
|
+
' setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);',
|
|
2449
|
+
" })",
|
|
2450
|
+
" ]);",
|
|
2451
|
+
" }",
|
|
2452
|
+
" return result;",
|
|
2453
|
+
"} catch (err) {",
|
|
2454
|
+
' throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));',
|
|
2455
|
+
"}"
|
|
2456
|
+
].join("\n")
|
|
2396
2457
|
);
|
|
2397
2458
|
async function evaluateViaPlaywright(opts) {
|
|
2398
2459
|
const fnText = opts.fn.trim();
|
|
@@ -2460,6 +2521,18 @@ async function evaluateViaPlaywright(opts) {
|
|
|
2460
2521
|
|
|
2461
2522
|
// src/security.ts
|
|
2462
2523
|
var ipaddr = __toESM(require_ipaddr());
|
|
2524
|
+
function resolveDefaultBrowserTmpDir() {
|
|
2525
|
+
try {
|
|
2526
|
+
if (process.platform === "linux" || process.platform === "darwin") {
|
|
2527
|
+
return "/tmp/browserclaw";
|
|
2528
|
+
}
|
|
2529
|
+
} catch {
|
|
2530
|
+
}
|
|
2531
|
+
return path.join(os.tmpdir(), "browserclaw");
|
|
2532
|
+
}
|
|
2533
|
+
var DEFAULT_BROWSER_TMP_DIR = resolveDefaultBrowserTmpDir();
|
|
2534
|
+
path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
|
|
2535
|
+
var DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
|
|
2463
2536
|
var InvalidBrowserNavigationUrlError = class extends Error {
|
|
2464
2537
|
constructor(message) {
|
|
2465
2538
|
super(message);
|
|
@@ -2471,7 +2544,7 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
2471
2544
|
}
|
|
2472
2545
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2473
2546
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
2474
|
-
var
|
|
2547
|
+
var PROXY_ENV_KEYS2 = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
2475
2548
|
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
|
|
2476
2549
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
2477
2550
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
@@ -2480,7 +2553,7 @@ function isPrivateNetworkAllowedByPolicy(policy) {
|
|
|
2480
2553
|
return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
|
|
2481
2554
|
}
|
|
2482
2555
|
function hasProxyEnvConfigured2(env = process.env) {
|
|
2483
|
-
for (const key of
|
|
2556
|
+
for (const key of PROXY_ENV_KEYS2) {
|
|
2484
2557
|
const value = env[key];
|
|
2485
2558
|
if (typeof value === "string" && value.trim().length > 0) return true;
|
|
2486
2559
|
}
|
|
@@ -2685,6 +2758,8 @@ function dedupeAndPreferIpv4(results) {
|
|
|
2685
2758
|
}
|
|
2686
2759
|
function createPinnedLookup(params) {
|
|
2687
2760
|
const normalizedHost = normalizeHostname(params.hostname);
|
|
2761
|
+
if (params.addresses.length === 0)
|
|
2762
|
+
throw new Error(`Pinned lookup requires at least one address for ${params.hostname}`);
|
|
2688
2763
|
const fallback = params.fallback ?? dns.lookup;
|
|
2689
2764
|
const records = params.addresses.map((address) => ({
|
|
2690
2765
|
address,
|
|
@@ -2859,6 +2934,48 @@ async function resolveStrictExistingUploadPaths(params) {
|
|
|
2859
2934
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2860
2935
|
}
|
|
2861
2936
|
}
|
|
2937
|
+
function resolvePathWithinRoot(params) {
|
|
2938
|
+
const root = path.resolve(params.rootDir);
|
|
2939
|
+
const raw = params.requestedPath.trim();
|
|
2940
|
+
const effectivePath = raw === "" && params.defaultFileName != null && params.defaultFileName !== "" ? params.defaultFileName : raw;
|
|
2941
|
+
if (effectivePath === "") return { ok: false, error: `Empty path is not allowed (${params.scopeLabel}).` };
|
|
2942
|
+
const resolved = path.resolve(root, effectivePath);
|
|
2943
|
+
const rel = path.relative(root, resolved);
|
|
2944
|
+
if (!rel || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
2945
|
+
return { ok: false, error: `Path escapes ${params.scopeLabel}: "${params.requestedPath}".` };
|
|
2946
|
+
}
|
|
2947
|
+
return { ok: true, path: resolved };
|
|
2948
|
+
}
|
|
2949
|
+
async function resolveStrictExistingPathsWithinRoot(params) {
|
|
2950
|
+
const root = path.resolve(params.rootDir);
|
|
2951
|
+
const resolved = [];
|
|
2952
|
+
for (const raw of params.requestedPaths) {
|
|
2953
|
+
const lexical = resolvePathWithinRoot({ rootDir: root, requestedPath: raw, scopeLabel: params.scopeLabel });
|
|
2954
|
+
if (!lexical.ok) return lexical;
|
|
2955
|
+
let real;
|
|
2956
|
+
try {
|
|
2957
|
+
real = await promises$1.realpath(lexical.path);
|
|
2958
|
+
} catch (e) {
|
|
2959
|
+
if (e.code === "ENOENT") {
|
|
2960
|
+
return { ok: false, error: `Path does not exist (${params.scopeLabel}): "${raw}".` };
|
|
2961
|
+
}
|
|
2962
|
+
return { ok: false, error: `Cannot resolve "${raw}" (${params.scopeLabel}): ${e.message}` };
|
|
2963
|
+
}
|
|
2964
|
+
const rel = path.relative(root, real);
|
|
2965
|
+
if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
2966
|
+
return { ok: false, error: `Path escapes ${params.scopeLabel} via symlink: "${raw}".` };
|
|
2967
|
+
}
|
|
2968
|
+
const stat = await promises$1.lstat(real);
|
|
2969
|
+
if (stat.isSymbolicLink()) {
|
|
2970
|
+
return { ok: false, error: `Path is a symbolic link (${params.scopeLabel}): "${raw}".` };
|
|
2971
|
+
}
|
|
2972
|
+
if (!stat.isFile()) {
|
|
2973
|
+
return { ok: false, error: `Path is not a regular file (${params.scopeLabel}): "${raw}".` };
|
|
2974
|
+
}
|
|
2975
|
+
resolved.push(real);
|
|
2976
|
+
}
|
|
2977
|
+
return { ok: true, paths: resolved };
|
|
2978
|
+
}
|
|
2862
2979
|
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
2863
2980
|
const trimmed = fileName.trim();
|
|
2864
2981
|
if (trimmed === "") return fallbackName;
|
|
@@ -2939,6 +3056,35 @@ function resolveLocator(page, resolved) {
|
|
|
2939
3056
|
const sel = resolved.selector ?? "";
|
|
2940
3057
|
return page.locator(sel);
|
|
2941
3058
|
}
|
|
3059
|
+
async function mouseClickViaPlaywright(opts) {
|
|
3060
|
+
const page = await getRestoredPageForTarget(opts);
|
|
3061
|
+
await page.mouse.click(opts.x, opts.y, {
|
|
3062
|
+
button: opts.button,
|
|
3063
|
+
clickCount: opts.clickCount,
|
|
3064
|
+
delay: opts.delayMs
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
async function clickByTextViaPlaywright(opts) {
|
|
3068
|
+
const page = await getRestoredPageForTarget(opts);
|
|
3069
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3070
|
+
try {
|
|
3071
|
+
await page.getByText(opts.text, { exact: opts.exact }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3072
|
+
} catch (err) {
|
|
3073
|
+
throw toAIFriendlyError(err, `text="${opts.text}"`);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
async function clickByRoleViaPlaywright(opts) {
|
|
3077
|
+
const page = await getRestoredPageForTarget(opts);
|
|
3078
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3079
|
+
try {
|
|
3080
|
+
await page.getByRole(opts.role, { name: opts.name }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3081
|
+
} catch (err) {
|
|
3082
|
+
throw toAIFriendlyError(
|
|
3083
|
+
err,
|
|
3084
|
+
`role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
2942
3088
|
async function clickViaPlaywright(opts) {
|
|
2943
3089
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2944
3090
|
const page = await getRestoredPageForTarget(opts);
|
|
@@ -3102,9 +3248,10 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
3102
3248
|
if (inputRef && element) throw new Error("ref and element are mutually exclusive");
|
|
3103
3249
|
if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
|
|
3104
3250
|
const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
|
|
3105
|
-
const uploadPathsResult = await
|
|
3251
|
+
const uploadPathsResult = await resolveStrictExistingPathsWithinRoot({
|
|
3252
|
+
rootDir: DEFAULT_UPLOAD_DIR,
|
|
3106
3253
|
requestedPaths: opts.paths,
|
|
3107
|
-
scopeLabel:
|
|
3254
|
+
scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`
|
|
3108
3255
|
});
|
|
3109
3256
|
if (!uploadPathsResult.ok) throw new Error(uploadPathsResult.error);
|
|
3110
3257
|
const resolvedPaths = uploadPathsResult.paths;
|
|
@@ -3132,9 +3279,14 @@ async function armDialogViaPlaywright(opts) {
|
|
|
3132
3279
|
const armId = state.armIdDialog;
|
|
3133
3280
|
page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
|
|
3134
3281
|
if (state.armIdDialog !== armId) return;
|
|
3135
|
-
|
|
3136
|
-
|
|
3282
|
+
try {
|
|
3283
|
+
if (opts.accept) await dialog.accept(opts.promptText);
|
|
3284
|
+
else await dialog.dismiss();
|
|
3285
|
+
} finally {
|
|
3286
|
+
if (state.armIdDialog === armId) state.armIdDialog = 0;
|
|
3287
|
+
}
|
|
3137
3288
|
}).catch(() => {
|
|
3289
|
+
if (state.armIdDialog === armId) state.armIdDialog = 0;
|
|
3138
3290
|
});
|
|
3139
3291
|
}
|
|
3140
3292
|
async function armFileUploadViaPlaywright(opts) {
|
|
@@ -3152,9 +3304,10 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3152
3304
|
}
|
|
3153
3305
|
return;
|
|
3154
3306
|
}
|
|
3155
|
-
const uploadPathsResult = await
|
|
3307
|
+
const uploadPathsResult = await resolveStrictExistingPathsWithinRoot({
|
|
3308
|
+
rootDir: DEFAULT_UPLOAD_DIR,
|
|
3156
3309
|
requestedPaths: opts.paths,
|
|
3157
|
-
scopeLabel:
|
|
3310
|
+
scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`
|
|
3158
3311
|
});
|
|
3159
3312
|
if (!uploadPathsResult.ok) {
|
|
3160
3313
|
try {
|
|
@@ -3188,6 +3341,17 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
3188
3341
|
}
|
|
3189
3342
|
|
|
3190
3343
|
// src/actions/navigation.ts
|
|
3344
|
+
var recordingContexts = /* @__PURE__ */ new Map();
|
|
3345
|
+
function clearRecordingContext(cdpUrl) {
|
|
3346
|
+
recordingContexts.delete(cdpUrl);
|
|
3347
|
+
}
|
|
3348
|
+
async function createRecordingContext(browser, cdpUrl, recordVideo) {
|
|
3349
|
+
const context = await browser.newContext({ recordVideo });
|
|
3350
|
+
observeContext(context);
|
|
3351
|
+
recordingContexts.set(cdpUrl, context);
|
|
3352
|
+
context.on("close", () => recordingContexts.delete(cdpUrl));
|
|
3353
|
+
return context;
|
|
3354
|
+
}
|
|
3191
3355
|
function isRetryableNavigateError(err) {
|
|
3192
3356
|
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
3193
3357
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
@@ -3240,7 +3404,7 @@ async function listPagesViaPlaywright(opts) {
|
|
|
3240
3404
|
}
|
|
3241
3405
|
async function createPageViaPlaywright(opts) {
|
|
3242
3406
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3243
|
-
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
3407
|
+
const context = opts.recordVideo ? recordingContexts.get(opts.cdpUrl) ?? await createRecordingContext(browser, opts.cdpUrl, opts.recordVideo) : browser.contexts()[0] ?? await browser.newContext();
|
|
3244
3408
|
ensureContextState(context);
|
|
3245
3409
|
const page = await context.newPage();
|
|
3246
3410
|
ensurePageState(page);
|
|
@@ -3317,10 +3481,10 @@ async function waitForViaPlaywright(opts) {
|
|
|
3317
3481
|
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3318
3482
|
}
|
|
3319
3483
|
if (opts.text !== void 0 && opts.text !== "") {
|
|
3320
|
-
await page.
|
|
3484
|
+
await page.waitForFunction((text) => (document.body?.innerText ?? "").includes(text), opts.text, { timeout });
|
|
3321
3485
|
}
|
|
3322
3486
|
if (opts.textGone !== void 0 && opts.textGone !== "") {
|
|
3323
|
-
await page.
|
|
3487
|
+
await page.waitForFunction((text) => !(document.body?.innerText ?? "").includes(text), opts.textGone, { timeout });
|
|
3324
3488
|
}
|
|
3325
3489
|
if (opts.selector !== void 0 && opts.selector !== "") {
|
|
3326
3490
|
const selector = opts.selector.trim();
|
|
@@ -3592,7 +3756,7 @@ async function waitForDownloadViaPlaywright(opts) {
|
|
|
3592
3756
|
try {
|
|
3593
3757
|
const download = await waiter.promise;
|
|
3594
3758
|
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
3595
|
-
const savePath = opts.path ?? download.suggestedFilename();
|
|
3759
|
+
const savePath = opts.path ?? sanitizeUntrustedFileName(download.suggestedFilename() || "download.bin", "download.bin");
|
|
3596
3760
|
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
3597
3761
|
return await saveDownloadPayload(download, savePath);
|
|
3598
3762
|
} catch (err) {
|
|
@@ -4259,6 +4423,16 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4259
4423
|
return match ? match[1] : null;
|
|
4260
4424
|
}
|
|
4261
4425
|
if (options.interactive === true) {
|
|
4426
|
+
let interactiveMaxRef = 0;
|
|
4427
|
+
for (const line of lines) {
|
|
4428
|
+
const refMatch = /\[ref=e(\d+)\]/.exec(line);
|
|
4429
|
+
if (refMatch) interactiveMaxRef = Math.max(interactiveMaxRef, Number.parseInt(refMatch[1], 10));
|
|
4430
|
+
}
|
|
4431
|
+
let interactiveCounter = interactiveMaxRef;
|
|
4432
|
+
const nextInteractiveRef = () => {
|
|
4433
|
+
interactiveCounter++;
|
|
4434
|
+
return `e${String(interactiveCounter)}`;
|
|
4435
|
+
};
|
|
4262
4436
|
const out2 = [];
|
|
4263
4437
|
for (const line of lines) {
|
|
4264
4438
|
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
@@ -4266,13 +4440,32 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4266
4440
|
const { roleRaw, role, name, suffix } = parsed;
|
|
4267
4441
|
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
4268
4442
|
const ref = parseAiSnapshotRef(suffix);
|
|
4269
|
-
if (ref === null) continue;
|
|
4270
4443
|
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
4271
|
-
|
|
4272
|
-
|
|
4444
|
+
if (ref !== null) {
|
|
4445
|
+
refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
4446
|
+
out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
|
|
4447
|
+
} else {
|
|
4448
|
+
const generatedRef = nextInteractiveRef();
|
|
4449
|
+
refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
4450
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
4451
|
+
if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
|
|
4452
|
+
enhanced += ` [ref=${generatedRef}]`;
|
|
4453
|
+
if (suffix.trim() !== "") enhanced += suffix;
|
|
4454
|
+
out2.push(enhanced);
|
|
4455
|
+
}
|
|
4273
4456
|
}
|
|
4274
4457
|
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
4275
4458
|
}
|
|
4459
|
+
let maxRef = 0;
|
|
4460
|
+
for (const line of lines) {
|
|
4461
|
+
const refMatch = /\[ref=e(\d+)\]/.exec(line);
|
|
4462
|
+
if (refMatch) maxRef = Math.max(maxRef, Number.parseInt(refMatch[1], 10));
|
|
4463
|
+
}
|
|
4464
|
+
let generatedCounter = maxRef;
|
|
4465
|
+
const nextGeneratedRef = () => {
|
|
4466
|
+
generatedCounter++;
|
|
4467
|
+
return `e${String(generatedCounter)}`;
|
|
4468
|
+
};
|
|
4276
4469
|
const out = [];
|
|
4277
4470
|
for (const line of lines) {
|
|
4278
4471
|
const depth = getIndentLevel(line);
|
|
@@ -4282,7 +4475,7 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4282
4475
|
out.push(line);
|
|
4283
4476
|
continue;
|
|
4284
4477
|
}
|
|
4285
|
-
const [, , roleRaw, name, suffix] = match;
|
|
4478
|
+
const [, prefix, roleRaw, name, suffix] = match;
|
|
4286
4479
|
if (roleRaw.startsWith("/")) {
|
|
4287
4480
|
out.push(line);
|
|
4288
4481
|
continue;
|
|
@@ -4291,8 +4484,20 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4291
4484
|
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4292
4485
|
if (options.compact === true && isStructural && name === "") continue;
|
|
4293
4486
|
const ref = parseAiSnapshotRef(suffix);
|
|
4294
|
-
if (ref !== null)
|
|
4295
|
-
|
|
4487
|
+
if (ref !== null) {
|
|
4488
|
+
refs[ref] = { role, ...name !== "" ? { name } : {} };
|
|
4489
|
+
out.push(line);
|
|
4490
|
+
} else if (INTERACTIVE_ROLES.has(role)) {
|
|
4491
|
+
const generatedRef = nextGeneratedRef();
|
|
4492
|
+
refs[generatedRef] = { role, ...name !== "" ? { name } : {} };
|
|
4493
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
4494
|
+
if (name !== "") enhanced += ` "${name}"`;
|
|
4495
|
+
enhanced += ` [ref=${generatedRef}]`;
|
|
4496
|
+
if (suffix.trim() !== "") enhanced += suffix;
|
|
4497
|
+
out.push(enhanced);
|
|
4498
|
+
} else {
|
|
4499
|
+
out.push(line);
|
|
4500
|
+
}
|
|
4296
4501
|
}
|
|
4297
4502
|
const tree = out.join("\n") || "(empty)";
|
|
4298
4503
|
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
@@ -4653,6 +4858,111 @@ var CrawlPage = class {
|
|
|
4653
4858
|
timeoutMs: opts?.timeoutMs
|
|
4654
4859
|
});
|
|
4655
4860
|
}
|
|
4861
|
+
/**
|
|
4862
|
+
* Click an element by CSS selector (no snapshot/ref needed).
|
|
4863
|
+
*
|
|
4864
|
+
* Finds and clicks atomically — no stale ref problem.
|
|
4865
|
+
*
|
|
4866
|
+
* @param selector - CSS selector (e.g. `'#submit-btn'`, `'.modal button'`)
|
|
4867
|
+
* @param opts - Click options (double-click, button, modifiers)
|
|
4868
|
+
*
|
|
4869
|
+
* @example
|
|
4870
|
+
* ```ts
|
|
4871
|
+
* await page.clickBySelector('#submit-btn');
|
|
4872
|
+
* await page.clickBySelector('.modal .close', { button: 'right' });
|
|
4873
|
+
* ```
|
|
4874
|
+
*/
|
|
4875
|
+
async clickBySelector(selector, opts) {
|
|
4876
|
+
return clickViaPlaywright({
|
|
4877
|
+
cdpUrl: this.cdpUrl,
|
|
4878
|
+
targetId: this.targetId,
|
|
4879
|
+
selector,
|
|
4880
|
+
doubleClick: opts?.doubleClick,
|
|
4881
|
+
button: opts?.button,
|
|
4882
|
+
modifiers: opts?.modifiers,
|
|
4883
|
+
delayMs: opts?.delayMs,
|
|
4884
|
+
timeoutMs: opts?.timeoutMs
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
/**
|
|
4888
|
+
* Click at specific page coordinates.
|
|
4889
|
+
*
|
|
4890
|
+
* Useful for canvas elements, custom widgets, or elements without ARIA roles.
|
|
4891
|
+
*
|
|
4892
|
+
* @param x - X coordinate in pixels
|
|
4893
|
+
* @param y - Y coordinate in pixels
|
|
4894
|
+
* @param opts - Click options (button, clickCount, delayMs)
|
|
4895
|
+
*
|
|
4896
|
+
* @example
|
|
4897
|
+
* ```ts
|
|
4898
|
+
* await page.mouseClick(100, 200);
|
|
4899
|
+
* await page.mouseClick(100, 200, { button: 'right' });
|
|
4900
|
+
* await page.mouseClick(100, 200, { clickCount: 2 }); // double-click
|
|
4901
|
+
* ```
|
|
4902
|
+
*/
|
|
4903
|
+
async mouseClick(x, y, opts) {
|
|
4904
|
+
return mouseClickViaPlaywright({
|
|
4905
|
+
cdpUrl: this.cdpUrl,
|
|
4906
|
+
targetId: this.targetId,
|
|
4907
|
+
x,
|
|
4908
|
+
y,
|
|
4909
|
+
button: opts?.button,
|
|
4910
|
+
clickCount: opts?.clickCount,
|
|
4911
|
+
delayMs: opts?.delayMs
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4914
|
+
/**
|
|
4915
|
+
* Click an element by its visible text content (no snapshot/ref needed).
|
|
4916
|
+
*
|
|
4917
|
+
* Finds and clicks atomically — no stale ref problem.
|
|
4918
|
+
*
|
|
4919
|
+
* @param text - Text content to match
|
|
4920
|
+
* @param opts - Options (exact: require full match, button, modifiers)
|
|
4921
|
+
*
|
|
4922
|
+
* @example
|
|
4923
|
+
* ```ts
|
|
4924
|
+
* await page.clickByText('Submit');
|
|
4925
|
+
* await page.clickByText('Save Changes', { exact: true });
|
|
4926
|
+
* ```
|
|
4927
|
+
*/
|
|
4928
|
+
async clickByText(text, opts) {
|
|
4929
|
+
return clickByTextViaPlaywright({
|
|
4930
|
+
cdpUrl: this.cdpUrl,
|
|
4931
|
+
targetId: this.targetId,
|
|
4932
|
+
text,
|
|
4933
|
+
exact: opts?.exact,
|
|
4934
|
+
button: opts?.button,
|
|
4935
|
+
modifiers: opts?.modifiers,
|
|
4936
|
+
timeoutMs: opts?.timeoutMs
|
|
4937
|
+
});
|
|
4938
|
+
}
|
|
4939
|
+
/**
|
|
4940
|
+
* Click an element by its ARIA role and accessible name (no snapshot/ref needed).
|
|
4941
|
+
*
|
|
4942
|
+
* Finds and clicks atomically — no stale ref problem.
|
|
4943
|
+
*
|
|
4944
|
+
* @param role - ARIA role (e.g. `'button'`, `'link'`, `'menuitem'`)
|
|
4945
|
+
* @param name - Accessible name to match (optional)
|
|
4946
|
+
* @param opts - Click options
|
|
4947
|
+
*
|
|
4948
|
+
* @example
|
|
4949
|
+
* ```ts
|
|
4950
|
+
* await page.clickByRole('button', 'Save');
|
|
4951
|
+
* await page.clickByRole('link', 'Settings');
|
|
4952
|
+
* await page.clickByRole('menuitem', 'Delete');
|
|
4953
|
+
* ```
|
|
4954
|
+
*/
|
|
4955
|
+
async clickByRole(role, name, opts) {
|
|
4956
|
+
return clickByRoleViaPlaywright({
|
|
4957
|
+
cdpUrl: this.cdpUrl,
|
|
4958
|
+
targetId: this.targetId,
|
|
4959
|
+
role,
|
|
4960
|
+
name,
|
|
4961
|
+
button: opts?.button,
|
|
4962
|
+
modifiers: opts?.modifiers,
|
|
4963
|
+
timeoutMs: opts?.timeoutMs
|
|
4964
|
+
});
|
|
4965
|
+
}
|
|
4656
4966
|
/**
|
|
4657
4967
|
* Type text into an input element by ref.
|
|
4658
4968
|
*
|
|
@@ -4817,6 +5127,48 @@ var CrawlPage = class {
|
|
|
4817
5127
|
timeoutMs: opts.timeoutMs
|
|
4818
5128
|
});
|
|
4819
5129
|
}
|
|
5130
|
+
/**
|
|
5131
|
+
* Register a persistent dialog handler for all dialogs (alert, confirm, prompt, beforeunload).
|
|
5132
|
+
*
|
|
5133
|
+
* Unlike `armDialog()` which handles a single expected dialog, `onDialog()` handles
|
|
5134
|
+
* every dialog that appears until cleared. This prevents unexpected dialogs from
|
|
5135
|
+
* blocking the page.
|
|
5136
|
+
*
|
|
5137
|
+
* The handler receives a `DialogEvent` with `accept()` and `dismiss()` methods.
|
|
5138
|
+
* If the handler throws or doesn't call either, the dialog is auto-dismissed.
|
|
5139
|
+
*
|
|
5140
|
+
* Pass `undefined` or `null` to clear the handler and restore default auto-dismiss.
|
|
5141
|
+
*
|
|
5142
|
+
* Note: `armDialog()` takes priority — if a one-shot handler is armed, it handles
|
|
5143
|
+
* the next dialog instead of the persistent handler.
|
|
5144
|
+
*
|
|
5145
|
+
* @param handler - Callback for dialog events, or `undefined`/`null` to clear
|
|
5146
|
+
*
|
|
5147
|
+
* @example
|
|
5148
|
+
* ```ts
|
|
5149
|
+
* // Accept all confirm dialogs, dismiss everything else
|
|
5150
|
+
* page.onDialog((event) => {
|
|
5151
|
+
* if (event.type === 'confirm') event.accept();
|
|
5152
|
+
* else event.dismiss();
|
|
5153
|
+
* });
|
|
5154
|
+
*
|
|
5155
|
+
* // Log and auto-accept all dialogs
|
|
5156
|
+
* page.onDialog(async (event) => {
|
|
5157
|
+
* console.log(`Dialog: ${event.type} — ${event.message}`);
|
|
5158
|
+
* await event.accept();
|
|
5159
|
+
* });
|
|
5160
|
+
*
|
|
5161
|
+
* // Clear the handler (restore default auto-dismiss)
|
|
5162
|
+
* page.onDialog(undefined);
|
|
5163
|
+
* ```
|
|
5164
|
+
*/
|
|
5165
|
+
async onDialog(handler) {
|
|
5166
|
+
return setDialogHandler({
|
|
5167
|
+
cdpUrl: this.cdpUrl,
|
|
5168
|
+
targetId: this.targetId,
|
|
5169
|
+
handler: handler ?? void 0
|
|
5170
|
+
});
|
|
5171
|
+
}
|
|
4820
5172
|
/**
|
|
4821
5173
|
* Arm a one-shot file chooser handler.
|
|
4822
5174
|
*
|
|
@@ -5484,15 +5836,69 @@ var CrawlPage = class {
|
|
|
5484
5836
|
pollMs: opts?.pollMs
|
|
5485
5837
|
});
|
|
5486
5838
|
}
|
|
5839
|
+
// ── Playwright Escape Hatches ─────────────────────────────────
|
|
5840
|
+
/**
|
|
5841
|
+
* Get the underlying Playwright `Page` object for this tab.
|
|
5842
|
+
*
|
|
5843
|
+
* Use this when browserclaw's API doesn't cover your use case and you need
|
|
5844
|
+
* direct access to Playwright's full API (custom locator strategies,
|
|
5845
|
+
* frame manipulation, request interception, etc.).
|
|
5846
|
+
*
|
|
5847
|
+
* **Warning:** Modifications made via the raw Playwright page may conflict
|
|
5848
|
+
* with browserclaw's internal state (e.g. ref tracking). Use with care.
|
|
5849
|
+
*
|
|
5850
|
+
* @returns The Playwright `Page` instance
|
|
5851
|
+
*
|
|
5852
|
+
* @example
|
|
5853
|
+
* ```ts
|
|
5854
|
+
* const pwPage = await page.playwrightPage();
|
|
5855
|
+
*
|
|
5856
|
+
* // Use Playwright's full API directly
|
|
5857
|
+
* await pwPage.locator('.my-component').waitFor({ state: 'visible' });
|
|
5858
|
+
* await pwPage.route('**\/api/**', route => route.fulfill({ body: '{}' }));
|
|
5859
|
+
*
|
|
5860
|
+
* // Access frames
|
|
5861
|
+
* const frame = pwPage.frameLocator('#my-iframe');
|
|
5862
|
+
* ```
|
|
5863
|
+
*/
|
|
5864
|
+
async playwrightPage() {
|
|
5865
|
+
return getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
5866
|
+
}
|
|
5867
|
+
/**
|
|
5868
|
+
* Create a Playwright `Locator` for a CSS selector on this page.
|
|
5869
|
+
*
|
|
5870
|
+
* Convenience method that returns a Playwright locator without needing
|
|
5871
|
+
* to first obtain the Page object. Useful for one-off Playwright operations.
|
|
5872
|
+
*
|
|
5873
|
+
* @param selector - CSS selector or Playwright selector string
|
|
5874
|
+
* @returns A Playwright `Locator` instance
|
|
5875
|
+
*
|
|
5876
|
+
* @example
|
|
5877
|
+
* ```ts
|
|
5878
|
+
* const loc = await page.locator('.modal-dialog button.confirm');
|
|
5879
|
+
* await loc.waitFor({ state: 'visible' });
|
|
5880
|
+
* await loc.click();
|
|
5881
|
+
*
|
|
5882
|
+
* // Use Playwright selectors
|
|
5883
|
+
* const input = await page.locator('input[name="email"]');
|
|
5884
|
+
* await input.fill('test@example.com');
|
|
5885
|
+
* ```
|
|
5886
|
+
*/
|
|
5887
|
+
async locator(selector) {
|
|
5888
|
+
const pwPage = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
5889
|
+
return pwPage.locator(selector);
|
|
5890
|
+
}
|
|
5487
5891
|
};
|
|
5488
5892
|
var BrowserClaw = class _BrowserClaw {
|
|
5489
5893
|
cdpUrl;
|
|
5490
5894
|
ssrfPolicy;
|
|
5895
|
+
recordVideo;
|
|
5491
5896
|
chrome;
|
|
5492
|
-
constructor(cdpUrl, chrome, ssrfPolicy) {
|
|
5897
|
+
constructor(cdpUrl, chrome, ssrfPolicy, recordVideo) {
|
|
5493
5898
|
this.cdpUrl = cdpUrl;
|
|
5494
5899
|
this.chrome = chrome;
|
|
5495
5900
|
this.ssrfPolicy = ssrfPolicy;
|
|
5901
|
+
this.recordVideo = recordVideo;
|
|
5496
5902
|
}
|
|
5497
5903
|
/**
|
|
5498
5904
|
* Launch a new Chrome instance and connect to it.
|
|
@@ -5521,7 +5927,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5521
5927
|
const chrome = await launchChrome(opts);
|
|
5522
5928
|
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
5523
5929
|
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5524
|
-
return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
|
|
5930
|
+
return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy, opts.recordVideo);
|
|
5525
5931
|
}
|
|
5526
5932
|
/**
|
|
5527
5933
|
* Connect to an already-running Chrome instance via its CDP endpoint.
|
|
@@ -5538,12 +5944,22 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5538
5944
|
* ```
|
|
5539
5945
|
*/
|
|
5540
5946
|
static async connect(cdpUrl, opts) {
|
|
5541
|
-
|
|
5542
|
-
|
|
5947
|
+
let resolvedUrl = cdpUrl;
|
|
5948
|
+
if (resolvedUrl === void 0 || resolvedUrl === "") {
|
|
5949
|
+
const discovered = await discoverChromeCdpUrl();
|
|
5950
|
+
if (discovered === null) {
|
|
5951
|
+
throw new Error(
|
|
5952
|
+
"No Chrome instance found on common CDP ports (9222-9226, 9229). Start Chrome with --remote-debugging-port=9222, or pass a CDP URL."
|
|
5953
|
+
);
|
|
5954
|
+
}
|
|
5955
|
+
resolvedUrl = discovered;
|
|
5956
|
+
}
|
|
5957
|
+
if (!await isChromeReachable(resolvedUrl, 3e3, opts?.authToken)) {
|
|
5958
|
+
throw new Error(`Cannot connect to Chrome at ${resolvedUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
5543
5959
|
}
|
|
5544
|
-
await connectBrowser(
|
|
5960
|
+
await connectBrowser(resolvedUrl, opts?.authToken);
|
|
5545
5961
|
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5546
|
-
return new _BrowserClaw(
|
|
5962
|
+
return new _BrowserClaw(resolvedUrl, null, ssrfPolicy, opts?.recordVideo);
|
|
5547
5963
|
}
|
|
5548
5964
|
/**
|
|
5549
5965
|
* Open a URL in a new tab and return the page handle.
|
|
@@ -5558,7 +5974,12 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5558
5974
|
* ```
|
|
5559
5975
|
*/
|
|
5560
5976
|
async open(url) {
|
|
5561
|
-
const tab = await createPageViaPlaywright({
|
|
5977
|
+
const tab = await createPageViaPlaywright({
|
|
5978
|
+
cdpUrl: this.cdpUrl,
|
|
5979
|
+
url,
|
|
5980
|
+
ssrfPolicy: this.ssrfPolicy,
|
|
5981
|
+
recordVideo: this.recordVideo
|
|
5982
|
+
});
|
|
5562
5983
|
return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
|
|
5563
5984
|
}
|
|
5564
5985
|
/**
|
|
@@ -5621,6 +6042,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5621
6042
|
* Playwright connection is closed.
|
|
5622
6043
|
*/
|
|
5623
6044
|
async stop() {
|
|
6045
|
+
clearRecordingContext(this.cdpUrl);
|
|
5624
6046
|
await disconnectBrowser();
|
|
5625
6047
|
if (this.chrome) {
|
|
5626
6048
|
await stopChrome(this.chrome);
|
|
@@ -5659,6 +6081,7 @@ exports.resolvePageByTargetIdOrThrow = resolvePageByTargetIdOrThrow;
|
|
|
5659
6081
|
exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
|
|
5660
6082
|
exports.resolveStrictExistingUploadPaths = resolveStrictExistingUploadPaths;
|
|
5661
6083
|
exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
|
|
6084
|
+
exports.setDialogHandler = setDialogHandler;
|
|
5662
6085
|
exports.waitForChallengeViaPlaywright = waitForChallengeViaPlaywright;
|
|
5663
6086
|
exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
|
|
5664
6087
|
exports.withPageScopedCdpClient = withPageScopedCdpClient;
|