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.
Files changed (44) hide show
  1. package/README.md +89 -667
  2. package/dist/actions.cjs +1073 -41
  3. package/dist/actions.d.cts +11 -3
  4. package/dist/actions.d.ts +11 -3
  5. package/dist/actions.mjs +1 -1
  6. package/dist/browser-ZCR6AA4D.mjs +11 -0
  7. package/dist/browser.cjs +1431 -62
  8. package/dist/browser.d.cts +4 -4
  9. package/dist/browser.d.ts +4 -4
  10. package/dist/browser.mjs +4 -4
  11. package/dist/cdp.cjs +5 -1
  12. package/dist/cdp.d.cts +1 -1
  13. package/dist/cdp.d.ts +1 -1
  14. package/dist/cdp.mjs +1 -1
  15. package/dist/{chunk-7NDR6V7S.mjs → chunk-6GBYX7C2.mjs} +1405 -528
  16. package/dist/{chunk-KIFB526Y.mjs → chunk-BVZALQT4.mjs} +5 -1
  17. package/dist/chunk-DTVRFXKI.mjs +35 -0
  18. package/dist/chunk-EZNZ72VA.mjs +563 -0
  19. package/dist/{chunk-SPSZZH22.mjs → chunk-LCNFBXB5.mjs} +9 -33
  20. package/dist/{chunk-IN5HPAPB.mjs → chunk-NNEHWWHL.mjs} +28 -10
  21. package/dist/chunk-TJ5B56NV.mjs +804 -0
  22. package/dist/{chunk-XMJABKCF.mjs → chunk-V3VLBQAM.mjs} +1073 -41
  23. package/dist/cli.mjs +2799 -1176
  24. package/dist/{client-Ck2nQksT.d.cts → client-B5QBRgIy.d.cts} +2 -0
  25. package/dist/{client-Ck2nQksT.d.ts → client-B5QBRgIy.d.ts} +2 -0
  26. package/dist/{client-3AFV2IAF.mjs → client-JWWZWO6L.mjs} +4 -2
  27. package/dist/index.cjs +1441 -52
  28. package/dist/index.d.cts +5 -5
  29. package/dist/index.d.ts +5 -5
  30. package/dist/index.mjs +19 -7
  31. package/dist/page-IUUTJ3SW.mjs +7 -0
  32. package/dist/providers.cjs +637 -2
  33. package/dist/providers.d.cts +2 -2
  34. package/dist/providers.d.ts +2 -2
  35. package/dist/providers.mjs +17 -3
  36. package/dist/{types-CjT0vClo.d.ts → types-BflRmiDz.d.cts} +17 -3
  37. package/dist/{types-BSoh5v1Y.d.cts → types-BzM-IfsL.d.ts} +17 -3
  38. package/dist/types-DeVSWhXj.d.cts +142 -0
  39. package/dist/types-DeVSWhXj.d.ts +142 -0
  40. package/package.json +1 -1
  41. package/dist/browser-LZTEHUDI.mjs +0 -9
  42. package/dist/chunk-BRAFQUMG.mjs +0 -229
  43. package/dist/types--wXNHUwt.d.cts +0 -56
  44. 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
+ };