browser-pilot 0.0.14 → 0.0.16
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 +89 -667
- package/dist/actions.cjs +1073 -41
- package/dist/actions.d.cts +11 -3
- package/dist/actions.d.ts +11 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser-ZCR6AA4D.mjs +11 -0
- package/dist/browser.cjs +1431 -62
- package/dist/browser.d.cts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.mjs +4 -4
- package/dist/cdp.cjs +5 -1
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +1 -1
- package/dist/{chunk-7NDR6V7S.mjs → chunk-6GBYX7C2.mjs} +1405 -528
- package/dist/{chunk-KIFB526Y.mjs → chunk-BVZALQT4.mjs} +5 -1
- package/dist/chunk-DTVRFXKI.mjs +35 -0
- package/dist/chunk-EZNZ72VA.mjs +563 -0
- package/dist/{chunk-SPSZZH22.mjs → chunk-LCNFBXB5.mjs} +9 -33
- package/dist/{chunk-IN5HPAPB.mjs → chunk-NNEHWWHL.mjs} +28 -10
- package/dist/chunk-TJ5B56NV.mjs +804 -0
- package/dist/{chunk-XMJABKCF.mjs → chunk-V3VLBQAM.mjs} +1073 -41
- package/dist/cli.mjs +2799 -1176
- package/dist/{client-Ck2nQksT.d.cts → client-B5QBRgIy.d.cts} +2 -0
- package/dist/{client-Ck2nQksT.d.ts → client-B5QBRgIy.d.ts} +2 -0
- package/dist/{client-3AFV2IAF.mjs → client-JWWZWO6L.mjs} +4 -2
- package/dist/index.cjs +1441 -52
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +19 -7
- package/dist/page-IUUTJ3SW.mjs +7 -0
- package/dist/providers.cjs +637 -2
- package/dist/providers.d.cts +2 -2
- package/dist/providers.d.ts +2 -2
- package/dist/providers.mjs +17 -3
- package/dist/{types-CjT0vClo.d.ts → types-BflRmiDz.d.cts} +17 -3
- package/dist/{types-BSoh5v1Y.d.cts → types-BzM-IfsL.d.ts} +17 -3
- package/dist/types-DeVSWhXj.d.cts +142 -0
- package/dist/types-DeVSWhXj.d.ts +142 -0
- package/package.json +1 -1
- package/dist/browser-LZTEHUDI.mjs +0 -9
- package/dist/chunk-BRAFQUMG.mjs +0 -229
- package/dist/types--wXNHUwt.d.cts +0 -56
- package/dist/types--wXNHUwt.d.ts +0 -56
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCDPClient
|
|
3
|
+
} from "./chunk-LCNFBXB5.mjs";
|
|
4
|
+
import {
|
|
5
|
+
Page
|
|
6
|
+
} from "./chunk-6GBYX7C2.mjs";
|
|
7
|
+
|
|
8
|
+
// src/providers/browserbase.ts
|
|
9
|
+
var BrowserBaseProvider = class {
|
|
10
|
+
name = "browserbase";
|
|
11
|
+
apiKey;
|
|
12
|
+
projectId;
|
|
13
|
+
baseUrl;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.apiKey = options.apiKey;
|
|
16
|
+
this.projectId = options.projectId;
|
|
17
|
+
this.baseUrl = options.baseUrl ?? "https://api.browserbase.com";
|
|
18
|
+
}
|
|
19
|
+
async createSession(options = {}) {
|
|
20
|
+
const response = await fetch(`${this.baseUrl}/v1/sessions`, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
"X-BB-API-Key": this.apiKey,
|
|
24
|
+
"Content-Type": "application/json"
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
projectId: this.projectId,
|
|
28
|
+
browserSettings: {
|
|
29
|
+
viewport: options.width && options.height ? {
|
|
30
|
+
width: options.width,
|
|
31
|
+
height: options.height
|
|
32
|
+
} : void 0
|
|
33
|
+
},
|
|
34
|
+
...options
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
throw new Error(`BrowserBase createSession failed: ${response.status} ${text}`);
|
|
40
|
+
}
|
|
41
|
+
const session = await response.json();
|
|
42
|
+
const connectResponse = await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
|
|
43
|
+
headers: {
|
|
44
|
+
"X-BB-API-Key": this.apiKey
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (!connectResponse.ok) {
|
|
48
|
+
throw new Error(`BrowserBase getSession failed: ${connectResponse.status}`);
|
|
49
|
+
}
|
|
50
|
+
const sessionDetails = await connectResponse.json();
|
|
51
|
+
if (!sessionDetails.connectUrl) {
|
|
52
|
+
throw new Error("BrowserBase session does not have a connectUrl");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
wsUrl: sessionDetails.connectUrl,
|
|
56
|
+
sessionId: session.id,
|
|
57
|
+
metadata: {
|
|
58
|
+
debugUrl: sessionDetails.debugUrl,
|
|
59
|
+
projectId: this.projectId,
|
|
60
|
+
status: sessionDetails.status
|
|
61
|
+
},
|
|
62
|
+
close: async () => {
|
|
63
|
+
await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
|
|
64
|
+
method: "DELETE",
|
|
65
|
+
headers: {
|
|
66
|
+
"X-BB-API-Key": this.apiKey
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async resumeSession(sessionId) {
|
|
73
|
+
const response = await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
|
|
74
|
+
headers: {
|
|
75
|
+
"X-BB-API-Key": this.apiKey
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`BrowserBase resumeSession failed: ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
const session = await response.json();
|
|
82
|
+
if (!session.connectUrl) {
|
|
83
|
+
throw new Error("BrowserBase session does not have a connectUrl (may be closed)");
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
wsUrl: session.connectUrl,
|
|
87
|
+
sessionId: session.id,
|
|
88
|
+
metadata: {
|
|
89
|
+
debugUrl: session.debugUrl,
|
|
90
|
+
projectId: this.projectId,
|
|
91
|
+
status: session.status
|
|
92
|
+
},
|
|
93
|
+
close: async () => {
|
|
94
|
+
await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
|
|
95
|
+
method: "DELETE",
|
|
96
|
+
headers: {
|
|
97
|
+
"X-BB-API-Key": this.apiKey
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/providers/browserless.ts
|
|
106
|
+
var BrowserlessProvider = class {
|
|
107
|
+
name = "browserless";
|
|
108
|
+
token;
|
|
109
|
+
baseUrl;
|
|
110
|
+
constructor(options) {
|
|
111
|
+
this.token = options.token;
|
|
112
|
+
this.baseUrl = options.baseUrl ?? "wss://chrome.browserless.io";
|
|
113
|
+
}
|
|
114
|
+
async createSession(options = {}) {
|
|
115
|
+
const params = new URLSearchParams({
|
|
116
|
+
token: this.token
|
|
117
|
+
});
|
|
118
|
+
if (options.width && options.height) {
|
|
119
|
+
params.set("--window-size", `${options.width},${options.height}`);
|
|
120
|
+
}
|
|
121
|
+
if (options.proxy?.server) {
|
|
122
|
+
params.set("--proxy-server", options.proxy.server);
|
|
123
|
+
}
|
|
124
|
+
const wsUrl = `${this.baseUrl}?${params.toString()}`;
|
|
125
|
+
return {
|
|
126
|
+
wsUrl,
|
|
127
|
+
metadata: {
|
|
128
|
+
provider: "browserless"
|
|
129
|
+
},
|
|
130
|
+
close: async () => {
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Browserless doesn't support session resumption in the same way
|
|
135
|
+
// Each connection is a fresh browser instance
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/providers/generic.ts
|
|
139
|
+
function sleep(ms) {
|
|
140
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
141
|
+
}
|
|
142
|
+
async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
|
|
143
|
+
const protocol = host.includes("://") ? "" : "http://";
|
|
144
|
+
const attempts = options.attempts ?? 1;
|
|
145
|
+
let delayMs = options.initialDelayMs ?? 50;
|
|
146
|
+
const maxDelayMs = options.maxDelayMs ?? 250;
|
|
147
|
+
let lastError;
|
|
148
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(`${protocol}${host}${path}`);
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
return await response.json();
|
|
153
|
+
}
|
|
154
|
+
lastError = new Error(`${errorPrefix}: ${response.status}`);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
lastError = new Error(
|
|
157
|
+
`${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (attempt < attempts) {
|
|
161
|
+
await sleep(delayMs);
|
|
162
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw lastError ?? new Error(errorPrefix);
|
|
166
|
+
}
|
|
167
|
+
var GenericProvider = class {
|
|
168
|
+
name = "generic";
|
|
169
|
+
wsUrl;
|
|
170
|
+
constructor(options) {
|
|
171
|
+
this.wsUrl = options.wsUrl;
|
|
172
|
+
}
|
|
173
|
+
async createSession(_options = {}) {
|
|
174
|
+
return {
|
|
175
|
+
wsUrl: this.wsUrl,
|
|
176
|
+
metadata: {
|
|
177
|
+
provider: "generic"
|
|
178
|
+
},
|
|
179
|
+
close: async () => {
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
async function getBrowserWebSocketUrl(host = "localhost:9222") {
|
|
185
|
+
const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
|
|
186
|
+
attempts: 10,
|
|
187
|
+
initialDelayMs: 50,
|
|
188
|
+
maxDelayMs: 250
|
|
189
|
+
});
|
|
190
|
+
return info.webSocketDebuggerUrl;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/providers/local-discovery.ts
|
|
194
|
+
var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
|
|
195
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
|
|
196
|
+
var DevToolsActivePortParseError = class extends Error {
|
|
197
|
+
constructor(message, reason) {
|
|
198
|
+
super(message);
|
|
199
|
+
this.reason = reason;
|
|
200
|
+
this.name = "DevToolsActivePortParseError";
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function getRuntimeEnv() {
|
|
204
|
+
if (typeof process === "undefined") {
|
|
205
|
+
return {};
|
|
206
|
+
}
|
|
207
|
+
return process.env;
|
|
208
|
+
}
|
|
209
|
+
function getRuntimePlatform() {
|
|
210
|
+
if (typeof process === "undefined") {
|
|
211
|
+
return void 0;
|
|
212
|
+
}
|
|
213
|
+
return process.platform;
|
|
214
|
+
}
|
|
215
|
+
function normalizePlatform(platform) {
|
|
216
|
+
if (platform === "darwin" || platform === "linux" || platform === "win32") {
|
|
217
|
+
return platform;
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
|
|
220
|
+
}
|
|
221
|
+
function trimTrailingSeparator(path) {
|
|
222
|
+
return path.replace(/[\\/]+$/, "");
|
|
223
|
+
}
|
|
224
|
+
function joinPath(platform, ...parts) {
|
|
225
|
+
const separator = platform === "win32" ? "\\" : "/";
|
|
226
|
+
const cleaned = parts.map((part, index) => {
|
|
227
|
+
if (index === 0) return trimTrailingSeparator(part);
|
|
228
|
+
return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
229
|
+
}).filter((part) => part.length > 0);
|
|
230
|
+
return cleaned.join(separator);
|
|
231
|
+
}
|
|
232
|
+
function resolveHomeDir(platform, env, explicitHomeDir) {
|
|
233
|
+
if (explicitHomeDir) {
|
|
234
|
+
return explicitHomeDir;
|
|
235
|
+
}
|
|
236
|
+
if (platform === "win32") {
|
|
237
|
+
return env["USERPROFILE"] ?? env["HOME"] ?? "";
|
|
238
|
+
}
|
|
239
|
+
return env["HOME"] ?? env["USERPROFILE"] ?? "";
|
|
240
|
+
}
|
|
241
|
+
function toFileFailure(target, error) {
|
|
242
|
+
const errno = error?.code;
|
|
243
|
+
if (errno === "ENOENT") {
|
|
244
|
+
return {
|
|
245
|
+
...target,
|
|
246
|
+
reason: "missing-file",
|
|
247
|
+
message: `DevToolsActivePort not found at ${target.portFile}`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
...target,
|
|
252
|
+
reason: "unreadable-file",
|
|
253
|
+
message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function toProbeFailure(target, wsUrl, error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
const lowerMessage = message.toLowerCase();
|
|
259
|
+
let reason = "connection-error";
|
|
260
|
+
if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
|
|
261
|
+
reason = "connection-refused";
|
|
262
|
+
} else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
|
|
263
|
+
reason = "connection-timeout";
|
|
264
|
+
} else if (lowerMessage.includes("closed")) {
|
|
265
|
+
reason = "unexpected-close";
|
|
266
|
+
} else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
|
|
267
|
+
reason = "cdp-error";
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
...target,
|
|
271
|
+
wsUrl,
|
|
272
|
+
reason,
|
|
273
|
+
message
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function readTextFile(path) {
|
|
277
|
+
const fs = await import("fs/promises");
|
|
278
|
+
return fs.readFile(path, "utf-8");
|
|
279
|
+
}
|
|
280
|
+
async function probeBrowserWebSocket(wsUrl, timeoutMs) {
|
|
281
|
+
let client;
|
|
282
|
+
try {
|
|
283
|
+
client = await createCDPClient(wsUrl, { timeout: timeoutMs });
|
|
284
|
+
const version = await client.send("Browser.getVersion", void 0, null);
|
|
285
|
+
return { browserVersion: version.product };
|
|
286
|
+
} finally {
|
|
287
|
+
await client?.close().catch(() => {
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
var defaultDependencies = {
|
|
292
|
+
readTextFile,
|
|
293
|
+
probeBrowserWebSocket,
|
|
294
|
+
getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
|
|
295
|
+
};
|
|
296
|
+
function resolveChromeUserDataDirs(options = {}) {
|
|
297
|
+
const env = options.env ?? getRuntimeEnv();
|
|
298
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
299
|
+
const homeDir = resolveHomeDir(platform, env, options.homeDir);
|
|
300
|
+
if (!homeDir) {
|
|
301
|
+
throw new Error("Could not determine home directory for local Chrome discovery");
|
|
302
|
+
}
|
|
303
|
+
switch (platform) {
|
|
304
|
+
case "darwin": {
|
|
305
|
+
const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
|
|
306
|
+
return {
|
|
307
|
+
stable: joinPath(platform, base, "Chrome"),
|
|
308
|
+
beta: joinPath(platform, base, "Chrome Beta"),
|
|
309
|
+
dev: joinPath(platform, base, "Chrome Dev"),
|
|
310
|
+
canary: joinPath(platform, base, "Chrome Canary")
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
case "linux": {
|
|
314
|
+
const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
|
|
315
|
+
return {
|
|
316
|
+
stable: joinPath(platform, configHome, "google-chrome"),
|
|
317
|
+
beta: joinPath(platform, configHome, "google-chrome-beta"),
|
|
318
|
+
dev: joinPath(platform, configHome, "google-chrome-dev"),
|
|
319
|
+
canary: joinPath(platform, configHome, "google-chrome-canary")
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
case "win32": {
|
|
323
|
+
const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
|
|
324
|
+
const base = joinPath(platform, localAppData, "Google");
|
|
325
|
+
return {
|
|
326
|
+
stable: joinPath(platform, base, "Chrome", "User Data"),
|
|
327
|
+
beta: joinPath(platform, base, "Chrome Beta", "User Data"),
|
|
328
|
+
dev: joinPath(platform, base, "Chrome Dev", "User Data"),
|
|
329
|
+
canary: joinPath(platform, base, "Chrome SxS", "User Data")
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
|
|
334
|
+
}
|
|
335
|
+
function buildLocalBrowserScanTargets(options = {}) {
|
|
336
|
+
const env = options.env ?? getRuntimeEnv();
|
|
337
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
338
|
+
if (options.userDataDir) {
|
|
339
|
+
return [
|
|
340
|
+
{
|
|
341
|
+
channel: options.channel ?? "custom",
|
|
342
|
+
userDataDir: options.userDataDir,
|
|
343
|
+
portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
|
|
344
|
+
}
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
const dirs = resolveChromeUserDataDirs({
|
|
348
|
+
platform,
|
|
349
|
+
env,
|
|
350
|
+
homeDir: options.homeDir
|
|
351
|
+
});
|
|
352
|
+
const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
|
|
353
|
+
return channels.map((channel) => ({
|
|
354
|
+
channel,
|
|
355
|
+
userDataDir: dirs[channel],
|
|
356
|
+
portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
function parseDevToolsActivePortFile(content) {
|
|
360
|
+
const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
361
|
+
if (lines.length !== 2) {
|
|
362
|
+
throw new DevToolsActivePortParseError(
|
|
363
|
+
`Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
|
|
364
|
+
"malformed-file"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const portText = lines[0];
|
|
368
|
+
const browserPath = lines[1];
|
|
369
|
+
const port = Number.parseInt(portText, 10);
|
|
370
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
371
|
+
throw new DevToolsActivePortParseError(
|
|
372
|
+
`Invalid DevToolsActivePort port: ${portText}`,
|
|
373
|
+
"invalid-port"
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
|
|
377
|
+
throw new DevToolsActivePortParseError(
|
|
378
|
+
`Invalid DevToolsActivePort browser path: ${browserPath}`,
|
|
379
|
+
"invalid-path"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
port,
|
|
384
|
+
browserPath,
|
|
385
|
+
wsUrl: `ws://127.0.0.1:${port}${browserPath}`
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async function inspectScanTarget(target, options, deps) {
|
|
389
|
+
let content;
|
|
390
|
+
try {
|
|
391
|
+
content = await deps.readTextFile(target.portFile);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return { kind: "failure", failure: toFileFailure(target, error) };
|
|
394
|
+
}
|
|
395
|
+
let parsed;
|
|
396
|
+
try {
|
|
397
|
+
parsed = parseDevToolsActivePortFile(content);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error instanceof DevToolsActivePortParseError) {
|
|
400
|
+
return {
|
|
401
|
+
kind: "failure",
|
|
402
|
+
failure: {
|
|
403
|
+
...target,
|
|
404
|
+
reason: error.reason,
|
|
405
|
+
message: error.message
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const probe = await deps.probeBrowserWebSocket(
|
|
413
|
+
parsed.wsUrl,
|
|
414
|
+
options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
|
|
415
|
+
);
|
|
416
|
+
return {
|
|
417
|
+
kind: "candidate",
|
|
418
|
+
candidate: {
|
|
419
|
+
...target,
|
|
420
|
+
port: parsed.port,
|
|
421
|
+
browserPath: parsed.browserPath,
|
|
422
|
+
wsUrl: parsed.wsUrl,
|
|
423
|
+
browserVersion: probe.browserVersion
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return {
|
|
428
|
+
kind: "failure",
|
|
429
|
+
failure: toProbeFailure(target, parsed.wsUrl, error)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
|
|
434
|
+
const scanTargets = buildLocalBrowserScanTargets(options);
|
|
435
|
+
const outcomes = await Promise.all(
|
|
436
|
+
scanTargets.map((target) => inspectScanTarget(target, options, deps))
|
|
437
|
+
);
|
|
438
|
+
const candidates = [];
|
|
439
|
+
const failures = [];
|
|
440
|
+
for (const outcome of outcomes) {
|
|
441
|
+
if (outcome.kind === "candidate") {
|
|
442
|
+
candidates.push(outcome.candidate);
|
|
443
|
+
} else {
|
|
444
|
+
failures.push(outcome.failure);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { candidates, failures };
|
|
448
|
+
}
|
|
449
|
+
var BrowserEndpointResolutionError = class extends Error {
|
|
450
|
+
constructor(code, message, details = {}) {
|
|
451
|
+
super(message);
|
|
452
|
+
this.code = code;
|
|
453
|
+
this.details = details;
|
|
454
|
+
}
|
|
455
|
+
name = "BrowserEndpointResolutionError";
|
|
456
|
+
};
|
|
457
|
+
async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
|
|
458
|
+
if (options.explicitWsUrl) {
|
|
459
|
+
return {
|
|
460
|
+
wsUrl: options.explicitWsUrl,
|
|
461
|
+
source: "explicit-ws"
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
let localDiscovery;
|
|
465
|
+
if (options.allowLocalDiscovery ?? true) {
|
|
466
|
+
localDiscovery = await discoverLocalBrowsers(options, deps);
|
|
467
|
+
if (localDiscovery.candidates.length === 1) {
|
|
468
|
+
const candidate = localDiscovery.candidates[0];
|
|
469
|
+
return {
|
|
470
|
+
wsUrl: candidate.wsUrl,
|
|
471
|
+
source: "devtools-active-port",
|
|
472
|
+
channel: candidate.channel,
|
|
473
|
+
userDataDir: candidate.userDataDir
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (localDiscovery.candidates.length > 1) {
|
|
477
|
+
throw new BrowserEndpointResolutionError(
|
|
478
|
+
"multiple-local-browsers",
|
|
479
|
+
"Multiple local Chrome profiles are available for auto-discovery",
|
|
480
|
+
{
|
|
481
|
+
candidates: localDiscovery.candidates,
|
|
482
|
+
failures: localDiscovery.failures
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (options.allowLegacyHostFallback ?? true) {
|
|
488
|
+
const legacyHost = options.legacyHost ?? "localhost:9222";
|
|
489
|
+
try {
|
|
490
|
+
return {
|
|
491
|
+
wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
|
|
492
|
+
source: "json-version"
|
|
493
|
+
};
|
|
494
|
+
} catch (error) {
|
|
495
|
+
throw new BrowserEndpointResolutionError(
|
|
496
|
+
"browser-not-found",
|
|
497
|
+
"Could not resolve a browser endpoint",
|
|
498
|
+
{
|
|
499
|
+
candidates: localDiscovery?.candidates,
|
|
500
|
+
failures: localDiscovery?.failures,
|
|
501
|
+
legacyError: error instanceof Error ? error : new Error(String(error)),
|
|
502
|
+
legacyHost
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
throw new BrowserEndpointResolutionError(
|
|
508
|
+
"browser-not-found",
|
|
509
|
+
"Could not resolve a browser endpoint",
|
|
510
|
+
{
|
|
511
|
+
candidates: localDiscovery?.candidates,
|
|
512
|
+
failures: localDiscovery?.failures
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/providers/index.ts
|
|
518
|
+
function createProvider(options) {
|
|
519
|
+
switch (options.provider) {
|
|
520
|
+
case "browserbase":
|
|
521
|
+
if (!options.apiKey) {
|
|
522
|
+
throw new Error("BrowserBase provider requires apiKey");
|
|
523
|
+
}
|
|
524
|
+
if (!options.projectId) {
|
|
525
|
+
throw new Error("BrowserBase provider requires projectId");
|
|
526
|
+
}
|
|
527
|
+
return new BrowserBaseProvider({
|
|
528
|
+
apiKey: options.apiKey,
|
|
529
|
+
projectId: options.projectId
|
|
530
|
+
});
|
|
531
|
+
case "browserless":
|
|
532
|
+
if (!options.apiKey) {
|
|
533
|
+
throw new Error("Browserless provider requires apiKey (token)");
|
|
534
|
+
}
|
|
535
|
+
return new BrowserlessProvider({
|
|
536
|
+
token: options.apiKey
|
|
537
|
+
});
|
|
538
|
+
case "generic":
|
|
539
|
+
if (!options.wsUrl) {
|
|
540
|
+
throw new Error("Generic provider requires wsUrl");
|
|
541
|
+
}
|
|
542
|
+
return new GenericProvider({
|
|
543
|
+
wsUrl: options.wsUrl
|
|
544
|
+
});
|
|
545
|
+
default:
|
|
546
|
+
throw new Error(`Unknown provider: ${options.provider}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/browser/browser.ts
|
|
551
|
+
function scoreTarget(t) {
|
|
552
|
+
let score = 0;
|
|
553
|
+
if (t.url.startsWith("http://") || t.url.startsWith("https://")) score += 10;
|
|
554
|
+
if (t.url.startsWith("chrome://")) score -= 20;
|
|
555
|
+
if (t.url.startsWith("chrome-extension://")) score -= 15;
|
|
556
|
+
if (t.url.startsWith("devtools://")) score -= 25;
|
|
557
|
+
if (t.url === "about:blank") score -= 5;
|
|
558
|
+
if (!t.attached) score += 3;
|
|
559
|
+
if (t.title && t.title.length > 0) score += 2;
|
|
560
|
+
return score;
|
|
561
|
+
}
|
|
562
|
+
function pickBestTarget(targets) {
|
|
563
|
+
if (targets.length === 0) return void 0;
|
|
564
|
+
const sorted = [...targets].sort((a, b) => scoreTarget(b) - scoreTarget(a));
|
|
565
|
+
return sorted[0].targetId;
|
|
566
|
+
}
|
|
567
|
+
var Browser = class _Browser {
|
|
568
|
+
cdp;
|
|
569
|
+
providerSession;
|
|
570
|
+
pages = /* @__PURE__ */ new Map();
|
|
571
|
+
pageCounter = 0;
|
|
572
|
+
constructor(cdp, _provider, providerSession, _options) {
|
|
573
|
+
this.cdp = cdp;
|
|
574
|
+
this.providerSession = providerSession;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Create a Browser from an existing CDPClient (used by daemon fast-path).
|
|
578
|
+
* The caller is responsible for the CDP connection lifecycle.
|
|
579
|
+
*/
|
|
580
|
+
static fromCDP(cdp, sessionInfo) {
|
|
581
|
+
const providerSession = {
|
|
582
|
+
wsUrl: sessionInfo.wsUrl,
|
|
583
|
+
sessionId: sessionInfo.sessionId,
|
|
584
|
+
async close() {
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
const provider = {
|
|
588
|
+
name: sessionInfo.provider ?? "daemon",
|
|
589
|
+
async createSession() {
|
|
590
|
+
return providerSession;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
return new _Browser(cdp, provider, providerSession, { provider: "generic" });
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Connect to a browser instance
|
|
597
|
+
*/
|
|
598
|
+
static async connect(options) {
|
|
599
|
+
let connectOptions = options;
|
|
600
|
+
if (options.provider === "generic" && !options.wsUrl) {
|
|
601
|
+
const endpoint = await resolveBrowserEndpoint({
|
|
602
|
+
channel: options.channel,
|
|
603
|
+
userDataDir: options.userDataDir,
|
|
604
|
+
allowLocalDiscovery: true,
|
|
605
|
+
allowLegacyHostFallback: true
|
|
606
|
+
});
|
|
607
|
+
connectOptions = {
|
|
608
|
+
...options,
|
|
609
|
+
wsUrl: endpoint.wsUrl
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const provider = createProvider(connectOptions);
|
|
613
|
+
const session = await provider.createSession(connectOptions.session);
|
|
614
|
+
const cdp = await createCDPClient(session.wsUrl, {
|
|
615
|
+
debug: connectOptions.debug,
|
|
616
|
+
timeout: connectOptions.timeout
|
|
617
|
+
});
|
|
618
|
+
return new _Browser(cdp, provider, session, connectOptions);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Get or create a page by name.
|
|
622
|
+
* If no name is provided, returns the first available page or creates a new one.
|
|
623
|
+
*
|
|
624
|
+
* Target selection heuristics (when no targetId is specified):
|
|
625
|
+
* - Prefer http/https URLs over chrome://, devtools://, about:blank
|
|
626
|
+
* - Prefer unattached targets (not already controlled by another client)
|
|
627
|
+
* - Filter by targetUrl if provided
|
|
628
|
+
*/
|
|
629
|
+
async page(name, options) {
|
|
630
|
+
const pageName = name ?? "default";
|
|
631
|
+
const cached = this.pages.get(pageName);
|
|
632
|
+
if (cached) return cached;
|
|
633
|
+
const targets = await this.cdp.send(
|
|
634
|
+
"Target.getTargets",
|
|
635
|
+
void 0,
|
|
636
|
+
null
|
|
637
|
+
);
|
|
638
|
+
let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
639
|
+
if (options?.targetUrl) {
|
|
640
|
+
const urlFilter = options.targetUrl;
|
|
641
|
+
const filtered = pageTargets.filter((t) => t.url.includes(urlFilter));
|
|
642
|
+
if (filtered.length > 0) {
|
|
643
|
+
pageTargets = filtered;
|
|
644
|
+
} else {
|
|
645
|
+
console.warn(
|
|
646
|
+
`[browser-pilot] No targets match URL filter "${urlFilter}", falling back to all page targets`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
let targetId;
|
|
651
|
+
if (options?.targetId) {
|
|
652
|
+
const targetExists = targets.targetInfos.some(
|
|
653
|
+
(t) => t.type === "page" && t.targetId === options.targetId
|
|
654
|
+
);
|
|
655
|
+
if (targetExists) {
|
|
656
|
+
targetId = options.targetId;
|
|
657
|
+
} else {
|
|
658
|
+
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
659
|
+
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
|
|
660
|
+
"Target.createTarget",
|
|
661
|
+
{
|
|
662
|
+
url: "about:blank"
|
|
663
|
+
},
|
|
664
|
+
null
|
|
665
|
+
)).targetId;
|
|
666
|
+
}
|
|
667
|
+
} else if (pageTargets.length > 0) {
|
|
668
|
+
targetId = pickBestTarget(pageTargets);
|
|
669
|
+
} else {
|
|
670
|
+
const result = await this.cdp.send(
|
|
671
|
+
"Target.createTarget",
|
|
672
|
+
{
|
|
673
|
+
url: "about:blank"
|
|
674
|
+
},
|
|
675
|
+
null
|
|
676
|
+
);
|
|
677
|
+
targetId = result.targetId;
|
|
678
|
+
}
|
|
679
|
+
await this.cdp.attachToTarget(targetId);
|
|
680
|
+
const page = new Page(this.cdp, targetId);
|
|
681
|
+
await page.init();
|
|
682
|
+
const minViewport = options?.minViewport !== void 0 ? options.minViewport : { width: 200, height: 200 };
|
|
683
|
+
if (minViewport !== false) {
|
|
684
|
+
try {
|
|
685
|
+
const viewport = await page.evaluate(
|
|
686
|
+
"({ w: window.innerWidth, h: window.innerHeight })"
|
|
687
|
+
);
|
|
688
|
+
if (viewport.w < minViewport.width || viewport.h < minViewport.height) {
|
|
689
|
+
console.warn(
|
|
690
|
+
`[browser-pilot] Attached target has small viewport (${viewport.w}x${viewport.h}). Applying default viewport override (1280x720). Use { minViewport: false } to disable this check.`
|
|
691
|
+
);
|
|
692
|
+
await page.setViewport({ width: 1280, height: 720 });
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
this.pages.set(pageName, page);
|
|
698
|
+
return page;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Create a new page (tab)
|
|
702
|
+
*/
|
|
703
|
+
async newPage(url = "about:blank") {
|
|
704
|
+
const result = await this.cdp.send(
|
|
705
|
+
"Target.createTarget",
|
|
706
|
+
{
|
|
707
|
+
url
|
|
708
|
+
},
|
|
709
|
+
null
|
|
710
|
+
);
|
|
711
|
+
await this.cdp.attachToTarget(result.targetId);
|
|
712
|
+
const page = new Page(this.cdp, result.targetId);
|
|
713
|
+
await page.init();
|
|
714
|
+
const name = `page-${++this.pageCounter}`;
|
|
715
|
+
this.pages.set(name, page);
|
|
716
|
+
return page;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Close a page by name
|
|
720
|
+
*/
|
|
721
|
+
async closePage(name) {
|
|
722
|
+
const page = this.pages.get(name);
|
|
723
|
+
if (!page) return;
|
|
724
|
+
const targetId = page.targetId;
|
|
725
|
+
await this.cdp.send("Target.closeTarget", { targetId }, null);
|
|
726
|
+
this.pages.delete(name);
|
|
727
|
+
const deadline = Date.now() + 5e3;
|
|
728
|
+
while (Date.now() < deadline) {
|
|
729
|
+
const { targetInfos } = await this.cdp.send(
|
|
730
|
+
"Target.getTargets",
|
|
731
|
+
void 0,
|
|
732
|
+
null
|
|
733
|
+
);
|
|
734
|
+
if (!targetInfos.some((t) => t.targetId === targetId)) return;
|
|
735
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* List all page targets in the connected browser.
|
|
740
|
+
*/
|
|
741
|
+
async listTargets() {
|
|
742
|
+
const { targetInfos } = await this.cdp.send(
|
|
743
|
+
"Target.getTargets",
|
|
744
|
+
void 0,
|
|
745
|
+
null
|
|
746
|
+
);
|
|
747
|
+
return targetInfos.filter((target) => target.type === "page");
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Get the WebSocket URL for this browser connection
|
|
751
|
+
*/
|
|
752
|
+
get wsUrl() {
|
|
753
|
+
return this.providerSession.wsUrl;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Get the provider session ID (for resumption)
|
|
757
|
+
*/
|
|
758
|
+
get sessionId() {
|
|
759
|
+
return this.providerSession.sessionId;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Get provider metadata
|
|
763
|
+
*/
|
|
764
|
+
get metadata() {
|
|
765
|
+
return this.providerSession.metadata;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Check if connected
|
|
769
|
+
*/
|
|
770
|
+
get isConnected() {
|
|
771
|
+
return this.cdp.isConnected;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Disconnect from the browser (keeps provider session alive for reconnection)
|
|
775
|
+
*/
|
|
776
|
+
async disconnect() {
|
|
777
|
+
this.pages.clear();
|
|
778
|
+
await this.cdp.close();
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Close the browser session completely
|
|
782
|
+
*/
|
|
783
|
+
async close() {
|
|
784
|
+
this.pages.clear();
|
|
785
|
+
await this.cdp.close();
|
|
786
|
+
await this.providerSession.close();
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Get the underlying CDP client (for advanced usage)
|
|
790
|
+
*/
|
|
791
|
+
get cdpClient() {
|
|
792
|
+
return this.cdp;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
function connect(options) {
|
|
796
|
+
return Browser.connect(options);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export {
|
|
800
|
+
BrowserEndpointResolutionError,
|
|
801
|
+
resolveBrowserEndpoint,
|
|
802
|
+
Browser,
|
|
803
|
+
connect
|
|
804
|
+
};
|