@ricsam/r5d-browser 0.0.136 → 0.0.138
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 +7 -33
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/browser-input.mjs +56 -1
- package/dist/mjs/cli-options.mjs +1 -0
- package/dist/mjs/main.mjs +25 -396
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal-diagnostics.mjs +24 -0
- package/dist/mjs/personal-main.mjs +159 -0
- package/dist/mjs/personal-port-forward.mjs +183 -0
- package/dist/mjs/personal-relay-client.mjs +33 -0
- package/dist/mjs/personal-relay-shutdown.mjs +46 -0
- package/dist/mjs/runtime/browser-executor.mjs +11 -0
- package/dist/mjs/runtime/chromium-factory.mjs +101 -0
- package/dist/mjs/runtime/ipc.mjs +128 -0
- package/dist/mjs/runtime/isolated-root.mjs +53 -0
- package/dist/mjs/runtime/keeper.mjs +169 -0
- package/dist/mjs/runtime/ledger.mjs +193 -0
- package/dist/mjs/runtime/main.mjs +105 -0
- package/dist/mjs/runtime/private-paths.mjs +48 -0
- package/dist/mjs/runtime/protocol.mjs +72 -0
- package/dist/types/browser-input.d.ts +4 -0
- package/dist/types/cli-options.d.ts +1 -0
- package/dist/types/personal-diagnostics.d.ts +13 -0
- package/dist/types/personal-main.d.ts +9 -0
- package/dist/types/personal-port-forward.d.ts +45 -0
- package/dist/types/personal-relay-client.d.ts +51 -0
- package/dist/types/personal-relay-shutdown.d.ts +19 -0
- package/dist/types/runtime/browser-executor.d.ts +8 -0
- package/dist/types/runtime/chromium-factory.d.ts +25 -0
- package/dist/types/runtime/fixtures/counter-main.d.ts +1 -0
- package/dist/types/runtime/fixtures/counter-runtime.d.ts +7 -0
- package/dist/types/runtime/ipc.d.ts +23 -0
- package/dist/types/runtime/isolated-root.d.ts +12 -0
- package/dist/types/runtime/keeper.d.ts +44 -0
- package/dist/types/runtime/ledger.d.ts +41 -0
- package/dist/types/runtime/main.d.ts +23 -0
- package/dist/types/runtime/private-paths.d.ts +4 -0
- package/dist/types/runtime/protocol.d.ts +228 -0
- package/package.json +6 -5
- package/dist/cjs/browser-input.cjs +0 -87
- package/dist/cjs/browser-runtime.cjs +0 -708
- package/dist/cjs/chrome-launcher.cjs +0 -701
- package/dist/cjs/cli-options.cjs +0 -95
- package/dist/cjs/main.cjs +0 -427
- package/dist/cjs/password-safe-snapshot.cjs +0 -150
- package/dist/cjs/port-forward-manager.cjs +0 -233
- package/dist/cjs/upload-store.cjs +0 -136
- package/dist/mjs/port-forward-manager.mjs +0 -199
- package/dist/types/port-forward-manager.d.ts +0 -23
|
@@ -1,708 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
var browser_runtime_exports = {};
|
|
30
|
-
__export(browser_runtime_exports, {
|
|
31
|
-
BrowserRuntime: () => BrowserRuntime
|
|
32
|
-
});
|
|
33
|
-
module.exports = __toCommonJS(browser_runtime_exports);
|
|
34
|
-
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
35
|
-
var import_node_path = __toESM(require("node:path"), 1);
|
|
36
|
-
var import_browser_input = require("./browser-input.cjs");
|
|
37
|
-
var import_upload_store = require("./upload-store.cjs");
|
|
38
|
-
var import_browser_upload = require("@ricsam/r5d-api/browser-upload");
|
|
39
|
-
var import_password_safe_snapshot = require("./password-safe-snapshot.cjs");
|
|
40
|
-
const MAX_DOWNLOAD_CHUNK_BYTES = 1024 * 1024;
|
|
41
|
-
const MAX_RUN_JS_RESULT_BYTES = 1024 * 1024;
|
|
42
|
-
const MAX_RUN_JS_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
|
43
|
-
const MAX_RUN_JS_ARTIFACT_COUNT = 32;
|
|
44
|
-
const REF_ACTION_TIMEOUT_MS = 5e3;
|
|
45
|
-
const NAVIGATION_TIMEOUT_MS = 2e4;
|
|
46
|
-
const READ_PAGE_TIMEOUT_MS = 1e4;
|
|
47
|
-
const MAX_FULL_PAGE_HEIGHT = 4e3;
|
|
48
|
-
const MAX_FULL_PAGE_WIDTH = 4096;
|
|
49
|
-
const MAX_TYPED_KEYS_CHARS = 500;
|
|
50
|
-
const MAX_TYPED_KEYS_TOTAL_MS = 2e4;
|
|
51
|
-
function clampInt(value, min, max, fallback) {
|
|
52
|
-
const num = value === void 0 ? fallback : Number(value);
|
|
53
|
-
if (!Number.isFinite(num)) return fallback;
|
|
54
|
-
return Math.min(max, Math.max(min, Math.round(num)));
|
|
55
|
-
}
|
|
56
|
-
function requireRef(value) {
|
|
57
|
-
if (typeof value !== "string" || !/^(f\d+)?e\d+$/.test(value)) {
|
|
58
|
-
throw new Error('ref must look like "e12" or "f1e3", exactly as printed by browser_read_page ([ref=e12]).');
|
|
59
|
-
}
|
|
60
|
-
return value;
|
|
61
|
-
}
|
|
62
|
-
function stripCallLog(message) {
|
|
63
|
-
const withoutCallLog = message.split(/\n\s*Call log:/)[0] ?? message;
|
|
64
|
-
return withoutCallLog.split("\n").filter((line) => !/^\s+at\s/.test(line)).join("\n").trim();
|
|
65
|
-
}
|
|
66
|
-
function isDestroyedContext(message) {
|
|
67
|
-
return /Execution context was destroyed|because of a navigation|Cannot find context with specified id|Target closed/i.test(message);
|
|
68
|
-
}
|
|
69
|
-
function refActionError(error, ref, verb) {
|
|
70
|
-
const message = stripCallLog(error instanceof Error ? error.message : String(error));
|
|
71
|
-
return new Error(
|
|
72
|
-
`${message}
|
|
73
|
-
Ref ${ref} comes from the most recent browser_read_page snapshot of this tab. If the page navigated or re-rendered since, take a new snapshot and retry. If another element covers the target, dismiss the overlay before the ${verb}.`
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
class BrowserRuntime {
|
|
77
|
-
constructor(context, downloadsPath, uploadsPath = `${downloadsPath}.uploads`) {
|
|
78
|
-
this.context = context;
|
|
79
|
-
this.downloadsPath = downloadsPath;
|
|
80
|
-
this.uploads = new import_upload_store.BrowserUploadStore(uploadsPath, (target) => {
|
|
81
|
-
void target.dispose().catch(() => void 0);
|
|
82
|
-
});
|
|
83
|
-
import_node_fs.default.mkdirSync(downloadsPath, { recursive: true });
|
|
84
|
-
for (const page of context.pages()) this.track(page);
|
|
85
|
-
context.on("page", (page) => this.track(page));
|
|
86
|
-
}
|
|
87
|
-
context;
|
|
88
|
-
downloadsPath;
|
|
89
|
-
ids = /* @__PURE__ */ new WeakMap();
|
|
90
|
-
pages = /* @__PURE__ */ new Map();
|
|
91
|
-
cursor = /* @__PURE__ */ new Map();
|
|
92
|
-
viewportSessions = /* @__PURE__ */ new Map();
|
|
93
|
-
uploads;
|
|
94
|
-
downloadSaveQueue = Promise.resolve();
|
|
95
|
-
track(page) {
|
|
96
|
-
const existing = this.ids.get(page);
|
|
97
|
-
if (existing) return existing;
|
|
98
|
-
const id = crypto.randomUUID();
|
|
99
|
-
this.ids.set(page, id);
|
|
100
|
-
this.pages.set(id, page);
|
|
101
|
-
page.on("download", (download) => {
|
|
102
|
-
this.downloadSaveQueue = this.downloadSaveQueue.then(() => this.persistDownload(download)).catch((error) => {
|
|
103
|
-
process.stderr.write(`[r5d-browser] failed to save download: ${error instanceof Error ? error.message : String(error)}
|
|
104
|
-
`);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
page.once("close", () => {
|
|
108
|
-
this.uploads.closeTab(id);
|
|
109
|
-
this.pages.delete(id);
|
|
110
|
-
this.cursor.delete(id);
|
|
111
|
-
this.viewportSessions.delete(id);
|
|
112
|
-
});
|
|
113
|
-
return id;
|
|
114
|
-
}
|
|
115
|
-
safeDownloadFilename(suggestedFilename) {
|
|
116
|
-
const basename = import_node_path.default.basename(suggestedFilename).replace(/[\\/\0]/g, "-").trim();
|
|
117
|
-
if (!basename || basename === "." || basename === "..") return "download";
|
|
118
|
-
const extension = import_node_path.default.extname(basename).slice(0, 30);
|
|
119
|
-
let stem = basename.slice(0, basename.length - import_node_path.default.extname(basename).length) || "download";
|
|
120
|
-
while (Buffer.byteLength(`${stem}${extension}`) > 200) stem = stem.slice(0, -1);
|
|
121
|
-
return `${stem || "download"}${extension}`;
|
|
122
|
-
}
|
|
123
|
-
availableDownloadPath(suggestedFilename) {
|
|
124
|
-
const filename = this.safeDownloadFilename(suggestedFilename);
|
|
125
|
-
const extension = import_node_path.default.extname(filename);
|
|
126
|
-
const stem = filename.slice(0, filename.length - extension.length) || "download";
|
|
127
|
-
for (let suffix = 1; ; suffix += 1) {
|
|
128
|
-
const candidate = suffix === 1 ? filename : `${stem}-${suffix}${extension}`;
|
|
129
|
-
const candidatePath = import_node_path.default.join(this.downloadsPath, candidate);
|
|
130
|
-
if (!import_node_fs.default.existsSync(candidatePath)) return candidatePath;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async persistDownload(download) {
|
|
134
|
-
const failure = await download.failure();
|
|
135
|
-
if (failure) throw new Error(failure);
|
|
136
|
-
const targetPath = this.availableDownloadPath(download.suggestedFilename());
|
|
137
|
-
await download.saveAs(targetPath);
|
|
138
|
-
process.stdout.write(`[r5d-browser] downloaded ${import_node_path.default.basename(targetPath)}
|
|
139
|
-
`);
|
|
140
|
-
}
|
|
141
|
-
downloadId(filename) {
|
|
142
|
-
return Buffer.from(filename, "utf8").toString("base64url");
|
|
143
|
-
}
|
|
144
|
-
resolveDownload(downloadId) {
|
|
145
|
-
if (typeof downloadId !== "string" || !downloadId) throw new Error("downloadId is required.");
|
|
146
|
-
const filename = Buffer.from(downloadId, "base64url").toString("utf8");
|
|
147
|
-
if (this.downloadId(filename) !== downloadId || filename !== import_node_path.default.basename(filename) || filename.includes("\0")) {
|
|
148
|
-
throw new Error("Invalid downloadId.");
|
|
149
|
-
}
|
|
150
|
-
const filePath = import_node_path.default.join(this.downloadsPath, filename);
|
|
151
|
-
let stats;
|
|
152
|
-
try {
|
|
153
|
-
stats = import_node_fs.default.lstatSync(filePath);
|
|
154
|
-
} catch {
|
|
155
|
-
throw new Error(`Browser download ${downloadId} no longer exists.`);
|
|
156
|
-
}
|
|
157
|
-
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Browser download is not a regular file.");
|
|
158
|
-
return { filename, filePath, stats };
|
|
159
|
-
}
|
|
160
|
-
async listDownloads() {
|
|
161
|
-
await this.downloadSaveQueue;
|
|
162
|
-
return import_node_fs.default.readdirSync(this.downloadsPath, { withFileTypes: true }).flatMap((entry) => {
|
|
163
|
-
if (!entry.isFile() || entry.name.startsWith(".")) return [];
|
|
164
|
-
const { filename, stats } = this.resolveDownload(this.downloadId(entry.name));
|
|
165
|
-
return [
|
|
166
|
-
{
|
|
167
|
-
downloadId: this.downloadId(filename),
|
|
168
|
-
filename,
|
|
169
|
-
size: stats.size,
|
|
170
|
-
modifiedAt: stats.mtime.toISOString()
|
|
171
|
-
}
|
|
172
|
-
];
|
|
173
|
-
}).sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt) || left.filename.localeCompare(right.filename));
|
|
174
|
-
}
|
|
175
|
-
async getDownloadChunk(input) {
|
|
176
|
-
await this.downloadSaveQueue;
|
|
177
|
-
const { filename, filePath, stats } = this.resolveDownload(input.downloadId);
|
|
178
|
-
const offset = input.offset === void 0 ? 0 : Number(input.offset);
|
|
179
|
-
const requestedBytes = input.maxBytes === void 0 ? MAX_DOWNLOAD_CHUNK_BYTES : Number(input.maxBytes);
|
|
180
|
-
if (!Number.isSafeInteger(offset) || offset < 0 || offset > stats.size) throw new Error("Invalid download offset.");
|
|
181
|
-
if (!Number.isSafeInteger(requestedBytes) || requestedBytes < 1) throw new Error("Invalid download chunk size.");
|
|
182
|
-
const chunkSize = Math.min(requestedBytes, MAX_DOWNLOAD_CHUNK_BYTES, stats.size - offset);
|
|
183
|
-
const bytes = Buffer.alloc(chunkSize);
|
|
184
|
-
const handle = import_node_fs.default.openSync(filePath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
185
|
-
let bytesRead = 0;
|
|
186
|
-
try {
|
|
187
|
-
const openedStats = import_node_fs.default.fstatSync(handle);
|
|
188
|
-
if (!openedStats.isFile() || openedStats.dev !== stats.dev || openedStats.ino !== stats.ino) {
|
|
189
|
-
throw new Error("Browser download changed before it could be read.");
|
|
190
|
-
}
|
|
191
|
-
bytesRead = import_node_fs.default.readSync(handle, bytes, 0, chunkSize, offset);
|
|
192
|
-
const completedStats = import_node_fs.default.fstatSync(handle);
|
|
193
|
-
if (completedStats.size !== stats.size || completedStats.mtimeMs !== stats.mtimeMs) {
|
|
194
|
-
throw new Error("Browser download changed while it was being read.");
|
|
195
|
-
}
|
|
196
|
-
} finally {
|
|
197
|
-
import_node_fs.default.closeSync(handle);
|
|
198
|
-
}
|
|
199
|
-
const nextOffset = offset + bytesRead;
|
|
200
|
-
return {
|
|
201
|
-
downloadId: this.downloadId(filename),
|
|
202
|
-
filename,
|
|
203
|
-
size: stats.size,
|
|
204
|
-
modifiedAt: stats.mtime.toISOString(),
|
|
205
|
-
offset,
|
|
206
|
-
nextOffset,
|
|
207
|
-
eof: nextOffset === stats.size,
|
|
208
|
-
base64: bytes.subarray(0, bytesRead).toString("base64")
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
page(tabId) {
|
|
212
|
-
if (typeof tabId !== "string") throw new Error("tabId is required.");
|
|
213
|
-
const page = this.pages.get(tabId);
|
|
214
|
-
if (!page || page.isClosed()) throw new Error(`Browser tab ${tabId} is no longer open.`);
|
|
215
|
-
return page;
|
|
216
|
-
}
|
|
217
|
-
refLocator(page, ref) {
|
|
218
|
-
return page.locator(`aria-ref=${ref}`);
|
|
219
|
-
}
|
|
220
|
-
async gotoSettled(page, url) {
|
|
221
|
-
try {
|
|
222
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: NAVIGATION_TIMEOUT_MS });
|
|
223
|
-
} catch (error) {
|
|
224
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
225
|
-
if (!/ERR_ABORTED|interrupted by another navigation/i.test(message)) throw new Error(stripCallLog(message));
|
|
226
|
-
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
async tabInfo(page) {
|
|
230
|
-
const tabId = this.track(page);
|
|
231
|
-
let windowId = "unknown";
|
|
232
|
-
try {
|
|
233
|
-
const cdp = await this.context.newCDPSession(page);
|
|
234
|
-
const result = await cdp.send("Browser.getWindowForTarget");
|
|
235
|
-
windowId = String(result.windowId);
|
|
236
|
-
await cdp.detach();
|
|
237
|
-
} catch {
|
|
238
|
-
}
|
|
239
|
-
return { tabId, windowId, title: await page.title().catch(() => ""), url: page.url() };
|
|
240
|
-
}
|
|
241
|
-
async listTabs() {
|
|
242
|
-
return await Promise.all([...this.pages.values()].filter((page) => !page.isClosed()).map((page) => this.tabInfo(page)));
|
|
243
|
-
}
|
|
244
|
-
async drainDownloads() {
|
|
245
|
-
await this.downloadSaveQueue;
|
|
246
|
-
}
|
|
247
|
-
async setViewport(page, tabId, viewport) {
|
|
248
|
-
const existing = this.viewportSessions.get(tabId);
|
|
249
|
-
if (viewport) {
|
|
250
|
-
const cdp = existing ?? await this.context.newCDPSession(page);
|
|
251
|
-
try {
|
|
252
|
-
await cdp.send("Emulation.setDeviceMetricsOverride", { ...viewport, deviceScaleFactor: 0, mobile: false });
|
|
253
|
-
this.viewportSessions.set(tabId, cdp);
|
|
254
|
-
} catch (error) {
|
|
255
|
-
if (!existing) await cdp.detach().catch(() => void 0);
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
} else if (existing) {
|
|
259
|
-
await existing.send("Emulation.clearDeviceMetricsOverride");
|
|
260
|
-
this.viewportSessions.delete(tabId);
|
|
261
|
-
await existing.detach().catch(() => void 0);
|
|
262
|
-
}
|
|
263
|
-
this.cursor.delete(tabId);
|
|
264
|
-
if (viewport) {
|
|
265
|
-
const ready = await page.waitForFunction(
|
|
266
|
-
({ width, height }) => window.innerWidth === width && window.innerHeight === height,
|
|
267
|
-
viewport,
|
|
268
|
-
{ timeout: 5e3 }
|
|
269
|
-
);
|
|
270
|
-
await ready.dispose();
|
|
271
|
-
}
|
|
272
|
-
const actual = await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
273
|
-
return { tabId, ...actual, reset: viewport === null };
|
|
274
|
-
}
|
|
275
|
-
closeUploads() {
|
|
276
|
-
this.uploads.close();
|
|
277
|
-
}
|
|
278
|
-
async execute(action, input) {
|
|
279
|
-
input = (0, import_browser_input.normalizeBrowserInput)(action, input);
|
|
280
|
-
if (action === "upload_abort") {
|
|
281
|
-
this.uploads.abort(input.uploadId, input.tabId);
|
|
282
|
-
return { aborted: true };
|
|
283
|
-
}
|
|
284
|
-
if (action === "list_tabs") return { tabs: await this.listTabs() };
|
|
285
|
-
if (action === "list_downloads") return { downloads: await this.listDownloads() };
|
|
286
|
-
if (action === "get_download") return await this.getDownloadChunk(input);
|
|
287
|
-
if (action === "open_tab") {
|
|
288
|
-
const previous = this.context.pages().findLast((candidate) => !candidate.isClosed());
|
|
289
|
-
let page2;
|
|
290
|
-
if (previous) {
|
|
291
|
-
const cdp = await this.context.newCDPSession(previous);
|
|
292
|
-
const pagePromise = this.context.waitForEvent("page");
|
|
293
|
-
await cdp.send("Target.createTarget", {
|
|
294
|
-
url: "about:blank",
|
|
295
|
-
newWindow: input.disposition === "window",
|
|
296
|
-
background: true
|
|
297
|
-
});
|
|
298
|
-
page2 = await pagePromise;
|
|
299
|
-
await cdp.detach();
|
|
300
|
-
if (input.disposition === "window") {
|
|
301
|
-
const pageCdp = await this.context.newCDPSession(page2);
|
|
302
|
-
const { windowId } = await pageCdp.send("Browser.getWindowForTarget");
|
|
303
|
-
await pageCdp.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "minimized" } });
|
|
304
|
-
await pageCdp.detach();
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
page2 = await this.context.newPage();
|
|
308
|
-
}
|
|
309
|
-
const url = typeof input.url === "string" ? input.url : "about:blank";
|
|
310
|
-
if (url !== "about:blank") await this.gotoSettled(page2, url);
|
|
311
|
-
return { tab: await this.tabInfo(page2) };
|
|
312
|
-
}
|
|
313
|
-
const page = this.page(input.tabId);
|
|
314
|
-
const tabId = input.tabId;
|
|
315
|
-
switch (action) {
|
|
316
|
-
case "close_tab":
|
|
317
|
-
await page.close();
|
|
318
|
-
return { closed: true, tabId };
|
|
319
|
-
case "navigate":
|
|
320
|
-
await this.gotoSettled(page, String(input.url));
|
|
321
|
-
return { tab: await this.tabInfo(page) };
|
|
322
|
-
case "set_viewport":
|
|
323
|
-
return await this.setViewport(page, tabId, (0, import_browser_input.parseViewportInput)(input));
|
|
324
|
-
case "read_page": {
|
|
325
|
-
const maxChars = clampInt(input.maxChars, 1e3, 2e5, 5e4);
|
|
326
|
-
const depth = input.depth === void 0 ? void 0 : clampInt(input.depth, 1, 50, 50);
|
|
327
|
-
const options = { mode: "ai", boxes: true, timeout: READ_PAGE_TIMEOUT_MS, ...depth === void 0 ? {} : { depth } };
|
|
328
|
-
let snapshot;
|
|
329
|
-
try {
|
|
330
|
-
snapshot = await (0, import_password_safe_snapshot.passwordSafeAriaSnapshot)(page, options);
|
|
331
|
-
} catch (error) {
|
|
332
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
333
|
-
if (isDestroyedContext(message)) {
|
|
334
|
-
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
335
|
-
snapshot = await (0, import_password_safe_snapshot.passwordSafeAriaSnapshot)(page, options);
|
|
336
|
-
} else if (/Timeout/i.test(message)) {
|
|
337
|
-
throw new Error(
|
|
338
|
-
`read_page timed out after ${READ_PAGE_TIMEOUT_MS}ms building the outline. The page is very large or busy; retry with a depth limit (e.g. depth: 10).`
|
|
339
|
-
);
|
|
340
|
-
} else {
|
|
341
|
-
throw new Error(stripCallLog(message));
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
let text = snapshot;
|
|
345
|
-
let truncated = false;
|
|
346
|
-
if (text.length > maxChars) {
|
|
347
|
-
const cut = text.lastIndexOf("\n", maxChars);
|
|
348
|
-
text = text.slice(0, cut > 0 ? cut : maxChars);
|
|
349
|
-
truncated = true;
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
tabId,
|
|
353
|
-
url: page.url(),
|
|
354
|
-
title: await page.title().catch(() => ""),
|
|
355
|
-
text,
|
|
356
|
-
truncated,
|
|
357
|
-
...truncated ? { note: `Truncated at ${maxChars} of ${snapshot.length} chars. Raise maxChars, pass depth for an overview, or scroll and re-read.` } : {}
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
case "screenshot": {
|
|
361
|
-
if (input.fullPage === true) {
|
|
362
|
-
const metrics = await page.evaluate(() => ({
|
|
363
|
-
// Match Playwright's full-page bounds, which exclude scrollbar gutters.
|
|
364
|
-
docWidth: Math.max(
|
|
365
|
-
document.documentElement.scrollWidth,
|
|
366
|
-
document.documentElement.offsetWidth,
|
|
367
|
-
document.documentElement.clientWidth,
|
|
368
|
-
document.body?.scrollWidth ?? 0,
|
|
369
|
-
document.body?.offsetWidth ?? 0,
|
|
370
|
-
document.body?.clientWidth ?? 0
|
|
371
|
-
),
|
|
372
|
-
docHeight: Math.max(
|
|
373
|
-
document.documentElement.scrollHeight,
|
|
374
|
-
document.documentElement.offsetHeight,
|
|
375
|
-
document.documentElement.clientHeight,
|
|
376
|
-
document.body?.scrollHeight ?? 0,
|
|
377
|
-
document.body?.offsetHeight ?? 0,
|
|
378
|
-
document.body?.clientHeight ?? 0
|
|
379
|
-
)
|
|
380
|
-
}));
|
|
381
|
-
const width = Math.min(metrics.docWidth, MAX_FULL_PAGE_WIDTH);
|
|
382
|
-
const height = Math.min(metrics.docHeight, MAX_FULL_PAGE_HEIGHT);
|
|
383
|
-
const truncated = metrics.docHeight > MAX_FULL_PAGE_HEIGHT || metrics.docWidth > MAX_FULL_PAGE_WIDTH;
|
|
384
|
-
const bytes2 = truncated ? await page.screenshot({ type: "png", scale: "css", timeout: 15e3, fullPage: true, clip: { x: 0, y: 0, width, height } }) : await page.screenshot({ type: "png", scale: "css", timeout: 15e3, fullPage: true });
|
|
385
|
-
return { base64: bytes2.toString("base64"), width, height, fullPage: true, pageHeight: metrics.docHeight, truncated };
|
|
386
|
-
}
|
|
387
|
-
const bytes = await page.screenshot({ type: "png", scale: "css" });
|
|
388
|
-
const viewport = page.viewportSize();
|
|
389
|
-
const cursor = this.cursor.get(tabId);
|
|
390
|
-
return {
|
|
391
|
-
base64: bytes.toString("base64"),
|
|
392
|
-
width: viewport?.width ?? Number(await page.evaluate(() => window.innerWidth)),
|
|
393
|
-
height: viewport?.height ?? Number(await page.evaluate(() => window.innerHeight)),
|
|
394
|
-
cursorX: cursor?.x,
|
|
395
|
-
cursorY: cursor?.y
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
case "move_mouse": {
|
|
399
|
-
const x = Number(input.x);
|
|
400
|
-
const y = Number(input.y);
|
|
401
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("move_mouse requires numeric x and y coordinates.");
|
|
402
|
-
await page.mouse.move(x, y);
|
|
403
|
-
this.cursor.set(tabId, { x, y });
|
|
404
|
-
return { tabId, x, y };
|
|
405
|
-
}
|
|
406
|
-
case "mouse_click": {
|
|
407
|
-
const button = input.button === "middle" || input.button === "right" ? input.button : "left";
|
|
408
|
-
const clickCount = input.clickCount === 2 ? 2 : 1;
|
|
409
|
-
if (input.ref !== void 0) {
|
|
410
|
-
if (input.x !== void 0 || input.y !== void 0) throw new Error("mouse_click accepts ref or x/y, not both.");
|
|
411
|
-
const ref = requireRef(input.ref);
|
|
412
|
-
const locator = this.refLocator(page, ref);
|
|
413
|
-
try {
|
|
414
|
-
await locator.click({ button, clickCount, timeout: REF_ACTION_TIMEOUT_MS });
|
|
415
|
-
} catch (error) {
|
|
416
|
-
throw refActionError(error, ref, "click");
|
|
417
|
-
}
|
|
418
|
-
const box = await locator.boundingBox().catch(() => null);
|
|
419
|
-
if (box) this.cursor.set(tabId, { x: box.x + box.width / 2, y: box.y + box.height / 2 });
|
|
420
|
-
return {
|
|
421
|
-
tabId,
|
|
422
|
-
ref,
|
|
423
|
-
button,
|
|
424
|
-
clickCount,
|
|
425
|
-
...box ? { x: Math.round(box.x + box.width / 2), y: Math.round(box.y + box.height / 2) } : {}
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
if (input.x !== void 0 || input.y !== void 0) {
|
|
429
|
-
const x = Number(input.x);
|
|
430
|
-
const y = Number(input.y);
|
|
431
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("mouse_click by coordinates requires numeric x and y.");
|
|
432
|
-
await page.mouse.move(x, y);
|
|
433
|
-
this.cursor.set(tabId, { x, y });
|
|
434
|
-
await page.mouse.click(x, y, { button, clickCount });
|
|
435
|
-
return { tabId, x, y, button, clickCount };
|
|
436
|
-
}
|
|
437
|
-
const cursor = this.cursor.get(tabId);
|
|
438
|
-
if (!cursor) throw new Error("Pass ref or x/y, or move the mouse in this tab before clicking.");
|
|
439
|
-
await page.mouse.click(cursor.x, cursor.y, { button, clickCount });
|
|
440
|
-
return { tabId, ...cursor, button };
|
|
441
|
-
}
|
|
442
|
-
case "scroll": {
|
|
443
|
-
if (input.ref !== void 0) {
|
|
444
|
-
if (input.direction !== void 0) throw new Error("scroll accepts ref or direction, not both.");
|
|
445
|
-
const ref = requireRef(input.ref);
|
|
446
|
-
const locator = this.refLocator(page, ref);
|
|
447
|
-
try {
|
|
448
|
-
await locator.scrollIntoViewIfNeeded({ timeout: REF_ACTION_TIMEOUT_MS });
|
|
449
|
-
} catch (error) {
|
|
450
|
-
throw refActionError(error, ref, "scroll");
|
|
451
|
-
}
|
|
452
|
-
const box = await locator.boundingBox().catch(() => null);
|
|
453
|
-
if (box) this.cursor.set(tabId, { x: box.x + box.width / 2, y: box.y + box.height / 2 });
|
|
454
|
-
return { tabId, ref, ...box ? { x: Math.round(box.x + box.width / 2), y: Math.round(box.y + box.height / 2) } : {} };
|
|
455
|
-
}
|
|
456
|
-
const direction = input.direction;
|
|
457
|
-
if (direction !== "up" && direction !== "down" && direction !== "left" && direction !== "right") {
|
|
458
|
-
throw new Error('scroll requires ref, or direction "up", "down", "left", or "right".');
|
|
459
|
-
}
|
|
460
|
-
const amount = clampInt(input.amount, 1, 2e4, 600);
|
|
461
|
-
let pointer;
|
|
462
|
-
if (input.x !== void 0 || input.y !== void 0) {
|
|
463
|
-
const x = Number(input.x);
|
|
464
|
-
const y = Number(input.y);
|
|
465
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("scroll requires numeric x and y when provided.");
|
|
466
|
-
pointer = { x, y };
|
|
467
|
-
} else {
|
|
468
|
-
pointer = this.cursor.get(tabId) ?? await page.evaluate(() => ({ x: Math.round(window.innerWidth / 2), y: Math.round(window.innerHeight / 2) }));
|
|
469
|
-
}
|
|
470
|
-
await page.mouse.move(pointer.x, pointer.y);
|
|
471
|
-
this.cursor.set(tabId, pointer);
|
|
472
|
-
const deltaX = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
473
|
-
const deltaY = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
474
|
-
await page.mouse.wheel(deltaX, deltaY);
|
|
475
|
-
return { tabId, direction, amount, ...pointer };
|
|
476
|
-
}
|
|
477
|
-
case "wait_for": {
|
|
478
|
-
const timeout = clampInt(input.timeoutMs, 500, 25e3, 1e4);
|
|
479
|
-
const provided = ["selector", "text", "url", "loadState"].filter((key) => input[key] !== void 0);
|
|
480
|
-
const condition = provided[0];
|
|
481
|
-
if (provided.length !== 1 || condition === void 0) {
|
|
482
|
-
throw new Error("wait_for requires exactly one of selector, text, url, or loadState.");
|
|
483
|
-
}
|
|
484
|
-
const state = input.state === "hidden" ? "hidden" : "visible";
|
|
485
|
-
const started = Date.now();
|
|
486
|
-
try {
|
|
487
|
-
if (typeof input.selector === "string") {
|
|
488
|
-
await page.locator(input.selector).first().waitFor({ state, timeout });
|
|
489
|
-
} else if (typeof input.text === "string") {
|
|
490
|
-
await page.getByText(input.text).first().waitFor({ state, timeout });
|
|
491
|
-
} else if (typeof input.url === "string") {
|
|
492
|
-
await page.waitForURL(input.url, { timeout, waitUntil: "domcontentloaded" });
|
|
493
|
-
} else if (input.loadState === "load" || input.loadState === "domcontentloaded" || input.loadState === "networkidle") {
|
|
494
|
-
await page.waitForLoadState(input.loadState, { timeout });
|
|
495
|
-
} else {
|
|
496
|
-
throw new Error('wait_for loadState must be "load", "domcontentloaded", or "networkidle".');
|
|
497
|
-
}
|
|
498
|
-
} catch (error) {
|
|
499
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
500
|
-
if (!/Timeout|exceeded/i.test(message)) throw new Error(stripCallLog(message));
|
|
501
|
-
throw new Error(
|
|
502
|
-
`wait_for gave up after ${timeout}ms waiting for ${condition} ${JSON.stringify(input[condition])}${condition === "selector" || condition === "text" ? ` to become ${state}` : ""}. Current URL: ${page.url()}. Take a browser_read_page snapshot to see the actual page state.`
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
return { tabId, matched: true, elapsedMs: Date.now() - started, url: page.url(), title: await page.title().catch(() => "") };
|
|
506
|
-
}
|
|
507
|
-
case "upload_begin": {
|
|
508
|
-
const target = (0, import_browser_upload.parseUploadTarget)(input);
|
|
509
|
-
const locator = target.ref ? this.refLocator(page, target.ref) : page.locator(target.selector);
|
|
510
|
-
const element = await locator.elementHandle({ timeout: REF_ACTION_TIMEOUT_MS });
|
|
511
|
-
if (!element) throw new Error("File input was not found.");
|
|
512
|
-
try {
|
|
513
|
-
const info = await element.evaluate((el) => ({
|
|
514
|
-
fileInput: el instanceof HTMLInputElement && el.type === "file",
|
|
515
|
-
directory: el.hasAttribute("webkitdirectory"),
|
|
516
|
-
multiple: el.hasAttribute("multiple")
|
|
517
|
-
}));
|
|
518
|
-
if (!info.fileInput || info.directory) throw new Error("Target must be a file input (directory inputs are not supported).");
|
|
519
|
-
if (Array.isArray(input.files) && input.files.length > 1 && !info.multiple) throw new Error("This file input does not accept multiple files.");
|
|
520
|
-
this.uploads.begin(input.uploadId, tabId, input.files, element);
|
|
521
|
-
return { tabId, uploadId: input.uploadId };
|
|
522
|
-
} catch (error) {
|
|
523
|
-
await element.dispose().catch(() => void 0);
|
|
524
|
-
throw error;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
case "upload_chunk":
|
|
528
|
-
this.uploads.append(input.uploadId, tabId, input.index, input.offset, input.base64);
|
|
529
|
-
return { tabId, uploadId: input.uploadId };
|
|
530
|
-
case "upload_attach": {
|
|
531
|
-
const upload = this.uploads.ready(input.uploadId, tabId, input.hashes);
|
|
532
|
-
if (!await upload.target.evaluate((el) => el.isConnected && el instanceof HTMLInputElement && el.type === "file")) {
|
|
533
|
-
throw new Error("The file input was removed or changed during the upload. Read the page again and retry.");
|
|
534
|
-
}
|
|
535
|
-
await upload.target.setInputFiles(upload.paths, { timeout: REF_ACTION_TIMEOUT_MS });
|
|
536
|
-
this.uploads.attached(input.uploadId, tabId);
|
|
537
|
-
return { tabId, files: upload.files };
|
|
538
|
-
}
|
|
539
|
-
case "form_input": {
|
|
540
|
-
const ref = requireRef(input.ref);
|
|
541
|
-
const provided = ["value", "values", "checked"].filter((key) => input[key] !== void 0);
|
|
542
|
-
if (provided.length !== 1) throw new Error("form_input requires exactly one of value, values, or checked.");
|
|
543
|
-
const locator = this.refLocator(page, ref);
|
|
544
|
-
let info;
|
|
545
|
-
try {
|
|
546
|
-
info = await locator.evaluate(
|
|
547
|
-
(el) => {
|
|
548
|
-
const tag = el.tagName.toLowerCase();
|
|
549
|
-
if (el instanceof HTMLSelectElement) {
|
|
550
|
-
return {
|
|
551
|
-
kind: el.multiple ? "multiselect" : "select",
|
|
552
|
-
tag,
|
|
553
|
-
options: [...el.options].map((option) => ({ value: option.value, label: option.label.trim() }))
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) return { kind: "checkable", tag };
|
|
557
|
-
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) return { kind: "text", tag };
|
|
558
|
-
if (el instanceof HTMLElement && el.isContentEditable) return { kind: "contenteditable", tag };
|
|
559
|
-
return { kind: "other", tag };
|
|
560
|
-
},
|
|
561
|
-
void 0,
|
|
562
|
-
{ timeout: REF_ACTION_TIMEOUT_MS }
|
|
563
|
-
);
|
|
564
|
-
} catch (error) {
|
|
565
|
-
throw refActionError(error, ref, "form_input");
|
|
566
|
-
}
|
|
567
|
-
const expectField = (field) => {
|
|
568
|
-
if (provided[0] !== field) throw new Error(`Ref ${ref} is a ${info.kind} (${info.tag}); pass ${field}, not ${provided[0]}.`);
|
|
569
|
-
};
|
|
570
|
-
const matchOption = (entry) => {
|
|
571
|
-
const match = info.options.find((option) => option.value === entry) ?? info.options.find((option) => option.label === entry.trim());
|
|
572
|
-
if (!match) {
|
|
573
|
-
const available = info.options.slice(0, 20).map((option) => `${JSON.stringify(option.label)} (value ${JSON.stringify(option.value)})`).join(", ");
|
|
574
|
-
throw new Error(`No option matches ${JSON.stringify(entry)}. Available: ${available}`);
|
|
575
|
-
}
|
|
576
|
-
return match.value;
|
|
577
|
-
};
|
|
578
|
-
try {
|
|
579
|
-
switch (info.kind) {
|
|
580
|
-
case "select": {
|
|
581
|
-
expectField("value");
|
|
582
|
-
await locator.selectOption(matchOption(input.value), { timeout: REF_ACTION_TIMEOUT_MS });
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
case "multiselect": {
|
|
586
|
-
expectField("values");
|
|
587
|
-
const entries = input.values;
|
|
588
|
-
if (!Array.isArray(entries) || entries.some((entry) => typeof entry !== "string")) {
|
|
589
|
-
throw new Error("form_input values must be an array of strings.");
|
|
590
|
-
}
|
|
591
|
-
await locator.selectOption(entries.map((entry) => matchOption(entry)), { timeout: REF_ACTION_TIMEOUT_MS });
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
case "checkable":
|
|
595
|
-
expectField("checked");
|
|
596
|
-
await locator.setChecked(input.checked === true, { timeout: REF_ACTION_TIMEOUT_MS });
|
|
597
|
-
break;
|
|
598
|
-
case "text":
|
|
599
|
-
case "contenteditable":
|
|
600
|
-
expectField("value");
|
|
601
|
-
await locator.fill(input.value, { timeout: REF_ACTION_TIMEOUT_MS });
|
|
602
|
-
break;
|
|
603
|
-
default:
|
|
604
|
-
throw new Error(
|
|
605
|
-
`Ref ${ref} (${info.tag}) does not accept direct value input. Click it with browser_mouse_click and type with browser_keyboard_type mode "keys".`
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
} catch (error) {
|
|
609
|
-
if (error instanceof Error && !/Timeout|exceeded|waiting for/i.test(error.message)) throw error;
|
|
610
|
-
throw refActionError(error, ref, "form_input");
|
|
611
|
-
}
|
|
612
|
-
return { tabId, ref, kind: info.kind };
|
|
613
|
-
}
|
|
614
|
-
case "keyboard": {
|
|
615
|
-
const keys = Array.isArray(input.keys) ? input.keys : [];
|
|
616
|
-
const modifiers = Array.isArray(input.modifiers) ? input.modifiers.filter((value) => typeof value === "string") : [];
|
|
617
|
-
for (const modifier of modifiers) await page.keyboard.down(modifier);
|
|
618
|
-
try {
|
|
619
|
-
for (const key of keys) {
|
|
620
|
-
if (typeof key !== "string") throw new Error("keyboard keys must all be strings.");
|
|
621
|
-
await page.keyboard.press(key);
|
|
622
|
-
}
|
|
623
|
-
} finally {
|
|
624
|
-
for (const modifier of modifiers.toReversed()) await page.keyboard.up(modifier);
|
|
625
|
-
}
|
|
626
|
-
return { tabId, keys, modifiers };
|
|
627
|
-
}
|
|
628
|
-
case "keyboard_type": {
|
|
629
|
-
if (typeof input.text !== "string") throw new Error("keyboard_type requires text.");
|
|
630
|
-
if (input.mode === "keys") {
|
|
631
|
-
if (input.text.length > MAX_TYPED_KEYS_CHARS) {
|
|
632
|
-
throw new Error(
|
|
633
|
-
`keyboard_type mode "keys" is limited to ${MAX_TYPED_KEYS_CHARS} characters per call; split the text or use the default insert mode.`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
const delay = clampInt(input.delayMs, 0, 100, 20);
|
|
637
|
-
if (input.text.length * delay > MAX_TYPED_KEYS_TOTAL_MS) {
|
|
638
|
-
throw new Error(`text.length x delayMs must stay at or below ${MAX_TYPED_KEYS_TOTAL_MS}ms; lower delayMs or split the text.`);
|
|
639
|
-
}
|
|
640
|
-
await page.keyboard.type(input.text, { delay });
|
|
641
|
-
return { tabId, characters: input.text.length, mode: "keys" };
|
|
642
|
-
}
|
|
643
|
-
await page.keyboard.insertText(input.text);
|
|
644
|
-
return { tabId, characters: input.text.length };
|
|
645
|
-
}
|
|
646
|
-
case "run_js": {
|
|
647
|
-
if (typeof input.code !== "string") throw new Error("run_js requires code.");
|
|
648
|
-
let evaluated;
|
|
649
|
-
try {
|
|
650
|
-
evaluated = await page.evaluate(
|
|
651
|
-
async ({ code, maxBytes, maxCount }) => {
|
|
652
|
-
const artifacts = [];
|
|
653
|
-
let totalBytes = 0;
|
|
654
|
-
const r5dCreateArtifact = (filename, content) => {
|
|
655
|
-
if (typeof filename !== "string" || !filename.trim()) {
|
|
656
|
-
throw new TypeError("r5dCreateArtifact(filename, content): filename must be a non-empty string.");
|
|
657
|
-
}
|
|
658
|
-
if (typeof content !== "string") {
|
|
659
|
-
throw new TypeError(
|
|
660
|
-
`r5dCreateArtifact(${JSON.stringify(filename)}, content): content must be a string. Use JSON.stringify(value) for objects.`
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
if (artifacts.length >= maxCount) {
|
|
664
|
-
throw new RangeError(`r5dCreateArtifact: at most ${maxCount} artifacts can be created in one browser_run_js call.`);
|
|
665
|
-
}
|
|
666
|
-
totalBytes += new TextEncoder().encode(content).length;
|
|
667
|
-
if (totalBytes > maxBytes) {
|
|
668
|
-
throw new RangeError(`r5dCreateArtifact: artifact content exceeded the ${maxBytes} byte budget for one browser_run_js call.`);
|
|
669
|
-
}
|
|
670
|
-
artifacts.push({ filename, content });
|
|
671
|
-
return filename;
|
|
672
|
-
};
|
|
673
|
-
const invoke = new Function("r5dCreateArtifact", `"use strict"; return (async () => {
|
|
674
|
-
${code}
|
|
675
|
-
})()`);
|
|
676
|
-
try {
|
|
677
|
-
return { result: await invoke(r5dCreateArtifact), artifacts };
|
|
678
|
-
} catch (error) {
|
|
679
|
-
if (artifacts.length === 0) throw error;
|
|
680
|
-
return { artifacts, failure: error instanceof Error ? error.message : String(error) };
|
|
681
|
-
}
|
|
682
|
-
},
|
|
683
|
-
{ code: input.code, maxBytes: MAX_RUN_JS_ARTIFACT_BYTES, maxCount: MAX_RUN_JS_ARTIFACT_COUNT }
|
|
684
|
-
);
|
|
685
|
-
} catch (error) {
|
|
686
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
687
|
-
if (isDestroyedContext(message)) {
|
|
688
|
-
throw new Error(
|
|
689
|
-
"The page navigated or reloaded while the script was running, destroying its JavaScript context. The script was NOT retried because it may have side effects \u2014 if part of it ran (clicks, submits), verify the page state with browser_read_page before repeating anything. Wait out navigations with browser_wait_for, or restructure the script so navigation-triggering actions come last."
|
|
690
|
-
);
|
|
691
|
-
}
|
|
692
|
-
throw new Error(stripCallLog(message));
|
|
693
|
-
}
|
|
694
|
-
const serialized = JSON.stringify(evaluated.result);
|
|
695
|
-
if (serialized && Buffer.byteLength(serialized) > MAX_RUN_JS_RESULT_BYTES) {
|
|
696
|
-
throw new Error("Browser JavaScript result exceeded the 1 MiB limit.");
|
|
697
|
-
}
|
|
698
|
-
return { tabId, result: evaluated.result, artifacts: evaluated.artifacts, failure: evaluated.failure };
|
|
699
|
-
}
|
|
700
|
-
default:
|
|
701
|
-
throw new Error(`Unsupported browser operation: ${action}`);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
706
|
-
0 && (module.exports = {
|
|
707
|
-
BrowserRuntime
|
|
708
|
-
});
|