@mozilla/firefox-devtools-mcp 0.9.15 → 0.10.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 +75 -37
- package/dist/index.js +1044 -707
- package/dist/snapshot.injected.global.js +1 -1
- package/package.json +5 -4
- package/scripts/build-mcpb.mjs +1 -1
package/dist/index.js
CHANGED
|
@@ -51,6 +51,13 @@ function previewExcerpt(text, preview) {
|
|
|
51
51
|
}
|
|
52
52
|
return truncateText(text, Math.min(preview, TOKEN_LIMITS.MAX_RESPONSE_CHARS), "...");
|
|
53
53
|
}
|
|
54
|
+
function truncationFooter(hiddenCount, unit, hints) {
|
|
55
|
+
if (hiddenCount <= 0) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
const head = `[+${hiddenCount} ${unit} hidden`;
|
|
59
|
+
return hints.length > 0 ? `${head}; ${hints.join(", ")}]` : `${head}]`;
|
|
60
|
+
}
|
|
54
61
|
function truncateHeaders(headers) {
|
|
55
62
|
if (!headers) {
|
|
56
63
|
return null;
|
|
@@ -124,21 +131,6 @@ var init_response_helpers = __esm({
|
|
|
124
131
|
}
|
|
125
132
|
});
|
|
126
133
|
|
|
127
|
-
// src/tools/module.ts
|
|
128
|
-
function defineModule(config2) {
|
|
129
|
-
return {
|
|
130
|
-
name: config2.name,
|
|
131
|
-
description: config2.description,
|
|
132
|
-
...config2.privileged ? { privileged: true } : {},
|
|
133
|
-
tools: config2.tools.map(([definition, handler]) => ({ definition, handler }))
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
var init_module = __esm({
|
|
137
|
-
"src/tools/module.ts"() {
|
|
138
|
-
"use strict";
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
134
|
// node_modules/zod/v4/core/core.js
|
|
143
135
|
// @__NO_SIDE_EFFECTS__
|
|
144
136
|
function $constructor(name, initializer3, params) {
|
|
@@ -14849,7 +14841,7 @@ var init_constants = __esm({
|
|
|
14849
14841
|
"src/config/constants.ts"() {
|
|
14850
14842
|
"use strict";
|
|
14851
14843
|
SERVER_NAME = true ? "@mozilla/firefox-devtools-mcp" : "firefox-devtools";
|
|
14852
|
-
SERVER_VERSION = true ? "0.
|
|
14844
|
+
SERVER_VERSION = true ? "0.10.1" : "dev";
|
|
14853
14845
|
}
|
|
14854
14846
|
});
|
|
14855
14847
|
|
|
@@ -14970,7 +14962,16 @@ var init_profile = __esm({
|
|
|
14970
14962
|
// src/firefox/core.ts
|
|
14971
14963
|
import { Builder, Browser, Capabilities } from "selenium-webdriver";
|
|
14972
14964
|
import firefox from "selenium-webdriver/firefox.js";
|
|
14973
|
-
import {
|
|
14965
|
+
import {
|
|
14966
|
+
mkdirSync as mkdirSync2,
|
|
14967
|
+
openSync,
|
|
14968
|
+
closeSync,
|
|
14969
|
+
existsSync as existsSync2,
|
|
14970
|
+
readdirSync,
|
|
14971
|
+
statSync,
|
|
14972
|
+
readFileSync
|
|
14973
|
+
} from "fs";
|
|
14974
|
+
import { connect as netConnect } from "net";
|
|
14974
14975
|
import { homedir } from "os";
|
|
14975
14976
|
import { join as join2, delimiter } from "path";
|
|
14976
14977
|
function findGeckodriverInPath(binaryName) {
|
|
@@ -15016,6 +15017,57 @@ async function findGeckodriverInNpmPackage() {
|
|
|
15016
15017
|
return null;
|
|
15017
15018
|
}
|
|
15018
15019
|
}
|
|
15020
|
+
function lookupMarionettePort() {
|
|
15021
|
+
const instancesDir = join2(homedir(), ".firefox-devtools-mcp", "instances");
|
|
15022
|
+
if (!existsSync2(instancesDir)) {
|
|
15023
|
+
logDebug(`Failed to lookup Marionette port: ${instancesDir} doesn't exist.`);
|
|
15024
|
+
return;
|
|
15025
|
+
}
|
|
15026
|
+
const files = readdirSync(instancesDir);
|
|
15027
|
+
const portFiles = files.filter((f) => /^\d+\.port$/.test(f));
|
|
15028
|
+
const mostRecent = portFiles.map((f) => ({
|
|
15029
|
+
name: f,
|
|
15030
|
+
path: join2(instancesDir, f),
|
|
15031
|
+
mtime: statSync(join2(instancesDir, f)).mtimeMs
|
|
15032
|
+
})).sort((a, b) => b.mtime - a.mtime)[0];
|
|
15033
|
+
if (mostRecent) {
|
|
15034
|
+
const pid = Number(mostRecent.name.substring(0, mostRecent.name.length - 5));
|
|
15035
|
+
if (!checkProcess(pid)) {
|
|
15036
|
+
logDebug(`Failed to lookup Marionette port: No process with PID ${pid} is running.`);
|
|
15037
|
+
return;
|
|
15038
|
+
}
|
|
15039
|
+
logDebug(`Reading Marionette port from ${mostRecent.path}`);
|
|
15040
|
+
const content = readFileSync(mostRecent.path, "utf-8").trim();
|
|
15041
|
+
if (/^\d+$/.test(content)) {
|
|
15042
|
+
return Number(content);
|
|
15043
|
+
} else {
|
|
15044
|
+
logDebug(`Failed to lookup Marionette port: "${content}" is not a number.`);
|
|
15045
|
+
}
|
|
15046
|
+
} else {
|
|
15047
|
+
logDebug(`Failed to lookup Marionette port: No port file found in ${instancesDir}.`);
|
|
15048
|
+
}
|
|
15049
|
+
}
|
|
15050
|
+
function checkProcess(pid) {
|
|
15051
|
+
try {
|
|
15052
|
+
process.kill(pid, 0);
|
|
15053
|
+
return true;
|
|
15054
|
+
} catch {
|
|
15055
|
+
return false;
|
|
15056
|
+
}
|
|
15057
|
+
}
|
|
15058
|
+
function checkPort(port, timeoutMs = 1e3) {
|
|
15059
|
+
return new Promise((resolve4) => {
|
|
15060
|
+
const socket = netConnect({ host: "127.0.0.1", port });
|
|
15061
|
+
const done = (reason) => {
|
|
15062
|
+
socket.destroy();
|
|
15063
|
+
resolve4(reason);
|
|
15064
|
+
};
|
|
15065
|
+
socket.setTimeout(timeoutMs);
|
|
15066
|
+
socket.once("connect", () => done(null));
|
|
15067
|
+
socket.once("timeout", () => done(`connection to 127.0.0.1:${port} timed out`));
|
|
15068
|
+
socket.once("error", (error2) => done(error2.message));
|
|
15069
|
+
});
|
|
15070
|
+
}
|
|
15019
15071
|
async function findGeckodriver() {
|
|
15020
15072
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
15021
15073
|
const binaryName = `geckodriver${ext}`;
|
|
@@ -15047,8 +15099,15 @@ var init_core3 = __esm({
|
|
|
15047
15099
|
*/
|
|
15048
15100
|
async connect() {
|
|
15049
15101
|
const isAndroid = this.options.androidDevice !== void 0;
|
|
15102
|
+
const androidPackage = this.options.androidPackage ?? "org.mozilla.firefox";
|
|
15103
|
+
if (isAndroid && !this.options.androidWipeAppData) {
|
|
15104
|
+
throw new Error(
|
|
15105
|
+
`Firefox for Android mode wipes all data of ${androidPackage} on the device: tabs, history, bookmarks, passwords, cookies and settings are all lost, because geckodriver clears the app data before every session and cannot be configured to skip it. Pass --android-wipe-app-data (or ANDROID_WIPE_APP_DATA=true) to confirm. Prefer a build dedicated to automation, for instance --android-package org.mozilla.fenix for Nightly.`
|
|
15106
|
+
);
|
|
15107
|
+
}
|
|
15050
15108
|
if (isAndroid) {
|
|
15051
15109
|
log("Launching Firefox for Android via ADB...");
|
|
15110
|
+
log(`Wiping all data of ${androidPackage} on the device`);
|
|
15052
15111
|
} else if (this.options.connectExisting) {
|
|
15053
15112
|
log("Connecting to existing Firefox via Marionette...");
|
|
15054
15113
|
} else {
|
|
@@ -15057,8 +15116,7 @@ var init_core3 = __esm({
|
|
|
15057
15116
|
if (isAndroid) {
|
|
15058
15117
|
const geckodriverPath = await findGeckodriver();
|
|
15059
15118
|
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
15060
|
-
const
|
|
15061
|
-
const mozOptions = { androidPackage: pkg };
|
|
15119
|
+
const mozOptions = { androidPackage };
|
|
15062
15120
|
const deviceSerial = this.options.androidDevice;
|
|
15063
15121
|
if (deviceSerial && deviceSerial !== "auto") {
|
|
15064
15122
|
mozOptions.androidDeviceSerial = deviceSerial;
|
|
@@ -15075,7 +15133,31 @@ var init_core3 = __esm({
|
|
|
15075
15133
|
const serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
|
|
15076
15134
|
this.driver = firefox.Driver.createSession(caps, serviceBuilder.build());
|
|
15077
15135
|
} else if (this.options.connectExisting) {
|
|
15078
|
-
|
|
15136
|
+
let port = this.options.marionettePort ?? 2828;
|
|
15137
|
+
if (this.options.lookupMarionettePort) {
|
|
15138
|
+
logDebug("Looking up Marionette port");
|
|
15139
|
+
const lookedUpPort = lookupMarionettePort();
|
|
15140
|
+
if (lookedUpPort !== void 0) {
|
|
15141
|
+
port = lookedUpPort;
|
|
15142
|
+
} else {
|
|
15143
|
+
throw new Error(
|
|
15144
|
+
"Marionette port not found: please enable Firefox remote control for AI tooling using the AI assistant companion button."
|
|
15145
|
+
);
|
|
15146
|
+
}
|
|
15147
|
+
}
|
|
15148
|
+
logDebug(`Using Marionette port ${port}`);
|
|
15149
|
+
const failure = await checkPort(port);
|
|
15150
|
+
if (failure) {
|
|
15151
|
+
if (this.options.lookupMarionettePort) {
|
|
15152
|
+
throw new Error(
|
|
15153
|
+
`No Marionette listener on 127.0.0.1:${port} (${failure}). Please enable Firefox remote control for AI tooling using the AI assistant companion button.`
|
|
15154
|
+
);
|
|
15155
|
+
} else {
|
|
15156
|
+
throw new Error(
|
|
15157
|
+
`No Marionette listener on 127.0.0.1:${port} (${failure}). Start Firefox with both flags: firefox --marionette --remote-debugging-port, or pass the port it is actually using via --marionette-port.`
|
|
15158
|
+
);
|
|
15159
|
+
}
|
|
15160
|
+
}
|
|
15079
15161
|
const geckodriverPath = await findGeckodriver();
|
|
15080
15162
|
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
15081
15163
|
const serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
|
|
@@ -15266,68 +15348,6 @@ var init_core3 = __esm({
|
|
|
15266
15348
|
getOptions() {
|
|
15267
15349
|
return this.options;
|
|
15268
15350
|
}
|
|
15269
|
-
/**
|
|
15270
|
-
* Wait for WebSocket to be in OPEN state
|
|
15271
|
-
*/
|
|
15272
|
-
async waitForWebSocketOpen(ws, timeout = 5e3) {
|
|
15273
|
-
if (ws.readyState === 1) {
|
|
15274
|
-
return;
|
|
15275
|
-
}
|
|
15276
|
-
if (ws.readyState === 0) {
|
|
15277
|
-
return new Promise((resolve4, reject) => {
|
|
15278
|
-
const timeoutId = setTimeout(() => {
|
|
15279
|
-
ws.off("open", onOpen);
|
|
15280
|
-
reject(new Error("Timeout waiting for WebSocket to open"));
|
|
15281
|
-
}, timeout);
|
|
15282
|
-
const onOpen = () => {
|
|
15283
|
-
clearTimeout(timeoutId);
|
|
15284
|
-
ws.off("open", onOpen);
|
|
15285
|
-
resolve4();
|
|
15286
|
-
};
|
|
15287
|
-
ws.on("open", onOpen);
|
|
15288
|
-
});
|
|
15289
|
-
}
|
|
15290
|
-
throw new Error(`WebSocket is not open: readyState ${ws.readyState}`);
|
|
15291
|
-
}
|
|
15292
|
-
/**
|
|
15293
|
-
* Send raw BiDi command and get response
|
|
15294
|
-
*/
|
|
15295
|
-
async sendBiDiCommand(method, params = {}) {
|
|
15296
|
-
if (!this.driver) {
|
|
15297
|
-
throw new Error("Driver not connected");
|
|
15298
|
-
}
|
|
15299
|
-
const bidi = await this.driver.getBidi();
|
|
15300
|
-
const ws = bidi.socket;
|
|
15301
|
-
await this.waitForWebSocketOpen(ws);
|
|
15302
|
-
const id = Math.floor(Math.random() * 1e6);
|
|
15303
|
-
return new Promise((resolve4, reject) => {
|
|
15304
|
-
const messageHandler = (data) => {
|
|
15305
|
-
try {
|
|
15306
|
-
const payload = JSON.parse(data.toString());
|
|
15307
|
-
if (payload.id === id) {
|
|
15308
|
-
ws.off("message", messageHandler);
|
|
15309
|
-
if (payload.error) {
|
|
15310
|
-
reject(new Error(`BiDi error: ${JSON.stringify(payload.error)}`));
|
|
15311
|
-
} else {
|
|
15312
|
-
resolve4(payload.result);
|
|
15313
|
-
}
|
|
15314
|
-
}
|
|
15315
|
-
} catch {
|
|
15316
|
-
}
|
|
15317
|
-
};
|
|
15318
|
-
ws.on("message", messageHandler);
|
|
15319
|
-
const command = {
|
|
15320
|
-
id,
|
|
15321
|
-
method,
|
|
15322
|
-
params
|
|
15323
|
-
};
|
|
15324
|
-
ws.send(JSON.stringify(command));
|
|
15325
|
-
setTimeout(() => {
|
|
15326
|
-
ws.off("message", messageHandler);
|
|
15327
|
-
reject(new Error(`BiDi command timeout: ${method}`));
|
|
15328
|
-
}, 1e4);
|
|
15329
|
-
});
|
|
15330
|
-
}
|
|
15331
15351
|
/**
|
|
15332
15352
|
* Close driver and cleanup.
|
|
15333
15353
|
* - Tries graceful quit() with a timeout; on timeout, force-kills via onQuit_().
|
|
@@ -15397,6 +15417,168 @@ var init_core3 = __esm({
|
|
|
15397
15417
|
}
|
|
15398
15418
|
});
|
|
15399
15419
|
|
|
15420
|
+
// src/firefox/bidi.ts
|
|
15421
|
+
import EventEmitter from "events";
|
|
15422
|
+
var BiDiFacade;
|
|
15423
|
+
var init_bidi = __esm({
|
|
15424
|
+
"src/firefox/bidi.ts"() {
|
|
15425
|
+
"use strict";
|
|
15426
|
+
init_logger();
|
|
15427
|
+
BiDiFacade = class extends EventEmitter {
|
|
15428
|
+
constructor(driver) {
|
|
15429
|
+
super();
|
|
15430
|
+
this.driver = driver;
|
|
15431
|
+
}
|
|
15432
|
+
listening = false;
|
|
15433
|
+
nextCommandId = 1;
|
|
15434
|
+
async subscribe(events) {
|
|
15435
|
+
const bidi = await this.driver.getBidi();
|
|
15436
|
+
if (!this.listening) {
|
|
15437
|
+
this.listenForEvents(bidi.socket);
|
|
15438
|
+
this.listening = true;
|
|
15439
|
+
}
|
|
15440
|
+
await bidi.subscribe(events);
|
|
15441
|
+
}
|
|
15442
|
+
async sendCommand(method, params = {}) {
|
|
15443
|
+
const bidi = await this.driver.getBidi();
|
|
15444
|
+
const ws = bidi.socket;
|
|
15445
|
+
await this.waitForWebSocketOpen(ws);
|
|
15446
|
+
const id = this.nextCommandId++;
|
|
15447
|
+
return new Promise((resolve4, reject) => {
|
|
15448
|
+
const messageHandler = (data) => {
|
|
15449
|
+
try {
|
|
15450
|
+
const payload = JSON.parse(data.toString());
|
|
15451
|
+
if (payload.id === id) {
|
|
15452
|
+
ws.off("message", messageHandler);
|
|
15453
|
+
if (payload.error) {
|
|
15454
|
+
reject(new Error(`BiDi error: ${JSON.stringify(payload.error)}`));
|
|
15455
|
+
} else {
|
|
15456
|
+
resolve4(payload.result);
|
|
15457
|
+
}
|
|
15458
|
+
}
|
|
15459
|
+
} catch {
|
|
15460
|
+
}
|
|
15461
|
+
};
|
|
15462
|
+
ws.on("message", messageHandler);
|
|
15463
|
+
const command = {
|
|
15464
|
+
id,
|
|
15465
|
+
method,
|
|
15466
|
+
params
|
|
15467
|
+
};
|
|
15468
|
+
ws.send(JSON.stringify(command));
|
|
15469
|
+
setTimeout(() => {
|
|
15470
|
+
ws.off("message", messageHandler);
|
|
15471
|
+
reject(new Error(`BiDi command timeout: ${method}`));
|
|
15472
|
+
}, 1e4);
|
|
15473
|
+
});
|
|
15474
|
+
}
|
|
15475
|
+
listenForEvents(ws) {
|
|
15476
|
+
ws.on("message", (data) => {
|
|
15477
|
+
let payload;
|
|
15478
|
+
try {
|
|
15479
|
+
payload = JSON.parse(data.toString());
|
|
15480
|
+
} catch {
|
|
15481
|
+
return;
|
|
15482
|
+
}
|
|
15483
|
+
if (payload?.type === "event" && payload.method) {
|
|
15484
|
+
try {
|
|
15485
|
+
this.emit(payload.method, payload.params);
|
|
15486
|
+
} catch (error2) {
|
|
15487
|
+
logDebug(
|
|
15488
|
+
`Error emitting ${payload.method} event: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
15489
|
+
);
|
|
15490
|
+
}
|
|
15491
|
+
}
|
|
15492
|
+
});
|
|
15493
|
+
}
|
|
15494
|
+
/**
|
|
15495
|
+
* Wait for WebSocket to be in OPEN state
|
|
15496
|
+
*/
|
|
15497
|
+
async waitForWebSocketOpen(ws, timeout = 5e3) {
|
|
15498
|
+
if (ws.readyState === 1) {
|
|
15499
|
+
return;
|
|
15500
|
+
}
|
|
15501
|
+
if (ws.readyState === 0) {
|
|
15502
|
+
return new Promise((resolve4, reject) => {
|
|
15503
|
+
const timeoutId = setTimeout(() => {
|
|
15504
|
+
ws.off("open", onOpen);
|
|
15505
|
+
reject(new Error("Timeout waiting for WebSocket to open"));
|
|
15506
|
+
}, timeout);
|
|
15507
|
+
const onOpen = () => {
|
|
15508
|
+
clearTimeout(timeoutId);
|
|
15509
|
+
ws.off("open", onOpen);
|
|
15510
|
+
resolve4();
|
|
15511
|
+
};
|
|
15512
|
+
ws.on("open", onOpen);
|
|
15513
|
+
});
|
|
15514
|
+
}
|
|
15515
|
+
throw new Error(`WebSocket is not open: readyState ${ws.readyState}`);
|
|
15516
|
+
}
|
|
15517
|
+
};
|
|
15518
|
+
}
|
|
15519
|
+
});
|
|
15520
|
+
|
|
15521
|
+
// src/utils/remote-value.ts
|
|
15522
|
+
function remoteValueToNative(rv) {
|
|
15523
|
+
if (!rv || typeof rv !== "object") {
|
|
15524
|
+
return rv;
|
|
15525
|
+
}
|
|
15526
|
+
const { type, value } = rv;
|
|
15527
|
+
switch (type) {
|
|
15528
|
+
case "undefined":
|
|
15529
|
+
return void 0;
|
|
15530
|
+
case "null":
|
|
15531
|
+
return null;
|
|
15532
|
+
case "string":
|
|
15533
|
+
case "boolean":
|
|
15534
|
+
return value;
|
|
15535
|
+
case "number":
|
|
15536
|
+
if (value === "NaN") {
|
|
15537
|
+
return "NaN";
|
|
15538
|
+
}
|
|
15539
|
+
if (value === "Infinity") {
|
|
15540
|
+
return "Infinity";
|
|
15541
|
+
}
|
|
15542
|
+
if (value === "-Infinity") {
|
|
15543
|
+
return "-Infinity";
|
|
15544
|
+
}
|
|
15545
|
+
if (value === "-0") {
|
|
15546
|
+
return "-0";
|
|
15547
|
+
}
|
|
15548
|
+
return value;
|
|
15549
|
+
case "bigint":
|
|
15550
|
+
return `${value}n`;
|
|
15551
|
+
case "array":
|
|
15552
|
+
return value.map(remoteValueToNative);
|
|
15553
|
+
case "object":
|
|
15554
|
+
return Object.fromEntries(
|
|
15555
|
+
value.map(([k, v]) => [k, remoteValueToNative(v)])
|
|
15556
|
+
);
|
|
15557
|
+
case "map":
|
|
15558
|
+
return Object.fromEntries(
|
|
15559
|
+
value.map(([k, v]) => [
|
|
15560
|
+
typeof k === "object" ? JSON.stringify(remoteValueToNative(k)) : String(k),
|
|
15561
|
+
remoteValueToNative(v)
|
|
15562
|
+
])
|
|
15563
|
+
);
|
|
15564
|
+
case "set":
|
|
15565
|
+
return value.map(remoteValueToNative);
|
|
15566
|
+
case "regexp": {
|
|
15567
|
+
const { pattern, flags } = value;
|
|
15568
|
+
return `/${pattern}/${flags ?? ""}`;
|
|
15569
|
+
}
|
|
15570
|
+
case "date":
|
|
15571
|
+
return value;
|
|
15572
|
+
default:
|
|
15573
|
+
return `[${type}]`;
|
|
15574
|
+
}
|
|
15575
|
+
}
|
|
15576
|
+
var init_remote_value = __esm({
|
|
15577
|
+
"src/utils/remote-value.ts"() {
|
|
15578
|
+
"use strict";
|
|
15579
|
+
}
|
|
15580
|
+
});
|
|
15581
|
+
|
|
15400
15582
|
// src/firefox/events/console.ts
|
|
15401
15583
|
var MAX_CONSOLE_MESSAGES, CONSOLE_TTL_MS, ConsoleEvents;
|
|
15402
15584
|
var init_console = __esm({
|
|
@@ -15406,8 +15588,8 @@ var init_console = __esm({
|
|
|
15406
15588
|
MAX_CONSOLE_MESSAGES = 1e3;
|
|
15407
15589
|
CONSOLE_TTL_MS = 5 * 60 * 1e3;
|
|
15408
15590
|
ConsoleEvents = class {
|
|
15409
|
-
constructor(
|
|
15410
|
-
this.
|
|
15591
|
+
constructor(bidi, options = {}) {
|
|
15592
|
+
this.bidi = bidi;
|
|
15411
15593
|
this.options = {
|
|
15412
15594
|
autoClearOnNavigate: false,
|
|
15413
15595
|
// Changed default to false to preserve logs across tabs
|
|
@@ -15420,47 +15602,27 @@ var init_console = __esm({
|
|
|
15420
15602
|
/**
|
|
15421
15603
|
* Subscribe to BiDi console events and navigation lifecycle
|
|
15422
15604
|
*/
|
|
15423
|
-
async subscribe(
|
|
15605
|
+
async subscribe() {
|
|
15424
15606
|
if (this.subscribed) {
|
|
15425
15607
|
return;
|
|
15426
15608
|
}
|
|
15427
|
-
|
|
15428
|
-
await bidi.subscribe("
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
try {
|
|
15440
|
-
const payload = JSON.parse(data.toString());
|
|
15441
|
-
if (payload?.method === "log.entryAdded") {
|
|
15442
|
-
const entry = payload.params;
|
|
15443
|
-
const message = {
|
|
15444
|
-
level: entry.level || "info",
|
|
15445
|
-
text: entry.text || (entry.args ? JSON.stringify(entry.args) : ""),
|
|
15446
|
-
timestamp: entry.timestamp || Date.now(),
|
|
15447
|
-
source: entry.source?.realm,
|
|
15448
|
-
args: entry.args
|
|
15449
|
-
};
|
|
15450
|
-
this.consoleMessages.push(message);
|
|
15451
|
-
logDebug(`Console [${message.level}]: ${message.text}`);
|
|
15452
|
-
}
|
|
15453
|
-
if (payload?.method === "browsingContext.load" || payload?.method === "browsingContext.domContentLoaded") {
|
|
15454
|
-
if (this.options.autoClearOnNavigate) {
|
|
15455
|
-
this.clearMessages();
|
|
15456
|
-
}
|
|
15457
|
-
if (this.options.onNavigate) {
|
|
15458
|
-
this.options.onNavigate();
|
|
15459
|
-
}
|
|
15460
|
-
}
|
|
15461
|
-
} catch {
|
|
15462
|
-
}
|
|
15609
|
+
await this.bidi.subscribe("log.entryAdded");
|
|
15610
|
+
await this.bidi.subscribe(["browsingContext.load", "browsingContext.domContentLoaded"]);
|
|
15611
|
+
this.bidi.on("log.entryAdded", (entry) => {
|
|
15612
|
+
const message = {
|
|
15613
|
+
level: entry.level || "info",
|
|
15614
|
+
text: entry.text || (entry.args ? JSON.stringify(entry.args) : ""),
|
|
15615
|
+
timestamp: entry.timestamp || Date.now(),
|
|
15616
|
+
source: entry.source?.realm,
|
|
15617
|
+
args: entry.args
|
|
15618
|
+
};
|
|
15619
|
+
this.consoleMessages.push(message);
|
|
15620
|
+
logDebug(`Console [${message.level}]: ${message.text}`);
|
|
15463
15621
|
});
|
|
15622
|
+
if (this.options.autoClearOnNavigate) {
|
|
15623
|
+
this.bidi.on("browsingContext.load", () => this.clearMessages());
|
|
15624
|
+
this.bidi.on("browsingContext.domContentLoaded", () => this.clearMessages());
|
|
15625
|
+
}
|
|
15464
15626
|
this.subscribed = true;
|
|
15465
15627
|
logDebug("Console listener active with lifecycle hooks");
|
|
15466
15628
|
}
|
|
@@ -15499,16 +15661,17 @@ var init_console = __esm({
|
|
|
15499
15661
|
});
|
|
15500
15662
|
|
|
15501
15663
|
// src/firefox/events/network.ts
|
|
15502
|
-
var MAX_NETWORK_REQUESTS, NETWORK_TTL_MS, NetworkEvents;
|
|
15664
|
+
var MAX_NETWORK_REQUESTS, NETWORK_TTL_MS, MAX_ENCODED_DATA_SIZE, NetworkEvents;
|
|
15503
15665
|
var init_network = __esm({
|
|
15504
15666
|
"src/firefox/events/network.ts"() {
|
|
15505
15667
|
"use strict";
|
|
15506
15668
|
init_logger();
|
|
15507
15669
|
MAX_NETWORK_REQUESTS = 1e3;
|
|
15508
15670
|
NETWORK_TTL_MS = 5 * 60 * 1e3;
|
|
15671
|
+
MAX_ENCODED_DATA_SIZE = 10 * 1e3 * 1e3;
|
|
15509
15672
|
NetworkEvents = class {
|
|
15510
|
-
constructor(
|
|
15511
|
-
this.
|
|
15673
|
+
constructor(bidi, options = {}) {
|
|
15674
|
+
this.bidi = bidi;
|
|
15512
15675
|
this.options = {
|
|
15513
15676
|
autoClearOnNavigate: true,
|
|
15514
15677
|
...options
|
|
@@ -15519,102 +15682,155 @@ var init_network = __esm({
|
|
|
15519
15682
|
enabled = false;
|
|
15520
15683
|
requestStartTimes = /* @__PURE__ */ new Map();
|
|
15521
15684
|
options;
|
|
15685
|
+
collectorId = null;
|
|
15522
15686
|
/**
|
|
15523
15687
|
* Subscribe to BiDi network events and navigation lifecycle
|
|
15524
15688
|
* Enables monitoring by default (always-on capture)
|
|
15525
15689
|
*/
|
|
15526
|
-
async subscribe(
|
|
15690
|
+
async subscribe() {
|
|
15527
15691
|
if (this.subscribed) {
|
|
15528
15692
|
return;
|
|
15529
15693
|
}
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
|
|
15545
|
-
|
|
15546
|
-
|
|
15547
|
-
|
|
15548
|
-
|
|
15549
|
-
|
|
15550
|
-
|
|
15551
|
-
|
|
15552
|
-
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
15560
|
-
|
|
15561
|
-
|
|
15562
|
-
return;
|
|
15563
|
-
}
|
|
15564
|
-
this.requestStartTimes.set(requestId, Date.now());
|
|
15565
|
-
const record2 = {
|
|
15566
|
-
id: requestId,
|
|
15567
|
-
url: req.request?.url || "",
|
|
15568
|
-
method: req.request?.method || "GET",
|
|
15569
|
-
timestamp: Date.now(),
|
|
15570
|
-
resourceType: this.guessResourceType(req.request?.url || ""),
|
|
15571
|
-
isXHR: req.initiator?.type === "xmlhttprequest" || req.initiator?.type === "fetch",
|
|
15572
|
-
requestHeaders: this.parseHeaders(req.request?.headers || []),
|
|
15573
|
-
timings: {
|
|
15574
|
-
requestTime: Date.now()
|
|
15575
|
-
}
|
|
15576
|
-
};
|
|
15577
|
-
this.networkRecords.set(requestId, record2);
|
|
15578
|
-
logDebug(`Network request [${record2.method}]: ${record2.url}`);
|
|
15579
|
-
}
|
|
15580
|
-
if (payload?.method === "network.responseStarted") {
|
|
15581
|
-
const resp = payload.params;
|
|
15582
|
-
const requestId = resp.request?.request || resp.requestId;
|
|
15583
|
-
if (!requestId) {
|
|
15584
|
-
return;
|
|
15585
|
-
}
|
|
15586
|
-
const existing = this.networkRecords.get(requestId);
|
|
15587
|
-
if (existing) {
|
|
15588
|
-
existing.status = resp.response?.status;
|
|
15589
|
-
existing.statusText = resp.response?.statusText || "";
|
|
15590
|
-
existing.responseHeaders = this.parseHeaders(resp.response?.headers || []);
|
|
15591
|
-
}
|
|
15694
|
+
await this.bidi.subscribe([
|
|
15695
|
+
"network.beforeRequestSent",
|
|
15696
|
+
"network.responseStarted",
|
|
15697
|
+
"network.responseCompleted"
|
|
15698
|
+
]);
|
|
15699
|
+
await this.bidi.subscribe(["browsingContext.load", "browsingContext.domContentLoaded"]);
|
|
15700
|
+
const onLoadEvent = () => {
|
|
15701
|
+
if (this.enabled && this.options.autoClearOnNavigate) {
|
|
15702
|
+
this.clearRequests();
|
|
15703
|
+
}
|
|
15704
|
+
};
|
|
15705
|
+
this.bidi.on("browsingContext.domContentLoaded", onLoadEvent);
|
|
15706
|
+
this.bidi.on("browsingContext.load", onLoadEvent);
|
|
15707
|
+
this.bidi.on("network.beforeRequestSent", (req) => {
|
|
15708
|
+
if (!this.enabled) {
|
|
15709
|
+
return;
|
|
15710
|
+
}
|
|
15711
|
+
const requestId = req.request?.request || req.requestId;
|
|
15712
|
+
if (!requestId) {
|
|
15713
|
+
return;
|
|
15714
|
+
}
|
|
15715
|
+
this.requestStartTimes.set(requestId, Date.now());
|
|
15716
|
+
const record2 = {
|
|
15717
|
+
id: requestId,
|
|
15718
|
+
url: req.request?.url || "",
|
|
15719
|
+
method: req.request?.method || "GET",
|
|
15720
|
+
timestamp: Date.now(),
|
|
15721
|
+
resourceType: this.guessResourceType(req.request?.url || ""),
|
|
15722
|
+
isXHR: req.initiator?.type === "xmlhttprequest" || req.initiator?.type === "fetch",
|
|
15723
|
+
requestHeaders: this.parseHeaders(req.request?.headers || []),
|
|
15724
|
+
timings: {
|
|
15725
|
+
requestTime: Date.now()
|
|
15592
15726
|
}
|
|
15593
|
-
|
|
15594
|
-
|
|
15595
|
-
|
|
15596
|
-
|
|
15597
|
-
|
|
15598
|
-
|
|
15599
|
-
|
|
15600
|
-
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
|
|
15727
|
+
};
|
|
15728
|
+
this.networkRecords.set(requestId, record2);
|
|
15729
|
+
logDebug(`Network request [${record2.method}]: ${record2.url}`);
|
|
15730
|
+
});
|
|
15731
|
+
this.bidi.on("network.responseStarted", (resp) => {
|
|
15732
|
+
if (!this.enabled) {
|
|
15733
|
+
return;
|
|
15734
|
+
}
|
|
15735
|
+
const requestId = resp.request?.request || resp.requestId;
|
|
15736
|
+
if (!requestId) {
|
|
15737
|
+
return;
|
|
15738
|
+
}
|
|
15739
|
+
const existing = this.networkRecords.get(requestId);
|
|
15740
|
+
if (existing) {
|
|
15741
|
+
existing.status = resp.response?.status;
|
|
15742
|
+
existing.statusText = resp.response?.statusText || "";
|
|
15743
|
+
existing.responseHeaders = this.parseHeaders(resp.response?.headers || []);
|
|
15744
|
+
}
|
|
15745
|
+
});
|
|
15746
|
+
this.bidi.on("network.responseCompleted", (resp) => {
|
|
15747
|
+
if (!this.enabled) {
|
|
15748
|
+
return;
|
|
15749
|
+
}
|
|
15750
|
+
const requestId = resp.request?.request || resp.requestId;
|
|
15751
|
+
if (!requestId) {
|
|
15752
|
+
return;
|
|
15753
|
+
}
|
|
15754
|
+
const existing = this.networkRecords.get(requestId);
|
|
15755
|
+
const startTime = this.requestStartTimes.get(requestId);
|
|
15756
|
+
if (existing && startTime) {
|
|
15757
|
+
existing.timings.responseTime = Date.now();
|
|
15758
|
+
existing.timings.duration = Date.now() - startTime;
|
|
15759
|
+
if (!existing.status && resp.response?.status) {
|
|
15760
|
+
existing.status = resp.response.status;
|
|
15761
|
+
existing.statusText = resp.response.statusText || "";
|
|
15610
15762
|
}
|
|
15611
|
-
} catch {
|
|
15612
15763
|
}
|
|
15764
|
+
this.requestStartTimes.delete(requestId);
|
|
15613
15765
|
});
|
|
15766
|
+
await this.registerDataCollector();
|
|
15614
15767
|
this.subscribed = true;
|
|
15615
15768
|
this.enabled = true;
|
|
15616
15769
|
logDebug("Network listener ready with lifecycle hooks (monitoring enabled by default)");
|
|
15617
15770
|
}
|
|
15771
|
+
/**
|
|
15772
|
+
* Register a BiDi network data collector for request and response bodies.
|
|
15773
|
+
* Failures are non-fatal and simply leave body capture disabled.
|
|
15774
|
+
*/
|
|
15775
|
+
async registerDataCollector() {
|
|
15776
|
+
if (this.options.captureBodies === false) {
|
|
15777
|
+
logDebug("Network body capture disabled, skipping data collector registration");
|
|
15778
|
+
return;
|
|
15779
|
+
}
|
|
15780
|
+
try {
|
|
15781
|
+
const result = await this.bidi.sendCommand("network.addDataCollector", {
|
|
15782
|
+
dataTypes: ["request", "response"],
|
|
15783
|
+
maxEncodedDataSize: MAX_ENCODED_DATA_SIZE
|
|
15784
|
+
});
|
|
15785
|
+
this.collectorId = result?.collector ?? null;
|
|
15786
|
+
if (this.collectorId) {
|
|
15787
|
+
logDebug(`Network data collector registered (${this.collectorId})`);
|
|
15788
|
+
}
|
|
15789
|
+
} catch (error2) {
|
|
15790
|
+
logDebug(
|
|
15791
|
+
`Network data collector unavailable, response bodies will not be captured: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
15792
|
+
);
|
|
15793
|
+
}
|
|
15794
|
+
}
|
|
15795
|
+
/**
|
|
15796
|
+
* Fetch a captured request or response body via network.getData.
|
|
15797
|
+
* Returns a structured result so callers can render an appropriate marker
|
|
15798
|
+
* when the body was never collected, evicted, or the browser lacks support.
|
|
15799
|
+
*/
|
|
15800
|
+
async fetchBody(requestId, dataType) {
|
|
15801
|
+
if (!this.collectorId) {
|
|
15802
|
+
return { ok: false, reason: "unsupported" };
|
|
15803
|
+
}
|
|
15804
|
+
try {
|
|
15805
|
+
const result = await this.bidi.sendCommand("network.getData", {
|
|
15806
|
+
request: requestId,
|
|
15807
|
+
dataType
|
|
15808
|
+
});
|
|
15809
|
+
const bytes = result?.bytes;
|
|
15810
|
+
if (!bytes || typeof bytes.value !== "string") {
|
|
15811
|
+
return { ok: false, reason: "not-collected" };
|
|
15812
|
+
}
|
|
15813
|
+
return {
|
|
15814
|
+
ok: true,
|
|
15815
|
+
type: bytes.type === "base64" ? "base64" : "string",
|
|
15816
|
+
value: bytes.value
|
|
15817
|
+
};
|
|
15818
|
+
} catch (error2) {
|
|
15819
|
+
const message = (error2 instanceof Error ? error2.message : String(error2)).toLowerCase();
|
|
15820
|
+
if (message.includes("no such network data")) {
|
|
15821
|
+
return { ok: false, reason: "not-collected" };
|
|
15822
|
+
}
|
|
15823
|
+
if (message.includes("unavailable network data")) {
|
|
15824
|
+
if (message.includes("evicted")) {
|
|
15825
|
+
return { ok: false, reason: "evicted" };
|
|
15826
|
+
}
|
|
15827
|
+
if (message.includes("aborted")) {
|
|
15828
|
+
return { ok: false, reason: "aborted" };
|
|
15829
|
+
}
|
|
15830
|
+
}
|
|
15831
|
+
return { ok: false, reason: "error" };
|
|
15832
|
+
}
|
|
15833
|
+
}
|
|
15618
15834
|
/**
|
|
15619
15835
|
* Start collecting network requests
|
|
15620
15836
|
*/
|
|
@@ -15768,46 +15984,35 @@ var init_debugging = __esm({
|
|
|
15768
15984
|
init_logger();
|
|
15769
15985
|
MAX_LOGPOINT_RESULTS = 100;
|
|
15770
15986
|
DebuggingEvents = class {
|
|
15771
|
-
constructor(
|
|
15772
|
-
this.
|
|
15773
|
-
this.sendBiDiCommand = sendBiDiCommand;
|
|
15987
|
+
constructor(bidi) {
|
|
15988
|
+
this.bidi = bidi;
|
|
15774
15989
|
}
|
|
15775
15990
|
logpoints = /* @__PURE__ */ new Map();
|
|
15776
15991
|
subscribed = false;
|
|
15777
15992
|
/**
|
|
15778
15993
|
* Subscribe to moz:debugging events
|
|
15779
15994
|
*/
|
|
15780
|
-
async subscribe(
|
|
15995
|
+
async subscribe() {
|
|
15781
15996
|
if (this.subscribed) {
|
|
15782
15997
|
return;
|
|
15783
15998
|
}
|
|
15784
|
-
const bidi = await this.driver.getBidi();
|
|
15785
15999
|
try {
|
|
15786
|
-
await bidi.subscribe("moz:debugging.paused",
|
|
15787
|
-
await bidi.subscribe("moz:debugging.resumed", contextId ? [contextId] : void 0);
|
|
16000
|
+
await this.bidi.subscribe(["moz:debugging.paused", "moz:debugging.resumed"]);
|
|
15788
16001
|
} catch {
|
|
15789
16002
|
logDebug(
|
|
15790
16003
|
"Debugging events subscription skipped (may not be available in this Firefox version)"
|
|
15791
16004
|
);
|
|
15792
16005
|
}
|
|
15793
|
-
|
|
15794
|
-
|
|
15795
|
-
|
|
15796
|
-
|
|
15797
|
-
|
|
15798
|
-
const { context, url, line, column } = payload.params;
|
|
15799
|
-
const logpointId = this.findLogpointByLocation(url, line);
|
|
15800
|
-
if (logpointId) {
|
|
15801
|
-
void this.handleLogpointPause(context, logpointId);
|
|
15802
|
-
return;
|
|
15803
|
-
}
|
|
15804
|
-
logDebug(`moz:Debugging paused in context: ${context} at ${url}:${line}:${column}`);
|
|
15805
|
-
}
|
|
15806
|
-
if (payload?.method === "moz:debugging.resumed") {
|
|
15807
|
-
logDebug(`moz:Debugging resumed in context: ${payload.params.context}`);
|
|
15808
|
-
}
|
|
15809
|
-
} catch {
|
|
16006
|
+
this.bidi.on("moz:debugging.paused", ({ context, url, line, column }) => {
|
|
16007
|
+
const logpointId = this.findLogpointByLocation(url, line);
|
|
16008
|
+
if (logpointId) {
|
|
16009
|
+
void this.handleLogpointPause(context, logpointId);
|
|
16010
|
+
return;
|
|
15810
16011
|
}
|
|
16012
|
+
logDebug(`moz:Debugging paused in context: ${context} at ${url}:${line}:${column}`);
|
|
16013
|
+
});
|
|
16014
|
+
this.bidi.on("moz:debugging.resumed", (entry) => {
|
|
16015
|
+
logDebug(`moz:Debugging resumed in context: ${entry.context}`);
|
|
15811
16016
|
});
|
|
15812
16017
|
this.subscribed = true;
|
|
15813
16018
|
logDebug("moz:debugging listener active");
|
|
@@ -15841,7 +16046,7 @@ var init_debugging = __esm({
|
|
|
15841
16046
|
}
|
|
15842
16047
|
logDebug(`Logpoint hit: ${logpointId} in context ${contextId}`);
|
|
15843
16048
|
try {
|
|
15844
|
-
const result = await this.
|
|
16049
|
+
const result = await this.bidi.sendCommand("script.evaluate", {
|
|
15845
16050
|
expression: entry.expression,
|
|
15846
16051
|
target: { context: contextId },
|
|
15847
16052
|
awaitPromise: false
|
|
@@ -15873,7 +16078,7 @@ var init_debugging = __esm({
|
|
|
15873
16078
|
logDebug(`Logpoint ${logpointId}: result buffer capped at ${MAX_LOGPOINT_RESULTS}`);
|
|
15874
16079
|
}
|
|
15875
16080
|
}
|
|
15876
|
-
await this.
|
|
16081
|
+
await this.bidi.sendCommand("moz:debugging.resume", { context: contextId }).catch((err) => {
|
|
15877
16082
|
logDebug(`Failed to resume after logpoint: ${String(err)}`);
|
|
15878
16083
|
});
|
|
15879
16084
|
}
|
|
@@ -15891,8 +16096,8 @@ var init_downloads = __esm({
|
|
|
15891
16096
|
MAX_DOWNLOADS = 500;
|
|
15892
16097
|
DOWNLOAD_TTL_MS = 30 * 60 * 1e3;
|
|
15893
16098
|
DownloadEvents = class {
|
|
15894
|
-
constructor(
|
|
15895
|
-
this.
|
|
16099
|
+
constructor(bidi) {
|
|
16100
|
+
this.bidi = bidi;
|
|
15896
16101
|
}
|
|
15897
16102
|
downloads = /* @__PURE__ */ new Map();
|
|
15898
16103
|
subscribed = false;
|
|
@@ -15904,65 +16109,53 @@ var init_downloads = __esm({
|
|
|
15904
16109
|
/**
|
|
15905
16110
|
* Subscribe to BiDi download events.
|
|
15906
16111
|
*/
|
|
15907
|
-
async subscribe(
|
|
16112
|
+
async subscribe() {
|
|
15908
16113
|
if (this.subscribed) {
|
|
15909
16114
|
return;
|
|
15910
16115
|
}
|
|
15911
|
-
|
|
15912
|
-
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
|
|
15930
|
-
|
|
15931
|
-
|
|
15932
|
-
|
|
15933
|
-
|
|
15934
|
-
|
|
15935
|
-
|
|
15936
|
-
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
suggestedFilename: "",
|
|
15950
|
-
status: "in_progress",
|
|
15951
|
-
startTimestamp: p.timestamp ?? Date.now()
|
|
15952
|
-
};
|
|
15953
|
-
existing.status = p.status;
|
|
15954
|
-
existing.endTimestamp = p.timestamp ?? Date.now();
|
|
15955
|
-
existing.durationMs = existing.endTimestamp - existing.startTimestamp;
|
|
15956
|
-
if (p.status === "complete" && p.filepath) {
|
|
15957
|
-
existing.filepath = p.filepath;
|
|
15958
|
-
}
|
|
15959
|
-
this.downloads.set(key, existing);
|
|
15960
|
-
logDebug(
|
|
15961
|
-
`Download ${p.status}: filepath=${existing.filepath}, url=${existing.url}, id=${key}`
|
|
15962
|
-
);
|
|
15963
|
-
}
|
|
15964
|
-
} catch {
|
|
16116
|
+
await this.bidi.subscribe(["browsingContext.downloadWillBegin", "browsingContext.downloadEnd"]);
|
|
16117
|
+
this.bidi.on("browsingContext.downloadWillBegin", (p) => {
|
|
16118
|
+
let key = p.download ?? p.navigation;
|
|
16119
|
+
if (!key) {
|
|
16120
|
+
key = `download-${this.fallbackCounter++}`;
|
|
16121
|
+
this.pendingFallbackKey = key;
|
|
16122
|
+
}
|
|
16123
|
+
this.downloads.set(key, {
|
|
16124
|
+
id: key,
|
|
16125
|
+
context: p.context,
|
|
16126
|
+
navigation: p.navigation ?? null,
|
|
16127
|
+
url: p.url || "",
|
|
16128
|
+
suggestedFilename: p.suggestedFilename || "",
|
|
16129
|
+
status: "in_progress",
|
|
16130
|
+
startTimestamp: p.timestamp ?? Date.now()
|
|
16131
|
+
});
|
|
16132
|
+
logDebug(`Download started: filename=${p.suggestedFilename}, url=${p.url}, id=${key}`);
|
|
16133
|
+
});
|
|
16134
|
+
this.bidi.on("browsingContext.downloadEnd", (p) => {
|
|
16135
|
+
let key = p?.download ?? p?.navigation;
|
|
16136
|
+
if (!key) {
|
|
16137
|
+
key = this.pendingFallbackKey ?? `download-${this.fallbackCounter++}`;
|
|
16138
|
+
this.pendingFallbackKey = null;
|
|
16139
|
+
}
|
|
16140
|
+
const existing = this.downloads.get(key) ?? {
|
|
16141
|
+
id: key,
|
|
16142
|
+
context: p.context,
|
|
16143
|
+
navigation: p.navigation ?? null,
|
|
16144
|
+
url: p.url || "",
|
|
16145
|
+
suggestedFilename: "",
|
|
16146
|
+
status: "in_progress",
|
|
16147
|
+
startTimestamp: p.timestamp ?? Date.now()
|
|
16148
|
+
};
|
|
16149
|
+
existing.status = p.status;
|
|
16150
|
+
existing.endTimestamp = p.timestamp ?? Date.now();
|
|
16151
|
+
existing.durationMs = existing.endTimestamp - existing.startTimestamp;
|
|
16152
|
+
if (p.status === "complete" && p.filepath) {
|
|
16153
|
+
existing.filepath = p.filepath;
|
|
15965
16154
|
}
|
|
16155
|
+
this.downloads.set(key, existing);
|
|
16156
|
+
logDebug(
|
|
16157
|
+
`Download ${p.status}: filepath=${existing.filepath}, url=${existing.url}, id=${key}`
|
|
16158
|
+
);
|
|
15966
16159
|
});
|
|
15967
16160
|
this.subscribed = true;
|
|
15968
16161
|
logDebug("Download listener ready");
|
|
@@ -16033,19 +16226,6 @@ var init_dom = __esm({
|
|
|
16033
16226
|
this.driver = driver;
|
|
16034
16227
|
this.resolveUid = resolveUid;
|
|
16035
16228
|
}
|
|
16036
|
-
/**
|
|
16037
|
-
* Evaluate JavaScript - direct passthrough to executeScript
|
|
16038
|
-
*/
|
|
16039
|
-
async evaluate(script) {
|
|
16040
|
-
return await this.driver.executeScript(script);
|
|
16041
|
-
}
|
|
16042
|
-
/**
|
|
16043
|
-
* Get page HTML content
|
|
16044
|
-
*/
|
|
16045
|
-
async getContent() {
|
|
16046
|
-
const html = await this.evaluate("return document.documentElement.outerHTML");
|
|
16047
|
-
return String(html);
|
|
16048
|
-
}
|
|
16049
16229
|
// ============================================================================
|
|
16050
16230
|
// Element polling helpers
|
|
16051
16231
|
// ============================================================================
|
|
@@ -16509,7 +16689,12 @@ __export(formatter_exports, {
|
|
|
16509
16689
|
formatSnapshotTree: () => formatSnapshotTree
|
|
16510
16690
|
});
|
|
16511
16691
|
function formatSnapshotTree(node, depth = 0, options = {}) {
|
|
16512
|
-
const {
|
|
16692
|
+
const {
|
|
16693
|
+
includeAttributes = true,
|
|
16694
|
+
includeText = true,
|
|
16695
|
+
maxDepth,
|
|
16696
|
+
maxAttrLength = MAX_ATTR_LENGTH
|
|
16697
|
+
} = options;
|
|
16513
16698
|
if (maxDepth !== void 0 && depth >= maxDepth) {
|
|
16514
16699
|
return "";
|
|
16515
16700
|
}
|
|
@@ -16519,22 +16704,22 @@ function formatSnapshotTree(node, depth = 0, options = {}) {
|
|
|
16519
16704
|
const role = node.role || node.tag;
|
|
16520
16705
|
attrs.push(role);
|
|
16521
16706
|
if (node.name) {
|
|
16522
|
-
attrs.push(`"${truncate(node.name,
|
|
16707
|
+
attrs.push(`"${truncate(node.name, maxAttrLength)}"`);
|
|
16523
16708
|
}
|
|
16524
16709
|
if (node.role && node.role !== node.tag) {
|
|
16525
16710
|
attrs.push(`tag=${node.tag}`);
|
|
16526
16711
|
}
|
|
16527
16712
|
if (node.value) {
|
|
16528
|
-
attrs.push(`value="${truncate(node.value,
|
|
16713
|
+
attrs.push(`value="${truncate(node.value, maxAttrLength)}"`);
|
|
16529
16714
|
}
|
|
16530
16715
|
if (node.href) {
|
|
16531
|
-
attrs.push(`href="${truncate(node.href,
|
|
16716
|
+
attrs.push(`href="${truncate(node.href, maxAttrLength)}"`);
|
|
16532
16717
|
}
|
|
16533
16718
|
if (node.src) {
|
|
16534
|
-
attrs.push(`src="${truncate(node.src,
|
|
16719
|
+
attrs.push(`src="${truncate(node.src, maxAttrLength)}"`);
|
|
16535
16720
|
}
|
|
16536
16721
|
if (includeText && node.text) {
|
|
16537
|
-
attrs.push(`text="${truncate(node.text,
|
|
16722
|
+
attrs.push(`text="${truncate(node.text, maxAttrLength)}"`);
|
|
16538
16723
|
}
|
|
16539
16724
|
if (includeAttributes && node.aria) {
|
|
16540
16725
|
if (node.aria.disabled) {
|
|
@@ -16593,7 +16778,7 @@ function formatSnapshotTree(node, depth = 0, options = {}) {
|
|
|
16593
16778
|
if (node.isIframe) {
|
|
16594
16779
|
attrs.push("[iframe");
|
|
16595
16780
|
if (node.frameSrc) {
|
|
16596
|
-
attrs.push(`src="${truncate(node.frameSrc,
|
|
16781
|
+
attrs.push(`src="${truncate(node.frameSrc, maxAttrLength)}"`);
|
|
16597
16782
|
}
|
|
16598
16783
|
if (node.crossOrigin) {
|
|
16599
16784
|
attrs.push("cross-origin");
|
|
@@ -16621,140 +16806,60 @@ var init_formatter = __esm({
|
|
|
16621
16806
|
});
|
|
16622
16807
|
|
|
16623
16808
|
// src/firefox/snapshot/resolver.ts
|
|
16624
|
-
|
|
16625
|
-
|
|
16809
|
+
function notFoundMessage(uid) {
|
|
16810
|
+
return `UID not found: ${uid}. The element is gone from the page, or the page was reloaded. Take a fresh snapshot first.`;
|
|
16811
|
+
}
|
|
16812
|
+
var RESOLVE_SCRIPT, SELECTOR_SCRIPT, CLEAR_SCRIPT, UidResolver;
|
|
16626
16813
|
var init_resolver = __esm({
|
|
16627
16814
|
"src/firefox/snapshot/resolver.ts"() {
|
|
16628
16815
|
"use strict";
|
|
16629
16816
|
init_logger();
|
|
16817
|
+
RESOLVE_SCRIPT = "return window.__resolveUid ? window.__resolveUid(arguments[0]) : null;";
|
|
16818
|
+
SELECTOR_SCRIPT = "return window.__uidToSelector ? window.__uidToSelector(arguments[0]) : null;";
|
|
16819
|
+
CLEAR_SCRIPT = "if (window.__clearUidRegistry) { window.__clearUidRegistry(); }";
|
|
16630
16820
|
UidResolver = class {
|
|
16631
16821
|
constructor(driver) {
|
|
16632
16822
|
this.driver = driver;
|
|
16633
16823
|
}
|
|
16634
|
-
uidToEntry = /* @__PURE__ */ new Map();
|
|
16635
|
-
elementCache = /* @__PURE__ */ new Map();
|
|
16636
|
-
currentSnapshotId = 0;
|
|
16637
|
-
/**
|
|
16638
|
-
* Update current snapshot ID
|
|
16639
|
-
*/
|
|
16640
|
-
setSnapshotId(snapshotId) {
|
|
16641
|
-
this.currentSnapshotId = snapshotId;
|
|
16642
|
-
}
|
|
16643
16824
|
/**
|
|
16644
|
-
*
|
|
16825
|
+
* Forget all UID associations in the page, making existing UIDs unresolvable.
|
|
16826
|
+
* Best effort: the registry dies with the page anyway.
|
|
16645
16827
|
*/
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
storeUidMappings(uidMap) {
|
|
16653
|
-
this.uidToEntry.clear();
|
|
16654
|
-
for (const entry of uidMap) {
|
|
16655
|
-
this.uidToEntry.set(entry.uid, entry);
|
|
16656
|
-
}
|
|
16657
|
-
}
|
|
16658
|
-
/**
|
|
16659
|
-
* Clear all UID mappings and cache
|
|
16660
|
-
*/
|
|
16661
|
-
clear() {
|
|
16662
|
-
this.uidToEntry.clear();
|
|
16663
|
-
this.elementCache.clear();
|
|
16664
|
-
logDebug("Snapshot UIDs cleared");
|
|
16665
|
-
}
|
|
16666
|
-
/**
|
|
16667
|
-
* Validate UID (staleness check)
|
|
16668
|
-
*/
|
|
16669
|
-
validateUid(uid) {
|
|
16670
|
-
const parts = uid.split("_");
|
|
16671
|
-
if (parts.length < 2 || !parts[0]) {
|
|
16672
|
-
throw new Error(`Invalid UID format: ${uid}`);
|
|
16673
|
-
}
|
|
16674
|
-
const uidSnapshotId = parseInt(parts[0], 10);
|
|
16675
|
-
if (isNaN(uidSnapshotId)) {
|
|
16676
|
-
throw new Error(`Invalid UID format: ${uid}`);
|
|
16677
|
-
}
|
|
16678
|
-
if (uidSnapshotId !== this.currentSnapshotId) {
|
|
16679
|
-
throw new Error(
|
|
16680
|
-
`This uid is from a stale snapshot (snapshot ${uidSnapshotId}, current ${this.currentSnapshotId}). Take a fresh snapshot.`
|
|
16681
|
-
);
|
|
16828
|
+
async clear() {
|
|
16829
|
+
try {
|
|
16830
|
+
await this.driver.executeScript(CLEAR_SCRIPT);
|
|
16831
|
+
logDebug("Snapshot UIDs cleared");
|
|
16832
|
+
} catch {
|
|
16833
|
+
logDebug("Unable to clear snapshot UIDs (page may be navigating)");
|
|
16682
16834
|
}
|
|
16683
16835
|
}
|
|
16684
16836
|
/**
|
|
16685
|
-
* Resolve UID to CSS selector
|
|
16837
|
+
* Resolve UID to a CSS selector, generated on demand from the element it points at
|
|
16686
16838
|
*/
|
|
16687
|
-
resolveUidToSelector(uid) {
|
|
16688
|
-
this.
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
throw new Error(`UID not found: ${uid}. Take a fresh snapshot first.`);
|
|
16839
|
+
async resolveUidToSelector(uid) {
|
|
16840
|
+
const selector = await this.driver.executeScript(SELECTOR_SCRIPT, uid);
|
|
16841
|
+
if (!selector) {
|
|
16842
|
+
throw new Error(notFoundMessage(uid));
|
|
16692
16843
|
}
|
|
16693
|
-
return
|
|
16844
|
+
return selector;
|
|
16694
16845
|
}
|
|
16695
16846
|
/**
|
|
16696
|
-
* Resolve UID to element
|
|
16697
|
-
* Tries CSS first, falls back to XPath
|
|
16847
|
+
* Resolve UID to the element it was assigned to during the snapshot
|
|
16698
16848
|
*/
|
|
16699
16849
|
async resolveUidToElement(uid) {
|
|
16700
|
-
this.
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
throw new Error(`UID not found: ${uid}. Take a fresh snapshot first.`);
|
|
16704
|
-
}
|
|
16705
|
-
const cached2 = this.elementCache.get(uid);
|
|
16706
|
-
if (cached2?.cachedElement) {
|
|
16707
|
-
try {
|
|
16708
|
-
await cached2.cachedElement.isDisplayed();
|
|
16709
|
-
logDebug(`Using cached element for UID: ${uid}`);
|
|
16710
|
-
return cached2.cachedElement;
|
|
16711
|
-
} catch {
|
|
16712
|
-
logDebug(`Cached element stale for UID: ${uid}, re-finding...`);
|
|
16713
|
-
}
|
|
16714
|
-
}
|
|
16715
|
-
try {
|
|
16716
|
-
const element = await this.driver.findElement(By2.css(entry.css));
|
|
16717
|
-
this.elementCache.set(uid, {
|
|
16718
|
-
selector: entry.css,
|
|
16719
|
-
...entry.xpath && { xpath: entry.xpath },
|
|
16720
|
-
cachedElement: element,
|
|
16721
|
-
snapshotId: this.currentSnapshotId,
|
|
16722
|
-
timestamp: Date.now()
|
|
16723
|
-
});
|
|
16724
|
-
logDebug(`Found element by CSS for UID: ${uid}`);
|
|
16725
|
-
return element;
|
|
16726
|
-
} catch {
|
|
16727
|
-
logDebug(`CSS selector failed for UID: ${uid}, trying XPath fallback...`);
|
|
16728
|
-
const xpathSelector = entry.xpath;
|
|
16729
|
-
if (xpathSelector) {
|
|
16730
|
-
try {
|
|
16731
|
-
const element = await this.driver.findElement(By2.xpath(xpathSelector));
|
|
16732
|
-
this.elementCache.set(uid, {
|
|
16733
|
-
selector: entry.css,
|
|
16734
|
-
...xpathSelector && { xpath: xpathSelector },
|
|
16735
|
-
cachedElement: element,
|
|
16736
|
-
snapshotId: this.currentSnapshotId,
|
|
16737
|
-
timestamp: Date.now()
|
|
16738
|
-
});
|
|
16739
|
-
logDebug(`Found element by XPath for UID: ${uid}`);
|
|
16740
|
-
return element;
|
|
16741
|
-
} catch {
|
|
16742
|
-
throw new Error(
|
|
16743
|
-
`Element not found for UID: ${uid}. The element may have changed. Take a fresh snapshot.`
|
|
16744
|
-
);
|
|
16745
|
-
}
|
|
16746
|
-
}
|
|
16747
|
-
throw new Error(
|
|
16748
|
-
`Element not found for UID: ${uid}. The element may have changed. Take a fresh snapshot.`
|
|
16749
|
-
);
|
|
16850
|
+
const element = await this.driver.executeScript(RESOLVE_SCRIPT, uid);
|
|
16851
|
+
if (!element) {
|
|
16852
|
+
throw new Error(notFoundMessage(uid));
|
|
16750
16853
|
}
|
|
16854
|
+
logDebug(`Resolved element for UID: ${uid}`);
|
|
16855
|
+
return element;
|
|
16751
16856
|
}
|
|
16752
16857
|
};
|
|
16753
16858
|
}
|
|
16754
16859
|
});
|
|
16755
16860
|
|
|
16756
16861
|
// src/firefox/snapshot/manager.ts
|
|
16757
|
-
import { readFileSync } from "fs";
|
|
16862
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
16758
16863
|
import { dirname, resolve } from "path";
|
|
16759
16864
|
import { fileURLToPath } from "url";
|
|
16760
16865
|
var SnapshotManager;
|
|
@@ -16768,7 +16873,8 @@ var init_manager = __esm({
|
|
|
16768
16873
|
driver;
|
|
16769
16874
|
resolver;
|
|
16770
16875
|
injectedScript = null;
|
|
16771
|
-
|
|
16876
|
+
/** Counter handed to the injected script so UIDs stay unique across snapshots */
|
|
16877
|
+
nextElementId = 0;
|
|
16772
16878
|
constructor(driver) {
|
|
16773
16879
|
this.driver = driver;
|
|
16774
16880
|
this.resolver = new UidResolver(driver);
|
|
@@ -16796,7 +16902,7 @@ var init_manager = __esm({
|
|
|
16796
16902
|
for (const path of possiblePaths) {
|
|
16797
16903
|
attemptedPaths.push(path);
|
|
16798
16904
|
try {
|
|
16799
|
-
this.injectedScript =
|
|
16905
|
+
this.injectedScript = readFileSync2(path, "utf-8");
|
|
16800
16906
|
const sizeKB = (this.injectedScript.length / 1024).toFixed(1);
|
|
16801
16907
|
logDebug(`\u2713 Loaded snapshot bundle: ${path.split("/").pop()} (${sizeKB} KB)`);
|
|
16802
16908
|
return this.injectedScript;
|
|
@@ -16815,14 +16921,25 @@ ${attemptedPaths.map((p) => ` - ${p}`).join("\n")}`
|
|
|
16815
16921
|
}
|
|
16816
16922
|
/**
|
|
16817
16923
|
* Take a snapshot of the current page
|
|
16818
|
-
* Returns text and JSON
|
|
16924
|
+
* Returns text and JSON, no DOM mutations
|
|
16819
16925
|
*/
|
|
16820
16926
|
async takeSnapshot(options) {
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16927
|
+
if (options?.selector || options?.includeAll) {
|
|
16928
|
+
const optionsOutput = [];
|
|
16929
|
+
if (options.selector) {
|
|
16930
|
+
optionsOutput.push(`selector: ${options.selector}`);
|
|
16931
|
+
}
|
|
16932
|
+
if (options.includeAll) {
|
|
16933
|
+
optionsOutput.push("include all");
|
|
16934
|
+
}
|
|
16935
|
+
logDebug(`Taking snapshot (${optionsOutput.join(", ")})...`);
|
|
16936
|
+
} else {
|
|
16937
|
+
logDebug("Taking snapshot...");
|
|
16938
|
+
}
|
|
16939
|
+
const result = await this.executeInjectedScript(this.nextElementId, options);
|
|
16940
|
+
if (typeof result?.nextElementId === "number") {
|
|
16941
|
+
this.nextElementId = result.nextElementId;
|
|
16942
|
+
}
|
|
16826
16943
|
logDebug(
|
|
16827
16944
|
`Snapshot executeScript result: hasResult=${!!result}, hasTree=${!!result?.tree}, truncated=${result?.truncated || false}`
|
|
16828
16945
|
);
|
|
@@ -16844,60 +16961,60 @@ ${attemptedPaths.map((p) => ` - ${p}`).join("\n")}`
|
|
|
16844
16961
|
logDebug(`Snapshot generation failed: ${errorMsg}`);
|
|
16845
16962
|
throw new Error(`Failed to generate snapshot: ${errorMsg}`);
|
|
16846
16963
|
}
|
|
16847
|
-
this.resolver.storeUidMappings(result.uidMap);
|
|
16848
16964
|
const snapshotJson = {
|
|
16849
16965
|
root: result.tree,
|
|
16850
|
-
snapshotId,
|
|
16851
16966
|
timestamp: Date.now(),
|
|
16852
|
-
truncated: result.truncated || false
|
|
16853
|
-
uidMap: result.uidMap
|
|
16967
|
+
truncated: result.truncated || false
|
|
16854
16968
|
};
|
|
16855
16969
|
const snapshot = {
|
|
16856
16970
|
text: formatSnapshotTree(result.tree),
|
|
16857
16971
|
json: snapshotJson
|
|
16858
16972
|
};
|
|
16859
16973
|
logDebug(
|
|
16860
|
-
`Snapshot created: ${result.
|
|
16974
|
+
`Snapshot created: ${result.nodeCount} elements with UIDs${result.truncated ? " (truncated)" : ""}`
|
|
16861
16975
|
);
|
|
16862
16976
|
return snapshot;
|
|
16863
16977
|
}
|
|
16864
16978
|
/**
|
|
16865
|
-
* Resolve UID to CSS selector
|
|
16979
|
+
* Resolve UID to a CSS selector generated on demand
|
|
16866
16980
|
*/
|
|
16867
|
-
resolveUidToSelector(uid) {
|
|
16868
|
-
return this.resolver.resolveUidToSelector(uid);
|
|
16981
|
+
async resolveUidToSelector(uid) {
|
|
16982
|
+
return await this.resolver.resolveUidToSelector(uid);
|
|
16869
16983
|
}
|
|
16870
16984
|
/**
|
|
16871
|
-
* Resolve UID to WebElement
|
|
16985
|
+
* Resolve UID to the WebElement it was assigned to
|
|
16872
16986
|
*/
|
|
16873
16987
|
async resolveUidToElement(uid) {
|
|
16874
16988
|
return await this.resolver.resolveUidToElement(uid);
|
|
16875
16989
|
}
|
|
16876
16990
|
/**
|
|
16877
|
-
* Clear snapshot
|
|
16991
|
+
* Clear snapshot UIDs
|
|
16878
16992
|
*/
|
|
16879
|
-
clear() {
|
|
16880
|
-
this.resolver.clear();
|
|
16993
|
+
async clear() {
|
|
16994
|
+
await this.resolver.clear();
|
|
16881
16995
|
}
|
|
16882
16996
|
/**
|
|
16883
16997
|
* Execute bundled injected snapshot script
|
|
16884
16998
|
*/
|
|
16885
|
-
async executeInjectedScript(
|
|
16999
|
+
async executeInjectedScript(nextElementId, options) {
|
|
16886
17000
|
const scriptSource = this.getInjectedScript();
|
|
16887
17001
|
const result = await this.driver.executeScript(
|
|
16888
17002
|
`
|
|
16889
17003
|
// Only inject the bundle if not already present
|
|
16890
17004
|
if (typeof window.__createSnapshot === 'undefined') {
|
|
16891
17005
|
${scriptSource}
|
|
16892
|
-
// Register the
|
|
17006
|
+
// Register the snapshot and UID resolution functions globally
|
|
16893
17007
|
if (typeof __SnapshotInjected !== 'undefined' && __SnapshotInjected.createSnapshot) {
|
|
16894
17008
|
window.__createSnapshot = __SnapshotInjected.createSnapshot;
|
|
17009
|
+
window.__resolveUid = __SnapshotInjected.resolveUid;
|
|
17010
|
+
window.__uidToSelector = __SnapshotInjected.uidToSelector;
|
|
17011
|
+
window.__clearUidRegistry = __SnapshotInjected.clearUidRegistry;
|
|
16895
17012
|
}
|
|
16896
17013
|
}
|
|
16897
17014
|
// Call it with options
|
|
16898
17015
|
return window.__createSnapshot(arguments[0], arguments[1]);
|
|
16899
17016
|
`,
|
|
16900
|
-
|
|
17017
|
+
nextElementId,
|
|
16901
17018
|
options || {}
|
|
16902
17019
|
);
|
|
16903
17020
|
return result;
|
|
@@ -16921,13 +17038,16 @@ var init_firefox = __esm({
|
|
|
16921
17038
|
"src/firefox/index.ts"() {
|
|
16922
17039
|
"use strict";
|
|
16923
17040
|
init_core3();
|
|
17041
|
+
init_bidi();
|
|
16924
17042
|
init_logger();
|
|
17043
|
+
init_remote_value();
|
|
16925
17044
|
init_events();
|
|
16926
17045
|
init_dom();
|
|
16927
17046
|
init_pages();
|
|
16928
17047
|
init_snapshot();
|
|
16929
17048
|
FirefoxClient = class {
|
|
16930
17049
|
core;
|
|
17050
|
+
bidi = null;
|
|
16931
17051
|
consoleEvents = null;
|
|
16932
17052
|
networkEvents = null;
|
|
16933
17053
|
debuggingEvents = null;
|
|
@@ -16938,33 +17058,52 @@ var init_firefox = __esm({
|
|
|
16938
17058
|
constructor(options) {
|
|
16939
17059
|
this.core = new FirefoxCore(options);
|
|
16940
17060
|
}
|
|
17061
|
+
getBidi() {
|
|
17062
|
+
if (!this.bidi) {
|
|
17063
|
+
throw new Error("Not connected");
|
|
17064
|
+
}
|
|
17065
|
+
return this.bidi;
|
|
17066
|
+
}
|
|
16941
17067
|
/**
|
|
16942
17068
|
* Connect and initialize all modules
|
|
16943
17069
|
*/
|
|
16944
17070
|
async connect() {
|
|
16945
17071
|
await this.core.connect();
|
|
16946
17072
|
const driver = this.core.getDriver();
|
|
17073
|
+
this.bidi = new BiDiFacade(driver);
|
|
16947
17074
|
this.snapshot = new SnapshotManager(driver);
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
16951
|
-
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
this.consoleEvents =
|
|
16956
|
-
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
this.
|
|
16960
|
-
|
|
16961
|
-
|
|
16962
|
-
|
|
16963
|
-
|
|
16964
|
-
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
17075
|
+
this.consoleEvents = new ConsoleEvents(this.bidi, {
|
|
17076
|
+
autoClearOnNavigate: false
|
|
17077
|
+
});
|
|
17078
|
+
try {
|
|
17079
|
+
await this.consoleEvents.subscribe();
|
|
17080
|
+
} catch {
|
|
17081
|
+
logDebug("Unable to subscribe to console events");
|
|
17082
|
+
this.consoleEvents = null;
|
|
17083
|
+
}
|
|
17084
|
+
this.networkEvents = new NetworkEvents(this.bidi, {
|
|
17085
|
+
autoClearOnNavigate: false,
|
|
17086
|
+
captureBodies: this.core.getOptions().captureNetworkBodies !== false
|
|
17087
|
+
});
|
|
17088
|
+
try {
|
|
17089
|
+
await this.networkEvents.subscribe();
|
|
17090
|
+
} catch {
|
|
17091
|
+
logDebug("Unable to subscribe to network events");
|
|
17092
|
+
this.networkEvents = null;
|
|
17093
|
+
}
|
|
17094
|
+
this.debuggingEvents = new DebuggingEvents(this.bidi);
|
|
17095
|
+
try {
|
|
17096
|
+
await this.debuggingEvents.subscribe();
|
|
17097
|
+
} catch {
|
|
17098
|
+
logDebug("Unable to subscribe to debugging events");
|
|
17099
|
+
this.debuggingEvents = null;
|
|
17100
|
+
}
|
|
17101
|
+
this.downloadEvents = new DownloadEvents(this.bidi);
|
|
17102
|
+
try {
|
|
17103
|
+
await this.downloadEvents.subscribe();
|
|
17104
|
+
} catch {
|
|
17105
|
+
logDebug("Unable to subscribe to download events");
|
|
17106
|
+
this.downloadEvents = null;
|
|
16968
17107
|
}
|
|
16969
17108
|
this.dom = new DomInteractions(
|
|
16970
17109
|
driver,
|
|
@@ -16974,55 +17113,30 @@ var init_firefox = __esm({
|
|
|
16974
17113
|
driver,
|
|
16975
17114
|
() => this.core.getCurrentContextId(),
|
|
16976
17115
|
(id) => this.core.setCurrentContextId(id),
|
|
16977
|
-
(method, params) => this.
|
|
17116
|
+
(method, params) => this.getBidi().sendCommand(method, params)
|
|
16978
17117
|
);
|
|
16979
|
-
if (this.consoleEvents) {
|
|
16980
|
-
try {
|
|
16981
|
-
await this.consoleEvents.subscribe(void 0);
|
|
16982
|
-
} catch {
|
|
16983
|
-
logDebug("Unable to subscribe to console events");
|
|
16984
|
-
this.consoleEvents = null;
|
|
16985
|
-
}
|
|
16986
|
-
}
|
|
16987
|
-
if (this.networkEvents) {
|
|
16988
|
-
try {
|
|
16989
|
-
await this.networkEvents.subscribe(void 0);
|
|
16990
|
-
} catch {
|
|
16991
|
-
logDebug("Unable to subscribe to network events");
|
|
16992
|
-
this.networkEvents = null;
|
|
16993
|
-
}
|
|
16994
|
-
}
|
|
16995
|
-
if (this.debuggingEvents) {
|
|
16996
|
-
try {
|
|
16997
|
-
await this.debuggingEvents.subscribe();
|
|
16998
|
-
} catch {
|
|
16999
|
-
logDebug("Unable to subscribe to debugging events");
|
|
17000
|
-
this.debuggingEvents = null;
|
|
17001
|
-
}
|
|
17002
|
-
}
|
|
17003
|
-
if (this.downloadEvents) {
|
|
17004
|
-
try {
|
|
17005
|
-
await this.downloadEvents.subscribe(void 0);
|
|
17006
|
-
} catch {
|
|
17007
|
-
logDebug("Unable to subscribe to download events");
|
|
17008
|
-
this.downloadEvents = null;
|
|
17009
|
-
}
|
|
17010
|
-
}
|
|
17011
17118
|
}
|
|
17012
17119
|
// ============================================================================
|
|
17013
17120
|
// DOM / Evaluate
|
|
17014
17121
|
// ============================================================================
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
async
|
|
17022
|
-
|
|
17023
|
-
|
|
17122
|
+
/**
|
|
17123
|
+
* Evaluate a JavaScript expression in the current browsing context over
|
|
17124
|
+
* WebDriver BiDi (script.evaluate), so reads target the BiDi-tracked context
|
|
17125
|
+
* rather than Selenium's classic window handle. Returns the result as a
|
|
17126
|
+
* native value; throws on a script exception.
|
|
17127
|
+
*/
|
|
17128
|
+
async evaluate(expression) {
|
|
17129
|
+
const result = await this.getBidi().sendCommand("script.evaluate", {
|
|
17130
|
+
expression,
|
|
17131
|
+
awaitPromise: true,
|
|
17132
|
+
target: { context: this.core.getCurrentContextId() }
|
|
17133
|
+
});
|
|
17134
|
+
if (result.type === "success") {
|
|
17135
|
+
return remoteValueToNative(result.result);
|
|
17024
17136
|
}
|
|
17025
|
-
|
|
17137
|
+
throw new Error(
|
|
17138
|
+
`Script evaluation failed: ${result.exceptionDetails?.text ?? "unknown error"}`
|
|
17139
|
+
);
|
|
17026
17140
|
}
|
|
17027
17141
|
async clickBySelector(selector) {
|
|
17028
17142
|
if (!this.dom) {
|
|
@@ -17118,7 +17232,6 @@ var init_firefox = __esm({
|
|
|
17118
17232
|
throw new Error("Not connected");
|
|
17119
17233
|
}
|
|
17120
17234
|
await this.pages.navigate(url);
|
|
17121
|
-
this.clearSnapshot();
|
|
17122
17235
|
}
|
|
17123
17236
|
async navigateBack() {
|
|
17124
17237
|
if (!this.pages) {
|
|
@@ -17221,6 +17334,18 @@ var init_firefox = __esm({
|
|
|
17221
17334
|
}
|
|
17222
17335
|
this.networkEvents.clearRequests();
|
|
17223
17336
|
}
|
|
17337
|
+
/**
|
|
17338
|
+
* Fetch a captured request or response body for a given request id.
|
|
17339
|
+
* Returns a structured result describing the body or why it is unavailable.
|
|
17340
|
+
*/
|
|
17341
|
+
async getNetworkRequestBody(requestId, dataType) {
|
|
17342
|
+
if (!this.networkEvents) {
|
|
17343
|
+
throw new Error(
|
|
17344
|
+
"Network events not available (Firefox Remote Agent not running \u2014 start Firefox with --remote-debugging-port to enable BiDi)"
|
|
17345
|
+
);
|
|
17346
|
+
}
|
|
17347
|
+
return this.networkEvents.fetchBody(requestId, dataType);
|
|
17348
|
+
}
|
|
17224
17349
|
// ============================================================================
|
|
17225
17350
|
// Downloads
|
|
17226
17351
|
// ============================================================================
|
|
@@ -17246,7 +17371,7 @@ var init_firefox = __esm({
|
|
|
17246
17371
|
*/
|
|
17247
17372
|
async setDownloadBehavior(behavior) {
|
|
17248
17373
|
const downloadBehavior = behavior === "default" ? null : behavior === "allowed" ? { type: "allowed" } : { type: "denied" };
|
|
17249
|
-
await this.
|
|
17374
|
+
await this.getBidi().sendCommand("browser.setDownloadBehavior", { downloadBehavior });
|
|
17250
17375
|
}
|
|
17251
17376
|
// ============================================================================
|
|
17252
17377
|
// Snapshot
|
|
@@ -17257,11 +17382,11 @@ var init_firefox = __esm({
|
|
|
17257
17382
|
}
|
|
17258
17383
|
return await this.snapshot.takeSnapshot(options);
|
|
17259
17384
|
}
|
|
17260
|
-
resolveUidToSelector(uid) {
|
|
17385
|
+
async resolveUidToSelector(uid) {
|
|
17261
17386
|
if (!this.snapshot) {
|
|
17262
17387
|
throw new Error("Not connected");
|
|
17263
17388
|
}
|
|
17264
|
-
return this.snapshot.resolveUidToSelector(uid);
|
|
17389
|
+
return await this.snapshot.resolveUidToSelector(uid);
|
|
17265
17390
|
}
|
|
17266
17391
|
async resolveUidToElement(uid) {
|
|
17267
17392
|
if (!this.snapshot) {
|
|
@@ -17269,11 +17394,11 @@ var init_firefox = __esm({
|
|
|
17269
17394
|
}
|
|
17270
17395
|
return await this.snapshot.resolveUidToElement(uid);
|
|
17271
17396
|
}
|
|
17272
|
-
clearSnapshot() {
|
|
17397
|
+
async clearSnapshot() {
|
|
17273
17398
|
if (!this.snapshot) {
|
|
17274
17399
|
throw new Error("Not connected");
|
|
17275
17400
|
}
|
|
17276
|
-
this.snapshot.clear();
|
|
17401
|
+
await this.snapshot.clear();
|
|
17277
17402
|
}
|
|
17278
17403
|
// ============================================================================
|
|
17279
17404
|
// Screenshot
|
|
@@ -17298,7 +17423,7 @@ var init_firefox = __esm({
|
|
|
17298
17423
|
* @internal
|
|
17299
17424
|
*/
|
|
17300
17425
|
async sendBiDiCommand(method, params = {}) {
|
|
17301
|
-
return await this.
|
|
17426
|
+
return await this.getBidi().sendCommand(method, params);
|
|
17302
17427
|
}
|
|
17303
17428
|
/**
|
|
17304
17429
|
* Get WebDriver instance (for advanced operations)
|
|
@@ -17343,7 +17468,7 @@ var init_firefox = __esm({
|
|
|
17343
17468
|
if (!this.debuggingEvents) {
|
|
17344
17469
|
throw new Error("Debugging events not available");
|
|
17345
17470
|
}
|
|
17346
|
-
const result = await this.
|
|
17471
|
+
const result = await this.getBidi().sendCommand("moz:debugging.setBreakpoint", {
|
|
17347
17472
|
location: { url, line }
|
|
17348
17473
|
});
|
|
17349
17474
|
const logpointId = result.breakpoint;
|
|
@@ -17357,7 +17482,7 @@ var init_firefox = __esm({
|
|
|
17357
17482
|
if (!this.debuggingEvents) {
|
|
17358
17483
|
throw new Error("Debugging events not available");
|
|
17359
17484
|
}
|
|
17360
|
-
await this.
|
|
17485
|
+
await this.getBidi().sendCommand("moz:debugging.removeBreakpoint", {
|
|
17361
17486
|
breakpoint: logpointId
|
|
17362
17487
|
});
|
|
17363
17488
|
this.debuggingEvents.removeLogpoint(logpointId);
|
|
@@ -17411,11 +17536,56 @@ var init_firefox = __esm({
|
|
|
17411
17536
|
}
|
|
17412
17537
|
});
|
|
17413
17538
|
|
|
17539
|
+
// src/tools/instructions.ts
|
|
17540
|
+
function buildInstructions(moduleNames, toolNames) {
|
|
17541
|
+
const byName = new Map(MODULES.map((m) => [m.name, m]));
|
|
17542
|
+
const capabilities = moduleNames.flatMap((name) => {
|
|
17543
|
+
const module17 = byName.get(name);
|
|
17544
|
+
return module17 ? [`- ${module17.name}: ${module17.description}`] : [];
|
|
17545
|
+
});
|
|
17546
|
+
const sections = [INSTRUCTIONS_INTRO, ["Enabled capabilities:", ...capabilities].join("\n")];
|
|
17547
|
+
const core = CORE_TOOLS.filter((name) => toolNames.has(name));
|
|
17548
|
+
if (core.length > 0) {
|
|
17549
|
+
const groups = TASK_TOOLS.flatMap(({ purpose, tools }) => {
|
|
17550
|
+
const available = tools.filter((name) => toolNames.has(name));
|
|
17551
|
+
return available.length > 0 ? [`${available.join(" / ")} for ${purpose}`] : [];
|
|
17552
|
+
});
|
|
17553
|
+
const extra = groups.length > 0 ? ` Add task-specific tools to the same search: ${groups.join(", ")}.` : "";
|
|
17554
|
+
sections.push(
|
|
17555
|
+
`When these tools are deferred, load the core set in one search rather than one at a time: ${core.join(", ")}.${extra}`
|
|
17556
|
+
);
|
|
17557
|
+
}
|
|
17558
|
+
return sections.join("\n\n");
|
|
17559
|
+
}
|
|
17560
|
+
var INSTRUCTIONS_INTRO, CORE_TOOLS, TASK_TOOLS;
|
|
17561
|
+
var init_instructions = __esm({
|
|
17562
|
+
"src/tools/instructions.ts"() {
|
|
17563
|
+
"use strict";
|
|
17564
|
+
init_tools();
|
|
17565
|
+
INSTRUCTIONS_INTRO = [
|
|
17566
|
+
"Browse and debug the web with Firefox.",
|
|
17567
|
+
"Use this for any browser or web page task: opening a URL, automating a web flow,",
|
|
17568
|
+
"or debugging a page (console errors, network requests, DOM state).",
|
|
17569
|
+
"The task does not need to name Firefox, reach for these tools unless another",
|
|
17570
|
+
"browser is explicitly requested.",
|
|
17571
|
+
"Chrome-only browser tools do not control Firefox, do not assume they cover this."
|
|
17572
|
+
].join(" ");
|
|
17573
|
+
CORE_TOOLS = ["list_pages", "new_page", "navigate_page", "take_snapshot", "get_page_text"];
|
|
17574
|
+
TASK_TOOLS = [
|
|
17575
|
+
{ purpose: "interaction", tools: ["click_by_uid", "fill_by_uid", "hover_by_uid"] },
|
|
17576
|
+
{ purpose: "debugging", tools: ["list_console_messages", "list_network_requests"] },
|
|
17577
|
+
{ purpose: "visuals", tools: ["screenshot_page", "screenshot_by_uid"] },
|
|
17578
|
+
{ purpose: "one-off JS", tools: ["evaluate_script"] }
|
|
17579
|
+
];
|
|
17580
|
+
}
|
|
17581
|
+
});
|
|
17582
|
+
|
|
17414
17583
|
// src/tools/registry.ts
|
|
17415
17584
|
function buildToolset(options) {
|
|
17416
17585
|
const { moduleNames, warnings } = selectModules(options);
|
|
17417
17586
|
const { toolDefinitions, handlers } = collectTools(moduleNames);
|
|
17418
|
-
|
|
17587
|
+
const instructions = buildInstructions(moduleNames, new Set(handlers.keys()));
|
|
17588
|
+
return { moduleNames, warnings, toolDefinitions, handlers, instructions };
|
|
17419
17589
|
}
|
|
17420
17590
|
function selectModules(options) {
|
|
17421
17591
|
const { tools: requested, preset, enableScript, enablePrivilegedContext } = options;
|
|
@@ -17503,6 +17673,7 @@ var init_registry = __esm({
|
|
|
17503
17673
|
"src/tools/registry.ts"() {
|
|
17504
17674
|
"use strict";
|
|
17505
17675
|
init_tools();
|
|
17676
|
+
init_instructions();
|
|
17506
17677
|
privilegedModuleNames = new Set(MODULES.filter((m) => m.privileged).map((m) => m.name));
|
|
17507
17678
|
}
|
|
17508
17679
|
});
|
|
@@ -17579,11 +17750,14 @@ async function getFirefox() {
|
|
|
17579
17750
|
acceptInsecureCerts: args.acceptInsecureCerts,
|
|
17580
17751
|
connectExisting: args.connectExisting,
|
|
17581
17752
|
marionettePort: args.marionettePort,
|
|
17753
|
+
lookupMarionettePort: args.lookupMarionettePort,
|
|
17582
17754
|
env: envVars,
|
|
17583
17755
|
logFile: args.outputFile ?? void 0,
|
|
17584
17756
|
prefs,
|
|
17585
17757
|
androidDevice: args.androidDevice ?? void 0,
|
|
17586
|
-
androidPackage: args.androidPackage ?? void 0
|
|
17758
|
+
androidPackage: args.androidPackage ?? void 0,
|
|
17759
|
+
androidWipeAppData: args.androidWipeAppData,
|
|
17760
|
+
captureNetworkBodies: !args.disableNetworkBodyCollection
|
|
17587
17761
|
};
|
|
17588
17762
|
}
|
|
17589
17763
|
firefox2 = new FirefoxClient(options);
|
|
@@ -17620,7 +17794,8 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
|
|
|
17620
17794
|
moduleNames,
|
|
17621
17795
|
warnings,
|
|
17622
17796
|
toolDefinitions: allTools,
|
|
17623
|
-
handlers: toolHandlers
|
|
17797
|
+
handlers: toolHandlers,
|
|
17798
|
+
instructions
|
|
17624
17799
|
} = buildToolset({
|
|
17625
17800
|
tools: args.tools,
|
|
17626
17801
|
preset: args.toolPreset,
|
|
@@ -17649,9 +17824,9 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
|
|
|
17649
17824
|
},
|
|
17650
17825
|
{
|
|
17651
17826
|
capabilities: {
|
|
17652
|
-
resources: {},
|
|
17653
17827
|
tools: {}
|
|
17654
|
-
}
|
|
17828
|
+
},
|
|
17829
|
+
instructions
|
|
17655
17830
|
}
|
|
17656
17831
|
);
|
|
17657
17832
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -17683,12 +17858,6 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
|
|
|
17683
17858
|
throw error2;
|
|
17684
17859
|
}
|
|
17685
17860
|
});
|
|
17686
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
17687
|
-
return { resources: [] };
|
|
17688
|
-
});
|
|
17689
|
-
server.setRequestHandler(ReadResourceRequestSchema, async () => {
|
|
17690
|
-
throw new Error("Resource reading not implemented");
|
|
17691
|
-
});
|
|
17692
17861
|
const transport = new StdioServerTransport();
|
|
17693
17862
|
await server.connect(transport);
|
|
17694
17863
|
log("Firefox DevTools MCP server running on stdio");
|
|
@@ -17700,48 +17869,123 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
|
|
|
17700
17869
|
});
|
|
17701
17870
|
process.exit(0);
|
|
17702
17871
|
};
|
|
17703
|
-
const onSignal = () => void cleanup();
|
|
17704
|
-
process.on("SIGTERM", onSignal);
|
|
17705
|
-
process.on("SIGINT", onSignal);
|
|
17706
|
-
process.stdin.on("end", onSignal);
|
|
17707
|
-
process.stdin.on("close", onSignal);
|
|
17872
|
+
const onSignal = () => void cleanup();
|
|
17873
|
+
process.on("SIGTERM", onSignal);
|
|
17874
|
+
process.on("SIGINT", onSignal);
|
|
17875
|
+
process.stdin.on("end", onSignal);
|
|
17876
|
+
process.stdin.on("close", onSignal);
|
|
17877
|
+
}
|
|
17878
|
+
var major, args, firefox2, nextLaunchOptions, pendingWarning;
|
|
17879
|
+
var init_src = __esm({
|
|
17880
|
+
"src/index.ts"() {
|
|
17881
|
+
"use strict";
|
|
17882
|
+
init_server2();
|
|
17883
|
+
init_stdio2();
|
|
17884
|
+
init_types();
|
|
17885
|
+
init_constants();
|
|
17886
|
+
init_logger();
|
|
17887
|
+
init_cli();
|
|
17888
|
+
init_firefox();
|
|
17889
|
+
init_registry();
|
|
17890
|
+
init_response_helpers();
|
|
17891
|
+
[major] = version2.substring(1).split(".").map(Number);
|
|
17892
|
+
if (!major || major < 20) {
|
|
17893
|
+
console.error(`Node ${version2} is not supported. Please use Node.js >=20.`);
|
|
17894
|
+
process.exit(1);
|
|
17895
|
+
}
|
|
17896
|
+
args = {};
|
|
17897
|
+
firefox2 = null;
|
|
17898
|
+
nextLaunchOptions = null;
|
|
17899
|
+
pendingWarning = null;
|
|
17900
|
+
}
|
|
17901
|
+
});
|
|
17902
|
+
|
|
17903
|
+
// src/utils/save-output.ts
|
|
17904
|
+
import { mkdir, rename, stat, unlink, writeFile } from "fs/promises";
|
|
17905
|
+
import { randomBytes } from "crypto";
|
|
17906
|
+
import { homedir as homedir2 } from "os";
|
|
17907
|
+
import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve3, sep } from "path";
|
|
17908
|
+
function homeRoot() {
|
|
17909
|
+
return join3(homedir2(), ".firefox-devtools-mcp");
|
|
17910
|
+
}
|
|
17911
|
+
function generatedName(baseName, extension) {
|
|
17912
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
17913
|
+
return `${baseName}-${timestamp}.${extension}`;
|
|
17914
|
+
}
|
|
17915
|
+
async function isDirectory(path) {
|
|
17916
|
+
try {
|
|
17917
|
+
return (await stat(path)).isDirectory();
|
|
17918
|
+
} catch {
|
|
17919
|
+
return false;
|
|
17920
|
+
}
|
|
17921
|
+
}
|
|
17922
|
+
async function assertAllowedPath(saveTo, resolvedPath) {
|
|
17923
|
+
const { args: args2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
17924
|
+
if (args2?.unrestrictedSavePaths) {
|
|
17925
|
+
return;
|
|
17926
|
+
}
|
|
17927
|
+
const root = isAbsolute(saveTo) ? homeRoot() : process.cwd();
|
|
17928
|
+
if (resolvedPath !== root && !resolvedPath.startsWith(root + sep)) {
|
|
17929
|
+
throw new Error(
|
|
17930
|
+
`saveTo "${saveTo}" resolves outside the allowed location (${resolvedPath}). Relative paths must stay within the current working directory and absolute paths within ${homeRoot()}. Start the server with --unrestricted-save-paths to write to arbitrary locations.`
|
|
17931
|
+
);
|
|
17932
|
+
}
|
|
17933
|
+
}
|
|
17934
|
+
async function saveOutput(content, saveTo, baseName, extension = "json") {
|
|
17935
|
+
let resolvedPath;
|
|
17936
|
+
if (saveTo) {
|
|
17937
|
+
resolvedPath = resolve3(saveTo);
|
|
17938
|
+
if (await isDirectory(resolvedPath)) {
|
|
17939
|
+
resolvedPath = join3(resolvedPath, generatedName(baseName, extension));
|
|
17940
|
+
}
|
|
17941
|
+
await assertAllowedPath(saveTo, resolvedPath);
|
|
17942
|
+
} else {
|
|
17943
|
+
resolvedPath = join3(homeRoot(), "output", generatedName(baseName, extension));
|
|
17944
|
+
}
|
|
17945
|
+
await mkdir(dirname2(resolvedPath), { recursive: true });
|
|
17946
|
+
const tmpPath = `${resolvedPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
17947
|
+
try {
|
|
17948
|
+
await writeFile(tmpPath, content);
|
|
17949
|
+
await rename(tmpPath, resolvedPath);
|
|
17950
|
+
} catch (error2) {
|
|
17951
|
+
await unlink(tmpPath).catch(() => void 0);
|
|
17952
|
+
throw error2;
|
|
17953
|
+
}
|
|
17954
|
+
return { path: resolvedPath, bytes: Buffer.byteLength(content) };
|
|
17955
|
+
}
|
|
17956
|
+
var init_save_output = __esm({
|
|
17957
|
+
"src/utils/save-output.ts"() {
|
|
17958
|
+
"use strict";
|
|
17959
|
+
}
|
|
17960
|
+
});
|
|
17961
|
+
|
|
17962
|
+
// src/tools/module.ts
|
|
17963
|
+
function defineModule(config2) {
|
|
17964
|
+
return {
|
|
17965
|
+
name: config2.name,
|
|
17966
|
+
description: config2.description,
|
|
17967
|
+
...config2.privileged ? { privileged: true } : {},
|
|
17968
|
+
tools: config2.tools.map(([definition, handler]) => ({ definition, handler }))
|
|
17969
|
+
};
|
|
17708
17970
|
}
|
|
17709
|
-
var
|
|
17710
|
-
|
|
17711
|
-
"src/index.ts"() {
|
|
17971
|
+
var init_module = __esm({
|
|
17972
|
+
"src/tools/module.ts"() {
|
|
17712
17973
|
"use strict";
|
|
17713
|
-
init_server2();
|
|
17714
|
-
init_stdio2();
|
|
17715
|
-
init_types();
|
|
17716
|
-
init_constants();
|
|
17717
|
-
init_logger();
|
|
17718
|
-
init_cli();
|
|
17719
|
-
init_firefox();
|
|
17720
|
-
init_registry();
|
|
17721
|
-
init_response_helpers();
|
|
17722
|
-
[major] = version2.substring(1).split(".").map(Number);
|
|
17723
|
-
if (!major || major < 20) {
|
|
17724
|
-
console.error(`Node ${version2} is not supported. Please use Node.js >=20.`);
|
|
17725
|
-
process.exit(1);
|
|
17726
|
-
}
|
|
17727
|
-
args = {};
|
|
17728
|
-
firefox2 = null;
|
|
17729
|
-
nextLaunchOptions = null;
|
|
17730
|
-
pendingWarning = null;
|
|
17731
17974
|
}
|
|
17732
17975
|
});
|
|
17733
17976
|
|
|
17734
17977
|
// src/tools/pages.ts
|
|
17735
17978
|
function formatPageList(tabs, selectedIdx) {
|
|
17736
17979
|
if (tabs.length === 0) {
|
|
17737
|
-
return "
|
|
17980
|
+
return "No pages";
|
|
17738
17981
|
}
|
|
17739
|
-
const lines = [
|
|
17982
|
+
const lines = [`${tabs.length} pages (selected: ${selectedIdx})`];
|
|
17740
17983
|
for (const tab of tabs) {
|
|
17741
17984
|
const idx = tabs.indexOf(tab);
|
|
17742
17985
|
const marker = idx === selectedIdx ? ">" : " ";
|
|
17743
17986
|
const title = (tab.title || "Untitled").substring(0, 40);
|
|
17744
|
-
|
|
17987
|
+
const url = (tab.url || "URL unavailable").substring(0, 200);
|
|
17988
|
+
lines.push(`${marker}[${idx}] ${title} (${url})`);
|
|
17745
17989
|
}
|
|
17746
17990
|
return lines.join("\n");
|
|
17747
17991
|
}
|
|
@@ -17848,12 +18092,57 @@ async function handleClosePage(args2) {
|
|
|
17848
18092
|
return errorResponse(error2);
|
|
17849
18093
|
}
|
|
17850
18094
|
}
|
|
17851
|
-
|
|
18095
|
+
async function respondWithContent(content, args2, baseName, extension) {
|
|
18096
|
+
const {
|
|
18097
|
+
maxLength = DEFAULT_MAX_CONTENT_CHARS,
|
|
18098
|
+
saveTo,
|
|
18099
|
+
preview
|
|
18100
|
+
} = args2 || {};
|
|
18101
|
+
if (saveTo) {
|
|
18102
|
+
const saved = await saveOutput(
|
|
18103
|
+
content,
|
|
18104
|
+
saveTo === true ? void 0 : saveTo,
|
|
18105
|
+
baseName,
|
|
18106
|
+
extension
|
|
18107
|
+
);
|
|
18108
|
+
let output = `${baseName} saved to: ${saved.path} (${(saved.bytes / 1024).toFixed(1)}KB)`;
|
|
18109
|
+
const excerpt = previewExcerpt(content, preview);
|
|
18110
|
+
if (excerpt) {
|
|
18111
|
+
output += "\nPreview:\n" + excerpt;
|
|
18112
|
+
}
|
|
18113
|
+
return successResponse(output);
|
|
18114
|
+
}
|
|
18115
|
+
if (content.length <= maxLength) {
|
|
18116
|
+
return successResponse(content + `
|
|
18117
|
+
|
|
18118
|
+
[full content, ${content.length} chars]`);
|
|
18119
|
+
}
|
|
18120
|
+
const footer = truncationFooter(content.length - maxLength, "chars", [
|
|
18121
|
+
"maxLength to show more",
|
|
18122
|
+
"saveTo to save the full content to a file"
|
|
18123
|
+
]);
|
|
18124
|
+
return successResponse(content.slice(0, maxLength) + "\n\n" + footer);
|
|
18125
|
+
}
|
|
18126
|
+
async function handleGetPageText(args2) {
|
|
18127
|
+
try {
|
|
18128
|
+
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
18129
|
+
const firefox3 = await getFirefox2();
|
|
18130
|
+
const text = await firefox3.evaluate(
|
|
18131
|
+
"document.body ? document.body.innerText : document.documentElement.innerText"
|
|
18132
|
+
);
|
|
18133
|
+
return respondWithContent(text ?? "", args2, "page-text", "txt");
|
|
18134
|
+
} catch (error2) {
|
|
18135
|
+
return errorResponse(error2);
|
|
18136
|
+
}
|
|
18137
|
+
}
|
|
18138
|
+
var DEFAULT_MAX_CONTENT_CHARS, listPagesTool, newPageTool, navigatePageTool, selectPageTool, closePageTool, getPageTextTool, module;
|
|
17852
18139
|
var init_pages2 = __esm({
|
|
17853
18140
|
"src/tools/pages.ts"() {
|
|
17854
18141
|
"use strict";
|
|
17855
18142
|
init_response_helpers();
|
|
18143
|
+
init_save_output();
|
|
17856
18144
|
init_module();
|
|
18145
|
+
DEFAULT_MAX_CONTENT_CHARS = 2e4;
|
|
17857
18146
|
listPagesTool = {
|
|
17858
18147
|
name: "list_pages",
|
|
17859
18148
|
description: "List open tabs (index, title, URL). Selected tab is marked.",
|
|
@@ -17941,6 +18230,30 @@ var init_pages2 = __esm({
|
|
|
17941
18230
|
required: ["pageIdx"]
|
|
17942
18231
|
}
|
|
17943
18232
|
};
|
|
18233
|
+
getPageTextTool = {
|
|
18234
|
+
name: "get_page_text",
|
|
18235
|
+
description: "Get the visible text of the page (document.body.innerText). Caps at maxLength (default 20000 chars); saveTo saves the full text to a file.",
|
|
18236
|
+
annotations: {
|
|
18237
|
+
readOnlyHint: true
|
|
18238
|
+
},
|
|
18239
|
+
inputSchema: {
|
|
18240
|
+
type: "object",
|
|
18241
|
+
properties: {
|
|
18242
|
+
maxLength: {
|
|
18243
|
+
type: "number",
|
|
18244
|
+
description: "Max characters to return inline (default: 20000). Ignored when saveTo is used."
|
|
18245
|
+
},
|
|
18246
|
+
saveTo: {
|
|
18247
|
+
type: ["boolean", "string"],
|
|
18248
|
+
description: "Save the full untruncated text to a file instead of returning it inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
18249
|
+
},
|
|
18250
|
+
preview: {
|
|
18251
|
+
type: "number",
|
|
18252
|
+
description: "Number of characters of the saved text to return inline as a preview when saveTo is used. Omit for no preview."
|
|
18253
|
+
}
|
|
18254
|
+
}
|
|
18255
|
+
}
|
|
18256
|
+
};
|
|
17944
18257
|
module = defineModule({
|
|
17945
18258
|
name: "pages",
|
|
17946
18259
|
description: "Open, navigate, select, and close pages.",
|
|
@@ -17949,7 +18262,8 @@ var init_pages2 = __esm({
|
|
|
17949
18262
|
[newPageTool, handleNewPage],
|
|
17950
18263
|
[navigatePageTool, handleNavigatePage],
|
|
17951
18264
|
[selectPageTool, handleSelectPage],
|
|
17952
|
-
[closePageTool, handleClosePage]
|
|
18265
|
+
[closePageTool, handleClosePage],
|
|
18266
|
+
[getPageTextTool, handleGetPageText]
|
|
17953
18267
|
]
|
|
17954
18268
|
});
|
|
17955
18269
|
}
|
|
@@ -17969,65 +18283,6 @@ var init_uid_helpers = __esm({
|
|
|
17969
18283
|
}
|
|
17970
18284
|
});
|
|
17971
18285
|
|
|
17972
|
-
// src/utils/save-output.ts
|
|
17973
|
-
import { mkdir, rename, stat, unlink, writeFile } from "fs/promises";
|
|
17974
|
-
import { randomBytes } from "crypto";
|
|
17975
|
-
import { homedir as homedir2 } from "os";
|
|
17976
|
-
import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve3, sep } from "path";
|
|
17977
|
-
function homeRoot() {
|
|
17978
|
-
return join3(homedir2(), ".firefox-devtools-mcp");
|
|
17979
|
-
}
|
|
17980
|
-
function generatedName(baseName, extension) {
|
|
17981
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
17982
|
-
return `${baseName}-${timestamp}.${extension}`;
|
|
17983
|
-
}
|
|
17984
|
-
async function isDirectory(path) {
|
|
17985
|
-
try {
|
|
17986
|
-
return (await stat(path)).isDirectory();
|
|
17987
|
-
} catch {
|
|
17988
|
-
return false;
|
|
17989
|
-
}
|
|
17990
|
-
}
|
|
17991
|
-
async function assertAllowedPath(saveTo, resolvedPath) {
|
|
17992
|
-
const { args: args2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
17993
|
-
if (args2?.unrestrictedSavePaths) {
|
|
17994
|
-
return;
|
|
17995
|
-
}
|
|
17996
|
-
const root = isAbsolute(saveTo) ? homeRoot() : process.cwd();
|
|
17997
|
-
if (resolvedPath !== root && !resolvedPath.startsWith(root + sep)) {
|
|
17998
|
-
throw new Error(
|
|
17999
|
-
`saveTo "${saveTo}" resolves outside the allowed location (${resolvedPath}). Relative paths must stay within the current working directory and absolute paths within ${homeRoot()}. Start the server with --unrestricted-save-paths to write to arbitrary locations.`
|
|
18000
|
-
);
|
|
18001
|
-
}
|
|
18002
|
-
}
|
|
18003
|
-
async function saveOutput(content, saveTo, baseName, extension = "json") {
|
|
18004
|
-
let resolvedPath;
|
|
18005
|
-
if (saveTo) {
|
|
18006
|
-
resolvedPath = resolve3(saveTo);
|
|
18007
|
-
if (await isDirectory(resolvedPath)) {
|
|
18008
|
-
resolvedPath = join3(resolvedPath, generatedName(baseName, extension));
|
|
18009
|
-
}
|
|
18010
|
-
await assertAllowedPath(saveTo, resolvedPath);
|
|
18011
|
-
} else {
|
|
18012
|
-
resolvedPath = join3(homeRoot(), "output", generatedName(baseName, extension));
|
|
18013
|
-
}
|
|
18014
|
-
await mkdir(dirname2(resolvedPath), { recursive: true });
|
|
18015
|
-
const tmpPath = `${resolvedPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
18016
|
-
try {
|
|
18017
|
-
await writeFile(tmpPath, content);
|
|
18018
|
-
await rename(tmpPath, resolvedPath);
|
|
18019
|
-
} catch (error2) {
|
|
18020
|
-
await unlink(tmpPath).catch(() => void 0);
|
|
18021
|
-
throw error2;
|
|
18022
|
-
}
|
|
18023
|
-
return { path: resolvedPath, bytes: Buffer.byteLength(content) };
|
|
18024
|
-
}
|
|
18025
|
-
var init_save_output = __esm({
|
|
18026
|
-
"src/utils/save-output.ts"() {
|
|
18027
|
-
"use strict";
|
|
18028
|
-
}
|
|
18029
|
-
});
|
|
18030
|
-
|
|
18031
18286
|
// src/tools/snapshot.ts
|
|
18032
18287
|
async function handleTakeSnapshot(args2) {
|
|
18033
18288
|
try {
|
|
@@ -18063,6 +18318,9 @@ async function handleTakeSnapshot(args2) {
|
|
|
18063
18318
|
if (maxDepth !== void 0) {
|
|
18064
18319
|
options.maxDepth = maxDepth;
|
|
18065
18320
|
}
|
|
18321
|
+
if (saveTo) {
|
|
18322
|
+
options.maxAttrLength = Infinity;
|
|
18323
|
+
}
|
|
18066
18324
|
const formattedText = formatSnapshotTree2(snapshot.json.root, 0, options);
|
|
18067
18325
|
if (saveTo) {
|
|
18068
18326
|
const saved = await saveOutput(
|
|
@@ -18071,7 +18329,7 @@ async function handleTakeSnapshot(args2) {
|
|
|
18071
18329
|
"snapshot",
|
|
18072
18330
|
"txt"
|
|
18073
18331
|
);
|
|
18074
|
-
let output2 = `Snapshot
|
|
18332
|
+
let output2 = `Snapshot saved to: ${saved.path} (${(saved.bytes / 1024).toFixed(1)}KB)`;
|
|
18075
18333
|
if (snapshot.json.truncated) {
|
|
18076
18334
|
output2 += " [DOM truncated]";
|
|
18077
18335
|
}
|
|
@@ -18084,7 +18342,7 @@ async function handleTakeSnapshot(args2) {
|
|
|
18084
18342
|
const lines = formattedText.split("\n");
|
|
18085
18343
|
const truncated = lines.length > maxLines;
|
|
18086
18344
|
const displayLines = truncated ? lines.slice(0, maxLines) : lines;
|
|
18087
|
-
let output =
|
|
18345
|
+
let output = "Snapshot";
|
|
18088
18346
|
if (selector) {
|
|
18089
18347
|
output += ` [selector: ${selector}]`;
|
|
18090
18348
|
}
|
|
@@ -18100,9 +18358,11 @@ async function handleTakeSnapshot(args2) {
|
|
|
18100
18358
|
output += "\n\n";
|
|
18101
18359
|
output += displayLines.join("\n");
|
|
18102
18360
|
if (truncated) {
|
|
18103
|
-
output +=
|
|
18104
|
-
|
|
18105
|
-
|
|
18361
|
+
output += "\n\n" + truncationFooter(lines.length - maxLines, "lines", [
|
|
18362
|
+
"maxLines to show more",
|
|
18363
|
+
"selector to scope",
|
|
18364
|
+
"saveTo to dump the full tree to a file"
|
|
18365
|
+
]);
|
|
18106
18366
|
}
|
|
18107
18367
|
return successResponse(output);
|
|
18108
18368
|
} catch (error2) {
|
|
@@ -18124,7 +18384,7 @@ async function handleResolveUidToSelector(args2) {
|
|
|
18124
18384
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
18125
18385
|
const firefox3 = await getFirefox2();
|
|
18126
18386
|
try {
|
|
18127
|
-
const selector = firefox3.resolveUidToSelector(uid);
|
|
18387
|
+
const selector = await firefox3.resolveUidToSelector(uid);
|
|
18128
18388
|
return successResponse(`${uid} \u2192 ${selector}`);
|
|
18129
18389
|
} catch (error2) {
|
|
18130
18390
|
throw handleUidError(error2, uid);
|
|
@@ -18137,7 +18397,7 @@ async function handleClearSnapshot(_args) {
|
|
|
18137
18397
|
try {
|
|
18138
18398
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
18139
18399
|
const firefox3 = await getFirefox2();
|
|
18140
|
-
firefox3.clearSnapshot();
|
|
18400
|
+
await firefox3.clearSnapshot();
|
|
18141
18401
|
return successResponse("\u{1F9F9} Snapshot cleared");
|
|
18142
18402
|
} catch (error2) {
|
|
18143
18403
|
return errorResponse(error2);
|
|
@@ -18154,7 +18414,7 @@ var init_snapshot2 = __esm({
|
|
|
18154
18414
|
DEFAULT_SNAPSHOT_LINES = 100;
|
|
18155
18415
|
takeSnapshotTool = {
|
|
18156
18416
|
name: "take_snapshot",
|
|
18157
|
-
description: "Capture DOM snapshot with stable UIDs.
|
|
18417
|
+
description: "Capture DOM snapshot with stable UIDs. A UID stays valid across snapshots until its element is removed or the page navigates. Output caps at maxLines (default 100); scope with selector or dump the full tree with saveTo.",
|
|
18158
18418
|
annotations: {
|
|
18159
18419
|
readOnlyHint: true
|
|
18160
18420
|
},
|
|
@@ -18198,7 +18458,7 @@ var init_snapshot2 = __esm({
|
|
|
18198
18458
|
};
|
|
18199
18459
|
resolveUidToSelectorTool = {
|
|
18200
18460
|
name: "resolve_uid_to_selector",
|
|
18201
|
-
description: "Resolve UID to CSS selector. Fails if
|
|
18461
|
+
description: "Resolve UID to CSS selector. Fails if the element is gone.",
|
|
18202
18462
|
annotations: {
|
|
18203
18463
|
readOnlyHint: true
|
|
18204
18464
|
},
|
|
@@ -18215,7 +18475,7 @@ var init_snapshot2 = __esm({
|
|
|
18215
18475
|
};
|
|
18216
18476
|
clearSnapshotTool = {
|
|
18217
18477
|
name: "clear_snapshot",
|
|
18218
|
-
description: "Clear snapshot
|
|
18478
|
+
description: "Clear snapshot UIDs. Usually not needed.",
|
|
18219
18479
|
annotations: {
|
|
18220
18480
|
readOnlyHint: false
|
|
18221
18481
|
},
|
|
@@ -18535,6 +18795,55 @@ var init_input = __esm({
|
|
|
18535
18795
|
});
|
|
18536
18796
|
|
|
18537
18797
|
// src/tools/network.ts
|
|
18798
|
+
async function safeFetchBody(firefox3, id, dataType) {
|
|
18799
|
+
if (typeof firefox3.getNetworkRequestBody !== "function") {
|
|
18800
|
+
return { ok: false, reason: "unsupported" };
|
|
18801
|
+
}
|
|
18802
|
+
try {
|
|
18803
|
+
return await firefox3.getNetworkRequestBody(id, dataType);
|
|
18804
|
+
} catch {
|
|
18805
|
+
return { ok: false, reason: "error" };
|
|
18806
|
+
}
|
|
18807
|
+
}
|
|
18808
|
+
function bodyUnavailableMarker(reason) {
|
|
18809
|
+
switch (reason) {
|
|
18810
|
+
case "unsupported":
|
|
18811
|
+
return "<not captured: body collection not supported by this Firefox>";
|
|
18812
|
+
case "not-collected":
|
|
18813
|
+
return "<not captured>";
|
|
18814
|
+
case "evicted":
|
|
18815
|
+
return "<not available: evicted from the capture buffer>";
|
|
18816
|
+
case "aborted":
|
|
18817
|
+
return "<not available: collection aborted>";
|
|
18818
|
+
default:
|
|
18819
|
+
return "<not available>";
|
|
18820
|
+
}
|
|
18821
|
+
}
|
|
18822
|
+
function hasRequestBody(result) {
|
|
18823
|
+
return result.ok || result.reason === "evicted" || result.reason === "aborted";
|
|
18824
|
+
}
|
|
18825
|
+
function renderBodyInline(result) {
|
|
18826
|
+
if (!result.ok) {
|
|
18827
|
+
return { body: bodyUnavailableMarker(result.reason) };
|
|
18828
|
+
}
|
|
18829
|
+
if (result.type === "base64") {
|
|
18830
|
+
const approxKb = (result.value.length * 3 / 4 / 1024).toFixed(1);
|
|
18831
|
+
return {
|
|
18832
|
+
body: `<binary data (~${approxKb}KB); use saveTo to retrieve the full body>`,
|
|
18833
|
+
encoding: "base64"
|
|
18834
|
+
};
|
|
18835
|
+
}
|
|
18836
|
+
return { body: truncateText(result.value, TOKEN_LIMITS.MAX_RESPONSE_CHARS) };
|
|
18837
|
+
}
|
|
18838
|
+
function renderBodyForFile(result) {
|
|
18839
|
+
if (!result.ok) {
|
|
18840
|
+
return { body: null, encoding: null, unavailable: result.reason };
|
|
18841
|
+
}
|
|
18842
|
+
return {
|
|
18843
|
+
body: result.value,
|
|
18844
|
+
encoding: result.type === "base64" ? "base64" : "utf-8"
|
|
18845
|
+
};
|
|
18846
|
+
}
|
|
18538
18847
|
async function handleListNetworkRequests(args2) {
|
|
18539
18848
|
try {
|
|
18540
18849
|
const {
|
|
@@ -18643,6 +18952,11 @@ async function handleListNetworkRequests(args2) {
|
|
|
18643
18952
|
const effectiveDetail = detail ?? "summary";
|
|
18644
18953
|
const limitedRequests = requests.slice(0, effectiveLimit);
|
|
18645
18954
|
const hasMore = requests.length > effectiveLimit;
|
|
18955
|
+
const moreFooter = hasMore ? "\n" + truncationFooter(requests.length - limitedRequests.length, "requests", [
|
|
18956
|
+
"limit to show more",
|
|
18957
|
+
"urlContains/method/status to filter",
|
|
18958
|
+
"saveTo to save all to a file"
|
|
18959
|
+
]) : "";
|
|
18646
18960
|
if (format === "json") {
|
|
18647
18961
|
const responseData = {
|
|
18648
18962
|
total: requests.length,
|
|
@@ -18684,7 +18998,7 @@ async function handleListNetworkRequests(args2) {
|
|
|
18684
18998
|
});
|
|
18685
18999
|
const header = `[network] ${requests.length} requests${hasMore ? ` (limit ${effectiveLimit})` : ""}
|
|
18686
19000
|
`;
|
|
18687
|
-
return successResponse(header + formattedRequests.join("\n"));
|
|
19001
|
+
return successResponse(header + formattedRequests.join("\n") + moreFooter);
|
|
18688
19002
|
} else if (effectiveDetail === "min") {
|
|
18689
19003
|
const minData = limitedRequests.map((req) => ({
|
|
18690
19004
|
id: req.id,
|
|
@@ -18698,7 +19012,7 @@ async function handleListNetworkRequests(args2) {
|
|
|
18698
19012
|
}));
|
|
18699
19013
|
return successResponse(
|
|
18700
19014
|
`[network] ${requests.length} requests${hasMore ? ` (limit ${effectiveLimit})` : ""}
|
|
18701
|
-
` + JSON.stringify(minData, null, 2)
|
|
19015
|
+
` + JSON.stringify(minData, null, 2) + moreFooter
|
|
18702
19016
|
);
|
|
18703
19017
|
} else {
|
|
18704
19018
|
const fullData = limitedRequests.map((req) => ({
|
|
@@ -18715,7 +19029,7 @@ async function handleListNetworkRequests(args2) {
|
|
|
18715
19029
|
}));
|
|
18716
19030
|
return successResponse(
|
|
18717
19031
|
`[network] ${requests.length} requests${hasMore ? ` (limit ${effectiveLimit})` : ""}
|
|
18718
|
-
` + JSON.stringify(fullData, null, 2)
|
|
19032
|
+
` + JSON.stringify(fullData, null, 2) + moreFooter
|
|
18719
19033
|
);
|
|
18720
19034
|
}
|
|
18721
19035
|
} catch (error2) {
|
|
@@ -18764,8 +19078,29 @@ async function handleGetNetworkRequest(args2) {
|
|
|
18764
19078
|
requestHeaders: request.requestHeaders ?? null,
|
|
18765
19079
|
responseHeaders: request.responseHeaders ?? null
|
|
18766
19080
|
};
|
|
19081
|
+
const [responseBodyResult, requestBodyResult] = await Promise.all([
|
|
19082
|
+
safeFetchBody(firefox3, request.id, "response"),
|
|
19083
|
+
safeFetchBody(firefox3, request.id, "request")
|
|
19084
|
+
]);
|
|
18767
19085
|
if (saveTo) {
|
|
18768
|
-
const
|
|
19086
|
+
const responseFile = renderBodyForFile(responseBodyResult);
|
|
19087
|
+
const fileObject = {
|
|
19088
|
+
...fullDetails,
|
|
19089
|
+
responseBody: responseFile.body,
|
|
19090
|
+
responseBodyEncoding: responseFile.encoding
|
|
19091
|
+
};
|
|
19092
|
+
if (responseFile.unavailable) {
|
|
19093
|
+
fileObject.responseBodyUnavailable = responseFile.unavailable;
|
|
19094
|
+
}
|
|
19095
|
+
if (hasRequestBody(requestBodyResult)) {
|
|
19096
|
+
const requestFile = renderBodyForFile(requestBodyResult);
|
|
19097
|
+
fileObject.requestBody = requestFile.body;
|
|
19098
|
+
fileObject.requestBodyEncoding = requestFile.encoding;
|
|
19099
|
+
if (requestFile.unavailable) {
|
|
19100
|
+
fileObject.requestBodyUnavailable = requestFile.unavailable;
|
|
19101
|
+
}
|
|
19102
|
+
}
|
|
19103
|
+
const fileBody = JSON.stringify(fileObject, null, 2);
|
|
18769
19104
|
const saved = await saveOutput(
|
|
18770
19105
|
fileBody,
|
|
18771
19106
|
saveTo === true ? void 0 : saveTo,
|
|
@@ -18778,11 +19113,23 @@ async function handleGetNetworkRequest(args2) {
|
|
|
18778
19113
|
}
|
|
18779
19114
|
return successResponse(output);
|
|
18780
19115
|
}
|
|
19116
|
+
const responseInline = renderBodyInline(responseBodyResult);
|
|
18781
19117
|
const details = {
|
|
18782
19118
|
...fullDetails,
|
|
18783
19119
|
requestHeaders: truncateHeaders(request.requestHeaders),
|
|
18784
|
-
responseHeaders: truncateHeaders(request.responseHeaders)
|
|
19120
|
+
responseHeaders: truncateHeaders(request.responseHeaders),
|
|
19121
|
+
responseBody: responseInline.body
|
|
18785
19122
|
};
|
|
19123
|
+
if (responseInline.encoding) {
|
|
19124
|
+
details.responseBodyEncoding = responseInline.encoding;
|
|
19125
|
+
}
|
|
19126
|
+
if (hasRequestBody(requestBodyResult)) {
|
|
19127
|
+
const requestInline = renderBodyInline(requestBodyResult);
|
|
19128
|
+
details.requestBody = requestInline.body;
|
|
19129
|
+
if (requestInline.encoding) {
|
|
19130
|
+
details.requestBodyEncoding = requestInline.encoding;
|
|
19131
|
+
}
|
|
19132
|
+
}
|
|
18786
19133
|
if (format === "json") {
|
|
18787
19134
|
return jsonResponse(details);
|
|
18788
19135
|
}
|
|
@@ -18800,7 +19147,7 @@ var init_network2 = __esm({
|
|
|
18800
19147
|
init_module();
|
|
18801
19148
|
listNetworkRequestsTool = {
|
|
18802
19149
|
name: "list_network_requests",
|
|
18803
|
-
description: "List network requests
|
|
19150
|
+
description: "List network requests, returning IDs for get_network_request. Filter by url/method/status; caps at limit (default 50); saveTo saves all matches to a file.",
|
|
18804
19151
|
annotations: {
|
|
18805
19152
|
readOnlyHint: true
|
|
18806
19153
|
},
|
|
@@ -18871,7 +19218,7 @@ var init_network2 = __esm({
|
|
|
18871
19218
|
};
|
|
18872
19219
|
getNetworkRequestTool = {
|
|
18873
19220
|
name: "get_network_request",
|
|
18874
|
-
description: "Get request details by ID. URL lookup as fallback.",
|
|
19221
|
+
description: "Get request details by ID, including the response body (and request body when present). Large text bodies are truncated inline; binary bodies are summarized. URL lookup as fallback.",
|
|
18875
19222
|
annotations: {
|
|
18876
19223
|
readOnlyHint: true
|
|
18877
19224
|
},
|
|
@@ -18893,7 +19240,7 @@ var init_network2 = __esm({
|
|
|
18893
19240
|
},
|
|
18894
19241
|
saveTo: {
|
|
18895
19242
|
type: ["boolean", "string"],
|
|
18896
|
-
description: "Save the request details with full untruncated headers to a file as JSON instead of returning them inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
19243
|
+
description: "Save the request details with full untruncated headers and bodies to a file as JSON instead of returning them inline (binary bodies are stored base64-encoded). Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
18897
19244
|
},
|
|
18898
19245
|
preview: {
|
|
18899
19246
|
type: "number",
|
|
@@ -19072,8 +19419,11 @@ Total messages: ${totalCount}${filterInfo.length > 0 ? `, Filters: ${filterInfo.
|
|
|
19072
19419
|
`;
|
|
19073
19420
|
}
|
|
19074
19421
|
if (truncated) {
|
|
19075
|
-
output +=
|
|
19076
|
-
|
|
19422
|
+
output += "\n" + truncationFooter(filteredCount - messages.length, "messages", [
|
|
19423
|
+
"limit to show more",
|
|
19424
|
+
"level/textContains/source to filter",
|
|
19425
|
+
"saveTo to save all to a file"
|
|
19426
|
+
]);
|
|
19077
19427
|
}
|
|
19078
19428
|
return successResponse(output);
|
|
19079
19429
|
} catch (error2) {
|
|
@@ -19100,7 +19450,7 @@ var init_console2 = __esm({
|
|
|
19100
19450
|
init_module();
|
|
19101
19451
|
listConsoleMessagesTool = {
|
|
19102
19452
|
name: "list_console_messages",
|
|
19103
|
-
description: "List console messages
|
|
19453
|
+
description: "List console messages, filterable by level, time, text, source. Caps at limit (default 50); saveTo saves all matches to a file.",
|
|
19104
19454
|
annotations: {
|
|
19105
19455
|
readOnlyHint: true
|
|
19106
19456
|
},
|
|
@@ -19570,7 +19920,7 @@ var init_utilities = __esm({
|
|
|
19570
19920
|
});
|
|
19571
19921
|
|
|
19572
19922
|
// src/tools/firefox-management.ts
|
|
19573
|
-
import { readFileSync as
|
|
19923
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
19574
19924
|
async function handleGetFirefoxLogs(input) {
|
|
19575
19925
|
try {
|
|
19576
19926
|
const {
|
|
@@ -19598,7 +19948,7 @@ async function handleGetFirefoxLogs(input) {
|
|
|
19598
19948
|
);
|
|
19599
19949
|
}
|
|
19600
19950
|
}
|
|
19601
|
-
const content =
|
|
19951
|
+
const content = readFileSync3(logFilePath, "utf-8");
|
|
19602
19952
|
let allLines = content.split("\n").filter((line) => line.trim().length > 0);
|
|
19603
19953
|
if (grep) {
|
|
19604
19954
|
const grepLower = grep.toLowerCase();
|
|
@@ -20488,67 +20838,6 @@ var init_screencast = __esm({
|
|
|
20488
20838
|
}
|
|
20489
20839
|
});
|
|
20490
20840
|
|
|
20491
|
-
// src/utils/remote-value.ts
|
|
20492
|
-
function remoteValueToNative(rv) {
|
|
20493
|
-
if (!rv || typeof rv !== "object") {
|
|
20494
|
-
return rv;
|
|
20495
|
-
}
|
|
20496
|
-
const { type, value } = rv;
|
|
20497
|
-
switch (type) {
|
|
20498
|
-
case "undefined":
|
|
20499
|
-
return void 0;
|
|
20500
|
-
case "null":
|
|
20501
|
-
return null;
|
|
20502
|
-
case "string":
|
|
20503
|
-
case "boolean":
|
|
20504
|
-
return value;
|
|
20505
|
-
case "number":
|
|
20506
|
-
if (value === "NaN") {
|
|
20507
|
-
return "NaN";
|
|
20508
|
-
}
|
|
20509
|
-
if (value === "Infinity") {
|
|
20510
|
-
return "Infinity";
|
|
20511
|
-
}
|
|
20512
|
-
if (value === "-Infinity") {
|
|
20513
|
-
return "-Infinity";
|
|
20514
|
-
}
|
|
20515
|
-
if (value === "-0") {
|
|
20516
|
-
return "-0";
|
|
20517
|
-
}
|
|
20518
|
-
return value;
|
|
20519
|
-
case "bigint":
|
|
20520
|
-
return `${value}n`;
|
|
20521
|
-
case "array":
|
|
20522
|
-
return value.map(remoteValueToNative);
|
|
20523
|
-
case "object":
|
|
20524
|
-
return Object.fromEntries(
|
|
20525
|
-
value.map(([k, v]) => [k, remoteValueToNative(v)])
|
|
20526
|
-
);
|
|
20527
|
-
case "map":
|
|
20528
|
-
return Object.fromEntries(
|
|
20529
|
-
value.map(([k, v]) => [
|
|
20530
|
-
typeof k === "object" ? JSON.stringify(remoteValueToNative(k)) : String(k),
|
|
20531
|
-
remoteValueToNative(v)
|
|
20532
|
-
])
|
|
20533
|
-
);
|
|
20534
|
-
case "set":
|
|
20535
|
-
return value.map(remoteValueToNative);
|
|
20536
|
-
case "regexp": {
|
|
20537
|
-
const { pattern, flags } = value;
|
|
20538
|
-
return `/${pattern}/${flags ?? ""}`;
|
|
20539
|
-
}
|
|
20540
|
-
case "date":
|
|
20541
|
-
return value;
|
|
20542
|
-
default:
|
|
20543
|
-
return `[${type}]`;
|
|
20544
|
-
}
|
|
20545
|
-
}
|
|
20546
|
-
var init_remote_value = __esm({
|
|
20547
|
-
"src/utils/remote-value.ts"() {
|
|
20548
|
-
"use strict";
|
|
20549
|
-
}
|
|
20550
|
-
});
|
|
20551
|
-
|
|
20552
20841
|
// src/utils/js-validation.ts
|
|
20553
20842
|
function validateFunction(fnString) {
|
|
20554
20843
|
if (!fnString || typeof fnString !== "string") {
|
|
@@ -20588,6 +20877,7 @@ async function handleEvaluateScript(args2) {
|
|
|
20588
20877
|
function: fnString,
|
|
20589
20878
|
args: fnArgs,
|
|
20590
20879
|
timeout,
|
|
20880
|
+
sandbox,
|
|
20591
20881
|
saveTo,
|
|
20592
20882
|
preview
|
|
20593
20883
|
} = args2;
|
|
@@ -20619,7 +20909,7 @@ Please call take_snapshot to get fresh UIDs and try again.`
|
|
|
20619
20909
|
functionDeclaration: fnString,
|
|
20620
20910
|
awaitPromise: true,
|
|
20621
20911
|
arguments: resolvedArgs,
|
|
20622
|
-
target: { context: firefox3.getCurrentContextId() }
|
|
20912
|
+
target: { context: firefox3.getCurrentContextId(), ...sandbox !== void 0 && { sandbox } }
|
|
20623
20913
|
});
|
|
20624
20914
|
const result = await Promise.race([
|
|
20625
20915
|
new Promise((r) => setTimeout(() => r(TIMEOUT), scriptTimeout)),
|
|
@@ -20678,7 +20968,7 @@ var init_script = __esm({
|
|
|
20678
20968
|
init_module();
|
|
20679
20969
|
evaluateScriptTool = {
|
|
20680
20970
|
name: "evaluate_script",
|
|
20681
|
-
description: "
|
|
20971
|
+
description: "Run a JS function in the page and return its result. Prefer this for targeted reads (a value, text, computed style, whether an element exists) instead of a full take_snapshot. Use the UID interaction tools for clicking, typing, and filling.",
|
|
20682
20972
|
annotations: {
|
|
20683
20973
|
readOnlyHint: false
|
|
20684
20974
|
},
|
|
@@ -20707,6 +20997,10 @@ var init_script = __esm({
|
|
|
20707
20997
|
type: "number",
|
|
20708
20998
|
description: "Timeout in ms (default: 5000)"
|
|
20709
20999
|
},
|
|
21000
|
+
sandbox: {
|
|
21001
|
+
type: "string",
|
|
21002
|
+
description: "Evaluate in an isolated sandbox realm with this name instead of the page realm. The sandbox shares the page DOM and keeps the native built-ins even where the page overrode them. Page-defined globals and expandos are invisible from the sandbox, and vice-versa. The same name reuses the same sandbox across calls; omit to evaluate in the page realm."
|
|
21003
|
+
},
|
|
20710
21004
|
saveTo: {
|
|
20711
21005
|
type: ["boolean", "string"],
|
|
20712
21006
|
description: "Save the result to a file as JSON instead of returning it inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
@@ -21182,6 +21476,25 @@ function formatContextList(contexts) {
|
|
|
21182
21476
|
}
|
|
21183
21477
|
return lines.join("\n");
|
|
21184
21478
|
}
|
|
21479
|
+
async function assertPrivilegedContext(firefox3, contextId) {
|
|
21480
|
+
let result;
|
|
21481
|
+
try {
|
|
21482
|
+
result = await firefox3.sendBiDiCommand("browsingContext.getTree", {
|
|
21483
|
+
"moz:scope": "chrome"
|
|
21484
|
+
});
|
|
21485
|
+
} catch (error2) {
|
|
21486
|
+
if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
|
|
21487
|
+
throw new Error(SYSTEM_ACCESS_ERROR);
|
|
21488
|
+
}
|
|
21489
|
+
throw error2;
|
|
21490
|
+
}
|
|
21491
|
+
const contexts = result.contexts || [];
|
|
21492
|
+
if (!contexts.some((ctx) => ctx.context === contextId)) {
|
|
21493
|
+
throw new Error(
|
|
21494
|
+
`${contextId} is not a privileged context. Use list_privileged_contexts to see valid ids.`
|
|
21495
|
+
);
|
|
21496
|
+
}
|
|
21497
|
+
}
|
|
21185
21498
|
async function handleListPrivilegedContexts(_args) {
|
|
21186
21499
|
try {
|
|
21187
21500
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
@@ -21193,11 +21506,7 @@ async function handleListPrivilegedContexts(_args) {
|
|
|
21193
21506
|
return successResponse(formatContextList(contexts));
|
|
21194
21507
|
} catch (error2) {
|
|
21195
21508
|
if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
|
|
21196
|
-
return errorResponse(
|
|
21197
|
-
new Error(
|
|
21198
|
-
"Privileged context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox."
|
|
21199
|
-
)
|
|
21200
|
-
);
|
|
21509
|
+
return errorResponse(new Error(SYSTEM_ACCESS_ERROR));
|
|
21201
21510
|
}
|
|
21202
21511
|
return errorResponse(error2);
|
|
21203
21512
|
}
|
|
@@ -21210,6 +21519,7 @@ async function handleSelectPrivilegedContext(args2) {
|
|
|
21210
21519
|
}
|
|
21211
21520
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
21212
21521
|
const firefox3 = await getFirefox2();
|
|
21522
|
+
await assertPrivilegedContext(firefox3, contextId);
|
|
21213
21523
|
const driver = firefox3.getDriver();
|
|
21214
21524
|
await driver.switchTo().window(contextId);
|
|
21215
21525
|
try {
|
|
@@ -21233,17 +21543,22 @@ async function handleEvaluatePrivilegedScript(args2) {
|
|
|
21233
21543
|
try {
|
|
21234
21544
|
const {
|
|
21235
21545
|
function: fnString,
|
|
21546
|
+
context,
|
|
21236
21547
|
saveTo,
|
|
21237
21548
|
preview
|
|
21238
21549
|
} = args2;
|
|
21239
21550
|
validateFunction(fnString);
|
|
21551
|
+
if (!context || typeof context !== "string") {
|
|
21552
|
+
throw new Error("context parameter is required and must be a string");
|
|
21553
|
+
}
|
|
21240
21554
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
21241
21555
|
const firefox3 = await getFirefox2();
|
|
21556
|
+
await assertPrivilegedContext(firefox3, context);
|
|
21242
21557
|
const result = await firefox3.sendBiDiCommand("script.callFunction", {
|
|
21243
21558
|
functionDeclaration: fnString,
|
|
21244
21559
|
awaitPromise: true,
|
|
21245
21560
|
arguments: [],
|
|
21246
|
-
target: { context
|
|
21561
|
+
target: { context }
|
|
21247
21562
|
});
|
|
21248
21563
|
if (result.type === EvaluateResultType2.Success) {
|
|
21249
21564
|
const json = JSON.stringify(remoteValueToNative(result.result), null, 2) ?? "undefined";
|
|
@@ -21280,7 +21595,7 @@ async function handleEvaluatePrivilegedScript(args2) {
|
|
|
21280
21595
|
return errorResponse(error2);
|
|
21281
21596
|
}
|
|
21282
21597
|
}
|
|
21283
|
-
var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool, EvaluateResultType2, module16;
|
|
21598
|
+
var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool, SYSTEM_ACCESS_ERROR, EvaluateResultType2, module16;
|
|
21284
21599
|
var init_privileged_context = __esm({
|
|
21285
21600
|
"src/tools/privileged-context.ts"() {
|
|
21286
21601
|
"use strict";
|
|
@@ -21320,7 +21635,7 @@ var init_privileged_context = __esm({
|
|
|
21320
21635
|
};
|
|
21321
21636
|
evaluatePrivilegedScriptTool = {
|
|
21322
21637
|
name: "evaluate_privileged_script",
|
|
21323
|
-
description: "Execute JS function in
|
|
21638
|
+
description: "Execute JS function in a privileged (chrome) browsing context. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Get context ids from list_privileged_contexts.",
|
|
21324
21639
|
annotations: {
|
|
21325
21640
|
readOnlyHint: false
|
|
21326
21641
|
},
|
|
@@ -21331,6 +21646,10 @@ var init_privileged_context = __esm({
|
|
|
21331
21646
|
type: "string",
|
|
21332
21647
|
description: 'JS function string, e.g. () => Services.prefs.getBoolPref("foo")'
|
|
21333
21648
|
},
|
|
21649
|
+
context: {
|
|
21650
|
+
type: "string",
|
|
21651
|
+
description: "Privileged browsing context ID from list_privileged_contexts"
|
|
21652
|
+
},
|
|
21334
21653
|
saveTo: {
|
|
21335
21654
|
type: ["boolean", "string"],
|
|
21336
21655
|
description: "Save the result to a file as JSON instead of returning it inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
@@ -21340,9 +21659,10 @@ var init_privileged_context = __esm({
|
|
|
21340
21659
|
description: "Number of characters of the saved result to return inline as a preview when saveTo is used. Omit for no preview."
|
|
21341
21660
|
}
|
|
21342
21661
|
},
|
|
21343
|
-
required: ["function"]
|
|
21662
|
+
required: ["function", "context"]
|
|
21344
21663
|
}
|
|
21345
21664
|
};
|
|
21665
|
+
SYSTEM_ACCESS_ERROR = "Privileged context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.";
|
|
21346
21666
|
EvaluateResultType2 = {
|
|
21347
21667
|
Exception: "exception",
|
|
21348
21668
|
Success: "success"
|
|
@@ -21547,6 +21867,12 @@ var init_cli = __esm({
|
|
|
21547
21867
|
description: "Marionette port to connect to when using --connect-existing (default: 2828)",
|
|
21548
21868
|
default: Number(process.env.MARIONETTE_PORT ?? "2828")
|
|
21549
21869
|
},
|
|
21870
|
+
lookupMarionettePort: {
|
|
21871
|
+
type: "boolean",
|
|
21872
|
+
hidden: true,
|
|
21873
|
+
description: "Lookup the port of a Marionette instance started by Firefox's AI assistant companion, instead of using --marionette-port. Only applies when --connect-existing is set.",
|
|
21874
|
+
default: (process.env.LOOKUP_MARIONETTE_PORT ?? "false") === "true"
|
|
21875
|
+
},
|
|
21550
21876
|
env: {
|
|
21551
21877
|
type: "array",
|
|
21552
21878
|
description: "Environment variables for Firefox in KEY=VALUE format. Can be specified multiple times. Example: --env MOZ_LOG=HTMLMediaElement:4"
|
|
@@ -21570,6 +21896,11 @@ var init_cli = __esm({
|
|
|
21570
21896
|
description: "Android app package name (default: org.mozilla.firefox). Use org.mozilla.fenix for Nightly.",
|
|
21571
21897
|
default: process.env.ANDROID_PACKAGE ?? "org.mozilla.firefox"
|
|
21572
21898
|
},
|
|
21899
|
+
androidWipeAppData: {
|
|
21900
|
+
type: "boolean",
|
|
21901
|
+
description: "Confirm that connecting to Firefox for Android wipes all data of the target app (tabs, history, bookmarks, passwords, settings). Required with --android-device.",
|
|
21902
|
+
default: (process.env.ANDROID_WIPE_APP_DATA ?? "false") === "true"
|
|
21903
|
+
},
|
|
21573
21904
|
logFile: {
|
|
21574
21905
|
type: "string",
|
|
21575
21906
|
description: "Path to a file where MCP server logs will be written. Set DEBUG=* to also enable verbose debug logs."
|
|
@@ -21604,6 +21935,12 @@ var init_cli = __esm({
|
|
|
21604
21935
|
type: "boolean",
|
|
21605
21936
|
description: "Allow tools that save files (e.g. screenshots, snapshots, network/console output) to write to arbitrary locations. By default, relative save paths resolve against the current working directory and absolute save paths are restricted to ~/.firefox-devtools-mcp; this flag lifts both restrictions. Use with caution.",
|
|
21606
21937
|
default: (process.env.UNRESTRICTED_SAVE_PATHS ?? "false") === "true"
|
|
21938
|
+
},
|
|
21939
|
+
disableNetworkBodyCollection: {
|
|
21940
|
+
type: "boolean",
|
|
21941
|
+
hidden: true,
|
|
21942
|
+
description: "Disable capturing network request/response bodies via BiDi data collectors. get_network_request will still return metadata and headers but no bodies. Reduces browser memory and stream-cloning overhead.",
|
|
21943
|
+
default: (process.env.DISABLE_NETWORK_BODY_COLLECTION ?? "false") === "true"
|
|
21607
21944
|
}
|
|
21608
21945
|
};
|
|
21609
21946
|
}
|