browser-pilot 0.0.15 → 0.0.17

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 (37) hide show
  1. package/README.md +38 -3
  2. package/dist/actions.cjs +848 -105
  3. package/dist/actions.d.cts +101 -4
  4. package/dist/actions.d.ts +101 -4
  5. package/dist/actions.mjs +17 -1
  6. package/dist/{browser-MEWT75IB.mjs → browser-4ZHNAQR5.mjs} +2 -2
  7. package/dist/browser.cjs +1684 -130
  8. package/dist/browser.d.cts +230 -6
  9. package/dist/browser.d.ts +230 -6
  10. package/dist/browser.mjs +37 -5
  11. package/dist/chunk-EZNZ72VA.mjs +563 -0
  12. package/dist/{chunk-ZAXQ5OTV.mjs → chunk-FEEGNSHB.mjs} +606 -12
  13. package/dist/{chunk-WPNW23CE.mjs → chunk-IRLHCVNH.mjs} +345 -7
  14. package/dist/chunk-MIJ7UIKB.mjs +96 -0
  15. package/dist/{chunk-USYSHCI3.mjs → chunk-MRY3HRFJ.mjs} +841 -370
  16. package/dist/chunk-OIHU7OFY.mjs +91 -0
  17. package/dist/{chunk-7YVCOL2W.mjs → chunk-ZDODXEBD.mjs} +637 -105
  18. package/dist/cli.mjs +1280 -549
  19. package/dist/combobox-RAKBA2BW.mjs +6 -0
  20. package/dist/index.cjs +1976 -144
  21. package/dist/index.d.cts +57 -6
  22. package/dist/index.d.ts +57 -6
  23. package/dist/index.mjs +206 -7
  24. package/dist/{page-XPS6IC6V.mjs → page-SD64DY3F.mjs} +1 -1
  25. package/dist/providers.cjs +637 -2
  26. package/dist/providers.d.cts +2 -2
  27. package/dist/providers.d.ts +2 -2
  28. package/dist/providers.mjs +17 -3
  29. package/dist/{types-Cvvf0oGu.d.ts → types-B_v62K7C.d.ts} +147 -3
  30. package/dist/types-DeVSWhXj.d.cts +142 -0
  31. package/dist/types-DeVSWhXj.d.ts +142 -0
  32. package/dist/{types-C9ySEdOX.d.cts → types-Yuybzq53.d.cts} +147 -3
  33. package/dist/upload-E6MCC2OF.mjs +6 -0
  34. package/package.json +10 -3
  35. package/dist/chunk-BRAFQUMG.mjs +0 -229
  36. package/dist/types--wXNHUwt.d.cts +0 -56
  37. package/dist/types--wXNHUwt.d.ts +0 -56
package/dist/cli.mjs CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bun
2
+ import "./chunk-MIJ7UIKB.mjs";
3
+ import "./chunk-OIHU7OFY.mjs";
2
4
  import {
5
+ BrowserEndpointResolutionError,
3
6
  connect,
4
- getBrowserWebSocketUrl
5
- } from "./chunk-WPNW23CE.mjs";
7
+ resolveBrowserEndpoint
8
+ } from "./chunk-IRLHCVNH.mjs";
6
9
  import "./chunk-LCNFBXB5.mjs";
7
10
  import {
8
11
  DEEP_QUERY_SCRIPT,
9
- LiveTraceCollector,
10
12
  SENSITIVE_AUTOCOMPLETE_TOKENS,
11
13
  TRACE_BINDING_NAME,
12
14
  TRACE_SCRIPT,
@@ -16,13 +18,17 @@ import {
16
18
  canonicalizeRecordingArtifact,
17
19
  createRecordingManifest,
18
20
  createTraceId,
21
+ formatConsoleArg,
19
22
  fuzzyMatchElements,
23
+ globToRegex,
20
24
  grantAudioPermissions,
21
25
  normalizeTraceEvent,
22
26
  pcmToWav,
27
+ readString,
28
+ readStringOr,
23
29
  redactValueForRecording,
24
30
  validateSteps
25
- } from "./chunk-USYSHCI3.mjs";
31
+ } from "./chunk-MRY3HRFJ.mjs";
26
32
  import {
27
33
  isRecord
28
34
  } from "./chunk-DTVRFXKI.mjs";
@@ -31,6 +37,68 @@ import {
31
37
  DAEMON_READY_TIMEOUT_MS
32
38
  } from "./chunk-LUGLEMVR.mjs";
33
39
 
40
+ // src/cli/command-registry.ts
41
+ var CLI_COMMANDS = [
42
+ { name: "quickstart", description: "Getting started guide", showInRootHelp: true },
43
+ { name: "connect", description: "Create or resume a browser session", showInRootHelp: true },
44
+ { name: "exec", description: "Execute high-level actions", showInRootHelp: true },
45
+ { name: "eval", description: "Run raw JavaScript as an escape hatch", showInRootHelp: true },
46
+ { name: "snapshot", description: "Inspect current page with refs", showInRootHelp: true },
47
+ { name: "text", description: "Extract readable page text", showInRootHelp: true },
48
+ { name: "page", description: "Compact page overview", showInRootHelp: true },
49
+ { name: "forms", description: "List form controls", showInRootHelp: true },
50
+ { name: "targets", description: "List available browser tabs", showInRootHelp: true },
51
+ { name: "diagnose", description: "Debug selectors and targeting failures", showInRootHelp: true },
52
+ { name: "review", description: "Structured business state after actions", showInRootHelp: true },
53
+ { name: "screenshot", description: "Capture a page screenshot", showInRootHelp: true },
54
+ { name: "run", description: "Run a workflow file", showInRootHelp: true },
55
+ {
56
+ name: "record",
57
+ description: "Record a human workflow and derive replayable output",
58
+ showInRootHelp: true
59
+ },
60
+ { name: "trace", description: "Inspect and analyze behavior over time", showInRootHelp: true },
61
+ {
62
+ name: "audio",
63
+ description: "Set up, validate, and drive voice pipelines",
64
+ showInRootHelp: true
65
+ },
66
+ { name: "env", description: "Session and browser-environment controls", showInRootHelp: true },
67
+ { name: "daemon", description: "Manage session daemon", showInRootHelp: true },
68
+ { name: "list", description: "List sessions", showInRootHelp: true },
69
+ { name: "close", description: "Close session", showInRootHelp: true },
70
+ { name: "clean", description: "Clean old sessions and artifacts", showInRootHelp: true },
71
+ { name: "actions", description: "Complete action reference", showInRootHelp: true }
72
+ ];
73
+ var ROOT_HELP_COMMANDS = CLI_COMMANDS.filter((command) => command.showInRootHelp);
74
+ var CLI_ROUTE_GROUPS = [
75
+ {
76
+ label: "Inspect page state",
77
+ commands: ["snapshot", "page", "forms", "review", "text", "targets", "diagnose"]
78
+ },
79
+ {
80
+ label: "Act in the browser",
81
+ commands: ["exec", "run"]
82
+ },
83
+ {
84
+ label: "Capture a human demo",
85
+ commands: ["record"]
86
+ },
87
+ {
88
+ label: "Analyze behavior over time",
89
+ commands: ["trace"],
90
+ note: "(listen is a compatibility alias)"
91
+ },
92
+ {
93
+ label: "Exercise voice/media",
94
+ commands: ["audio"]
95
+ },
96
+ {
97
+ label: "Change browser conditions",
98
+ commands: ["env"]
99
+ }
100
+ ];
101
+
34
102
  // src/cli/commands/actions.ts
35
103
  var ACTIONS_HELP = `
36
104
  bp actions - Complete action reference
@@ -231,7 +299,7 @@ EXAMPLES
231
299
  ]'
232
300
 
233
301
  # Use ref from snapshot
234
- bp snapshot --format text # Note the refs
302
+ bp snapshot -i # Note the refs
235
303
  bp exec '{"action":"click","selector":"ref:e4"}'
236
304
 
237
305
  # Scroll and wait
@@ -358,6 +426,268 @@ Content-Type: ${contentType}\r
358
426
  parts.push(data);
359
427
  }
360
428
 
429
+ // src/trace/live.ts
430
+ var LiveTraceCollector = class {
431
+ cdp;
432
+ options;
433
+ handlers = [];
434
+ wsUrls = /* @__PURE__ */ new Map();
435
+ httpUrls = /* @__PURE__ */ new Map();
436
+ events = [];
437
+ startTime = Date.now();
438
+ matchRegex;
439
+ constructor(cdp, options = {}) {
440
+ this.cdp = cdp;
441
+ this.options = options;
442
+ this.matchRegex = options.match ? globToRegex(options.match) : null;
443
+ }
444
+ async start() {
445
+ await this.cdp.send("Runtime.enable");
446
+ await this.cdp.send("Page.enable");
447
+ await this.cdp.send("Network.enable");
448
+ await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
449
+ await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
450
+ await this.cdp.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
451
+ if ((this.options.mode ?? "all") !== "http") {
452
+ this.subscribe("Network.webSocketCreated", (params) => {
453
+ const requestId = readStringOr(params["requestId"]);
454
+ const url = readStringOr(params["url"]);
455
+ if (!this.matchesUrl(url)) {
456
+ return;
457
+ }
458
+ this.wsUrls.set(requestId, url);
459
+ void this.emit({
460
+ channel: "ws",
461
+ event: "ws.connection.created",
462
+ summary: `WebSocket opened ${url}`,
463
+ connectionId: requestId,
464
+ requestId,
465
+ url,
466
+ data: { url }
467
+ });
468
+ });
469
+ this.subscribe("Network.webSocketFrameSent", (params) => {
470
+ const requestId = readStringOr(params["requestId"]);
471
+ const response = params["response"];
472
+ const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
473
+ const url = this.wsUrls.get(requestId);
474
+ if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
475
+ return;
476
+ }
477
+ void this.emit({
478
+ channel: "ws",
479
+ event: "ws.frame.sent",
480
+ summary: `WebSocket frame sent ${requestId}`,
481
+ connectionId: requestId,
482
+ requestId,
483
+ url,
484
+ data: {
485
+ opcode: response?.opcode ?? 1,
486
+ payload,
487
+ length: response?.payloadData?.length ?? 0
488
+ }
489
+ });
490
+ });
491
+ this.subscribe("Network.webSocketFrameReceived", (params) => {
492
+ const requestId = readStringOr(params["requestId"]);
493
+ const response = params["response"];
494
+ const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
495
+ const url = this.wsUrls.get(requestId);
496
+ if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
497
+ return;
498
+ }
499
+ void this.emit({
500
+ channel: "ws",
501
+ event: "ws.frame.received",
502
+ summary: `WebSocket frame received ${requestId}`,
503
+ connectionId: requestId,
504
+ requestId,
505
+ url,
506
+ data: {
507
+ opcode: response?.opcode ?? 1,
508
+ payload,
509
+ length: response?.payloadData?.length ?? 0
510
+ }
511
+ });
512
+ });
513
+ this.subscribe("Network.webSocketClosed", (params) => {
514
+ const requestId = readStringOr(params["requestId"]);
515
+ const url = this.wsUrls.get(requestId);
516
+ this.wsUrls.delete(requestId);
517
+ void this.emit({
518
+ channel: "ws",
519
+ event: "ws.connection.closed",
520
+ summary: `WebSocket closed ${requestId}`,
521
+ severity: "warn",
522
+ connectionId: requestId,
523
+ requestId,
524
+ url,
525
+ data: { url }
526
+ });
527
+ });
528
+ }
529
+ if ((this.options.mode ?? "all") !== "ws") {
530
+ this.subscribe("Network.requestWillBeSent", (params) => {
531
+ const request = params["request"];
532
+ const requestId = readStringOr(params["requestId"]);
533
+ const url = request?.url ?? "";
534
+ if (!this.matchesUrl(url)) {
535
+ return;
536
+ }
537
+ this.httpUrls.set(requestId, url);
538
+ void this.emit({
539
+ channel: "http",
540
+ event: "http.request.sent",
541
+ summary: `${request?.method ?? "GET"} ${url}`,
542
+ requestId,
543
+ url,
544
+ data: {
545
+ method: request?.method ?? "GET",
546
+ headers: request?.headers ?? {},
547
+ body: request?.postData ?? null
548
+ }
549
+ });
550
+ });
551
+ this.subscribe("Network.responseReceived", (params) => {
552
+ const requestId = readStringOr(params["requestId"]);
553
+ if (!this.httpUrls.has(requestId)) {
554
+ return;
555
+ }
556
+ const response = params["response"];
557
+ void this.emit({
558
+ channel: "http",
559
+ event: "http.response.received",
560
+ summary: `${response?.status ?? 0} ${response?.url ?? this.httpUrls.get(requestId) ?? ""}`,
561
+ requestId,
562
+ url: response?.url ?? this.httpUrls.get(requestId),
563
+ data: {
564
+ status: response?.status ?? 0,
565
+ headers: response?.headers ?? {},
566
+ mimeType: response?.mimeType ?? null
567
+ }
568
+ });
569
+ });
570
+ this.subscribe("Network.loadingFailed", (params) => {
571
+ const requestId = readStringOr(params["requestId"]);
572
+ const url = readString(params["blockedReason"]) ?? this.httpUrls.get(requestId) ?? "";
573
+ void this.emit({
574
+ channel: "http",
575
+ event: "http.response.failed",
576
+ summary: `HTTP request failed ${requestId}`,
577
+ severity: "error",
578
+ requestId,
579
+ url,
580
+ data: {
581
+ errorText: params["errorText"] ?? null,
582
+ blockedReason: params["blockedReason"] ?? null,
583
+ canceled: params["canceled"] ?? false
584
+ }
585
+ });
586
+ });
587
+ }
588
+ this.subscribe("Runtime.consoleAPICalled", (params) => {
589
+ const type = readStringOr(params["type"], "log");
590
+ if (type !== "log" && type !== "warn" && type !== "error") {
591
+ return;
592
+ }
593
+ const args = Array.isArray(params["args"]) ? params["args"] : [];
594
+ const text = args.map(formatConsoleArg).filter(Boolean).join(" ");
595
+ void this.emit({
596
+ channel: "console",
597
+ event: `console.${type}`,
598
+ severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
599
+ summary: text || `console.${type}`,
600
+ data: { args }
601
+ });
602
+ });
603
+ this.subscribe("Runtime.exceptionThrown", (params) => {
604
+ const details = params["exceptionDetails"] ?? {};
605
+ const text = readString(details["text"]) ?? "Runtime exception";
606
+ void this.emit({
607
+ channel: "runtime",
608
+ event: "runtime.exception",
609
+ severity: "error",
610
+ summary: text,
611
+ data: details
612
+ });
613
+ });
614
+ this.subscribe("Runtime.bindingCalled", (params) => {
615
+ if (params["name"] !== TRACE_BINDING_NAME) {
616
+ return;
617
+ }
618
+ const raw = readStringOr(params["payload"]);
619
+ try {
620
+ const payload = JSON.parse(raw);
621
+ const channel = this.channelForTraceEvent(payload.event);
622
+ void this.emit({
623
+ channel,
624
+ event: payload.event,
625
+ severity: payload.severity,
626
+ summary: payload.summary ?? payload.event,
627
+ ts: payload.ts ? new Date(payload.ts).toISOString() : void 0,
628
+ data: payload.data ?? {},
629
+ url: readString(payload.data?.["url"])
630
+ });
631
+ } catch {
632
+ }
633
+ });
634
+ }
635
+ async stop() {
636
+ for (const { event, handler } of this.handlers) {
637
+ this.cdp.off(event, handler);
638
+ }
639
+ this.handlers.length = 0;
640
+ return [...this.events];
641
+ }
642
+ getEvents() {
643
+ return [...this.events];
644
+ }
645
+ subscribe(event, handler) {
646
+ this.cdp.on(event, handler);
647
+ this.handlers.push({ event, handler });
648
+ }
649
+ matchesUrl(url) {
650
+ if (!this.matchRegex) {
651
+ return true;
652
+ }
653
+ return this.matchRegex.test(url);
654
+ }
655
+ formatPayload(payloadData, opcode) {
656
+ const data = payloadData ?? "";
657
+ const maxPayload = this.options.maxPayload ?? 256;
658
+ if (opcode === 2) {
659
+ const byteLength = Math.floor(data.length * 3 / 4);
660
+ return `[binary: ${byteLength} bytes]`;
661
+ }
662
+ if (data.length > maxPayload) {
663
+ return `${data.slice(0, maxPayload)}... [truncated, ${data.length} total]`;
664
+ }
665
+ return data;
666
+ }
667
+ channelForTraceEvent(eventName) {
668
+ if (eventName.startsWith("ws.")) return "ws";
669
+ if (eventName.startsWith("http.")) return "http";
670
+ if (eventName.startsWith("console.")) return "console";
671
+ if (eventName.startsWith("permission.")) return "permission";
672
+ if (eventName.startsWith("media.")) return "media";
673
+ if (eventName.startsWith("voice.")) return "voice";
674
+ if (eventName.startsWith("dom.")) return "dom";
675
+ if (eventName.startsWith("runtime.")) return "runtime";
676
+ return "session";
677
+ }
678
+ async emit(event) {
679
+ const normalized = normalizeTraceEvent({
680
+ traceId: event.traceId ?? createTraceId(event.channel),
681
+ sessionId: this.options.sessionId,
682
+ targetId: this.options.targetId,
683
+ elapsedMs: event.elapsedMs ?? Date.now() - this.startTime,
684
+ ...event
685
+ });
686
+ this.events.push(normalized);
687
+ await this.options.onEvent?.(normalized);
688
+ }
689
+ };
690
+
361
691
  // src/trace/store.ts
362
692
  import * as fs from "fs";
363
693
  import { homedir } from "os";
@@ -385,11 +715,159 @@ function readTraceEvents(path) {
385
715
  }).filter((event) => event !== null);
386
716
  }
387
717
 
388
- // src/cli/session-logger.ts
389
- import * as fs2 from "fs";
718
+ // src/cli/browser-endpoint.ts
719
+ async function resolveCLIEndpoint(options = {}) {
720
+ return resolveBrowserEndpoint({
721
+ explicitWsUrl: options.explicitWsUrl,
722
+ channel: options.channel,
723
+ userDataDir: options.userDataDir,
724
+ allowLocalDiscovery: true,
725
+ allowLegacyHostFallback: true
726
+ });
727
+ }
728
+ function formatCandidateLabel(candidate) {
729
+ const channelLabel = candidate.channel ? `${candidate.channel}` : "unknown";
730
+ return `${channelLabel}: ${candidate.userDataDir}`;
731
+ }
732
+ function formatBrowserDiscoveryError(error, options = {}) {
733
+ const explicitFlag = options.explicitFlag ?? "--browser-url";
734
+ if (error instanceof BrowserEndpointResolutionError) {
735
+ if (error.code === "multiple-local-browsers") {
736
+ const candidates = error.details.candidates ?? [];
737
+ const foundLines = candidates.length > 0 ? candidates.map((candidate) => ` - ${formatCandidateLabel(candidate)}`).join("\n") : " - Multiple local Chrome profiles were found";
738
+ return `Multiple running Chrome profiles have remote debugging enabled.
739
+ ${foundLines}
740
+ Pass --channel <stable|beta|dev|canary> or --user-data-dir <path>.`;
741
+ }
742
+ const lines = [
743
+ "Could not auto-discover browser.",
744
+ "Recommended for Chrome 144+:",
745
+ " 1. Open Chrome and enable remote debugging in chrome://inspect/#remote-debugging",
746
+ " 2. Keep Chrome running, then retry",
747
+ "Other options:",
748
+ options.explicitHint ?? ` - Pass ${explicitFlag} with a browser WebSocket URL`,
749
+ " - Launch Chrome with --remote-debugging-port=9222 and a custom --user-data-dir"
750
+ ];
751
+ if (options.reuseSessionHint) {
752
+ lines.push(` - Reuse an existing session: ${options.reuseSessionHint}`);
753
+ }
754
+ if (options.latestSessionHint) {
755
+ lines.push(` - Use latest session: ${options.latestSessionHint}`);
756
+ }
757
+ return lines.join("\n");
758
+ }
759
+ return error instanceof Error ? error.message : String(error);
760
+ }
761
+
762
+ // src/cli/session.ts
390
763
  import { homedir as homedir2 } from "os";
391
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
764
+ import { join as join2 } from "path";
392
765
  var SESSION_DIR2 = join2(homedir2(), ".browser-pilot", "sessions");
766
+ function getSessionFilePath(id) {
767
+ return join2(SESSION_DIR2, `${id}.json`);
768
+ }
769
+ async function ensureSessionDir() {
770
+ const fs9 = await import("fs/promises");
771
+ await fs9.mkdir(SESSION_DIR2, { recursive: true });
772
+ }
773
+ async function saveSession(session) {
774
+ await ensureSessionDir();
775
+ const fs9 = await import("fs/promises");
776
+ const filePath = join2(SESSION_DIR2, `${session.id}.json`);
777
+ await fs9.writeFile(filePath, JSON.stringify(session, null, 2));
778
+ }
779
+ async function loadSession(id) {
780
+ const fs9 = await import("fs/promises");
781
+ const filePath = join2(SESSION_DIR2, `${id}.json`);
782
+ try {
783
+ const content = await fs9.readFile(filePath, "utf-8");
784
+ return JSON.parse(content);
785
+ } catch (error) {
786
+ if (error.code === "ENOENT") {
787
+ throw new Error(`Session not found: ${id}`);
788
+ }
789
+ throw error;
790
+ }
791
+ }
792
+ async function updateSession(id, updates) {
793
+ const session = await loadSession(id);
794
+ const mergedMetadata = updates.metadata !== void 0 ? { ...session.metadata, ...updates.metadata } : session.metadata;
795
+ const updated = {
796
+ ...session,
797
+ ...updates,
798
+ metadata: mergedMetadata,
799
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString()
800
+ };
801
+ await saveSession(updated);
802
+ return updated;
803
+ }
804
+ async function deleteSession(id) {
805
+ const fs9 = await import("fs/promises");
806
+ const filePath = join2(SESSION_DIR2, `${id}.json`);
807
+ try {
808
+ await fs9.unlink(filePath);
809
+ } catch (error) {
810
+ if (error.code !== "ENOENT") {
811
+ throw error;
812
+ }
813
+ }
814
+ }
815
+ async function deleteSessionFull(id) {
816
+ const fs9 = await import("fs/promises");
817
+ const filePath = join2(SESSION_DIR2, `${id}.json`);
818
+ try {
819
+ await fs9.unlink(filePath);
820
+ } catch (error) {
821
+ if (error.code !== "ENOENT") {
822
+ throw error;
823
+ }
824
+ }
825
+ const dirPath = join2(SESSION_DIR2, id);
826
+ try {
827
+ await fs9.rm(dirPath, { recursive: true });
828
+ } catch (error) {
829
+ if (error.code !== "ENOENT") {
830
+ throw error;
831
+ }
832
+ }
833
+ }
834
+ async function listSessions() {
835
+ await ensureSessionDir();
836
+ const fs9 = await import("fs/promises");
837
+ try {
838
+ const files = await fs9.readdir(SESSION_DIR2);
839
+ const sessions = [];
840
+ for (const file of files) {
841
+ if (file.endsWith(".json")) {
842
+ try {
843
+ const content = await fs9.readFile(join2(SESSION_DIR2, file), "utf-8");
844
+ sessions.push(JSON.parse(content));
845
+ } catch {
846
+ }
847
+ }
848
+ }
849
+ return sessions.sort(
850
+ (a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
851
+ );
852
+ } catch {
853
+ return [];
854
+ }
855
+ }
856
+ function generateSessionId() {
857
+ const timestamp = Date.now().toString(36);
858
+ const random = Math.random().toString(36).slice(2, 8);
859
+ return `${timestamp}-${random}`;
860
+ }
861
+ async function getDefaultSession() {
862
+ const sessions = await listSessions();
863
+ return sessions[0] ?? null;
864
+ }
865
+
866
+ // src/cli/session-logger.ts
867
+ import * as fs2 from "fs";
868
+ import { homedir as homedir3 } from "os";
869
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
870
+ var SESSION_DIR3 = join3(homedir3(), ".browser-pilot", "sessions");
393
871
  var SessionLogger = class {
394
872
  logPath;
395
873
  exportLogPath = null;
@@ -397,8 +875,8 @@ var SessionLogger = class {
397
875
  sessionId;
398
876
  constructor(sessionId, exportLogPath) {
399
877
  this.sessionId = sessionId;
400
- const sessionDir = join2(SESSION_DIR2, sessionId);
401
- this.logPath = join2(sessionDir, TRACE_FILE_NAME);
878
+ const sessionDir = join3(SESSION_DIR3, sessionId);
879
+ this.logPath = join3(sessionDir, TRACE_FILE_NAME);
402
880
  if (!fs2.existsSync(sessionDir)) {
403
881
  fs2.mkdirSync(sessionDir, { recursive: true });
404
882
  }
@@ -635,110 +1113,6 @@ function getSessionLogger(sessionId, exportLogPath) {
635
1113
  return logger;
636
1114
  }
637
1115
 
638
- // src/cli/session.ts
639
- import { homedir as homedir3 } from "os";
640
- import { join as join3 } from "path";
641
- var SESSION_DIR3 = join3(homedir3(), ".browser-pilot", "sessions");
642
- function getSessionFilePath(id) {
643
- return join3(SESSION_DIR3, `${id}.json`);
644
- }
645
- async function ensureSessionDir() {
646
- const fs9 = await import("fs/promises");
647
- await fs9.mkdir(SESSION_DIR3, { recursive: true });
648
- }
649
- async function saveSession(session) {
650
- await ensureSessionDir();
651
- const fs9 = await import("fs/promises");
652
- const filePath = join3(SESSION_DIR3, `${session.id}.json`);
653
- await fs9.writeFile(filePath, JSON.stringify(session, null, 2));
654
- }
655
- async function loadSession(id) {
656
- const fs9 = await import("fs/promises");
657
- const filePath = join3(SESSION_DIR3, `${id}.json`);
658
- try {
659
- const content = await fs9.readFile(filePath, "utf-8");
660
- return JSON.parse(content);
661
- } catch (error) {
662
- if (error.code === "ENOENT") {
663
- throw new Error(`Session not found: ${id}`);
664
- }
665
- throw error;
666
- }
667
- }
668
- async function updateSession(id, updates) {
669
- const session = await loadSession(id);
670
- const mergedMetadata = updates.metadata !== void 0 ? { ...session.metadata, ...updates.metadata } : session.metadata;
671
- const updated = {
672
- ...session,
673
- ...updates,
674
- metadata: mergedMetadata,
675
- lastActivity: (/* @__PURE__ */ new Date()).toISOString()
676
- };
677
- await saveSession(updated);
678
- return updated;
679
- }
680
- async function deleteSession(id) {
681
- const fs9 = await import("fs/promises");
682
- const filePath = join3(SESSION_DIR3, `${id}.json`);
683
- try {
684
- await fs9.unlink(filePath);
685
- } catch (error) {
686
- if (error.code !== "ENOENT") {
687
- throw error;
688
- }
689
- }
690
- }
691
- async function deleteSessionFull(id) {
692
- const fs9 = await import("fs/promises");
693
- const filePath = join3(SESSION_DIR3, `${id}.json`);
694
- try {
695
- await fs9.unlink(filePath);
696
- } catch (error) {
697
- if (error.code !== "ENOENT") {
698
- throw error;
699
- }
700
- }
701
- const dirPath = join3(SESSION_DIR3, id);
702
- try {
703
- await fs9.rm(dirPath, { recursive: true });
704
- } catch (error) {
705
- if (error.code !== "ENOENT") {
706
- throw error;
707
- }
708
- }
709
- }
710
- async function listSessions() {
711
- await ensureSessionDir();
712
- const fs9 = await import("fs/promises");
713
- try {
714
- const files = await fs9.readdir(SESSION_DIR3);
715
- const sessions = [];
716
- for (const file of files) {
717
- if (file.endsWith(".json")) {
718
- try {
719
- const content = await fs9.readFile(join3(SESSION_DIR3, file), "utf-8");
720
- sessions.push(JSON.parse(content));
721
- } catch {
722
- }
723
- }
724
- }
725
- return sessions.sort(
726
- (a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
727
- );
728
- } catch {
729
- return [];
730
- }
731
- }
732
- function generateSessionId() {
733
- const timestamp = Date.now().toString(36);
734
- const random = Math.random().toString(36).slice(2, 8);
735
- return `${timestamp}-${random}`;
736
- }
737
- async function getDefaultSession() {
738
- const sessions = await listSessions();
739
- return sessions[0] ?? null;
740
- }
741
-
742
1116
  // src/cli/commands/audio.ts
743
1117
  var AUDIO_HELP = `
744
1118
  bp audio - Actively exercise voice and audio pipelines
@@ -894,10 +1268,14 @@ async function resolveConnection(sessionId, useLatestSession, trace) {
894
1268
  }
895
1269
  let wsUrl;
896
1270
  try {
897
- wsUrl = await getBrowserWebSocketUrl("localhost:9222");
898
- } catch {
1271
+ wsUrl = (await resolveCLIEndpoint()).wsUrl;
1272
+ } catch (error) {
899
1273
  throw new Error(
900
- "Could not auto-discover browser.\nEither:\n 1. Start Chrome with: --remote-debugging-port=9222\n 2. Use an existing session: bp audio -s <session-id>\n 3. Use latest session: bp audio -s"
1274
+ formatBrowserDiscoveryError(error, {
1275
+ explicitHint: " - Create a session first: bp connect --browser-url <ws-url>",
1276
+ reuseSessionHint: "bp audio -s <session-id>",
1277
+ latestSessionHint: "bp audio -s"
1278
+ })
901
1279
  );
902
1280
  }
903
1281
  const browser = await connect({ provider: "generic", wsUrl, debug: trace });
@@ -1171,7 +1549,7 @@ async function audioCommand(args, globalOptions) {
1171
1549
  event: checkJson.ready ? "voice.pipeline.ready" : "voice.pipeline.notReady",
1172
1550
  severity: checkJson.ready ? "info" : "error",
1173
1551
  summary: checkJson.ready ? "Audio pipeline ready" : "Audio pipeline not ready",
1174
- data: checkJson
1552
+ data: { ...checkJson }
1175
1553
  });
1176
1554
  if (checkJson.agentDetected) {
1177
1555
  logger.logTrace({
@@ -1472,13 +1850,15 @@ bp clean - Remove stale browser sessions
1472
1850
  Usage:
1473
1851
  bp clean [options]
1474
1852
 
1475
- Options:
1853
+ Local options:
1476
1854
  --max-age <hours> Remove sessions older than N hours (default: 24)
1477
1855
  --max-size <size> Remove oldest sessions until total size < limit (e.g. "100MB", "1GB")
1478
1856
  --dry-run Show what would be removed without deleting
1479
1857
  --all Remove all sessions regardless of age
1480
- -f, --format <fmt> Output format: json | pretty (default: pretty)
1481
- --json Alias for -f json
1858
+
1859
+ Global options:
1860
+ --json Output JSON
1861
+ --pretty Output readable text (default)
1482
1862
  -h, --help Show this help
1483
1863
 
1484
1864
  Examples:
@@ -1682,11 +2062,11 @@ bp close - Close a browser session
1682
2062
  Usage:
1683
2063
  bp close [session-id]
1684
2064
 
1685
- Options:
2065
+ Global options:
1686
2066
  -s, --session <id> Session to close (default: most recent)
1687
- -f, --format <fmt> Output format: json | pretty (default: pretty)
1688
- --json Alias for -f json
1689
- --trace Enable debug tracing
2067
+ --json Output JSON
2068
+ --pretty Output readable text (default)
2069
+ --debug Enable CDP transport debugging
1690
2070
  -h, --help Show this help
1691
2071
 
1692
2072
  Examples:
@@ -1791,16 +2171,30 @@ async function waitForDaemonReady(sessionFilePath, expectedPid, timeoutMs = DAEM
1791
2171
  var CONNECT_HELP = `
1792
2172
  bp connect - Create or resume a browser session
1793
2173
 
2174
+ When to use:
2175
+ Create a session before running inspect, exec, record, trace, audio, or env commands.
2176
+
2177
+ When not to use:
2178
+ You already have a session and only need to open a page. Use \`bp exec '{"action":"goto","url":"..."}'\`.
2179
+
2180
+ Browser and page URL guidance:
2181
+ Use \`--browser-url\` for a DevTools WebSocket endpoint.
2182
+ Use \`--page-url\` to open a page in the attached tab or a new tab.
2183
+ \`--url\` remains for compatibility and is ambiguous when paired with \`--new-tab\`.
2184
+
1794
2185
  Usage:
1795
2186
  bp connect [options]
1796
2187
 
1797
- Options:
2188
+ Local options:
1798
2189
  -p, --provider <type> Provider: generic | browserbase | browserless (default: generic)
1799
- --url <value> Browser WebSocket URL, or page URL when used with --new-tab
1800
- --browser-url <ws-url> Explicit browser WebSocket URL
1801
- --page-url <url> URL to open in the attached page/new tab
2190
+ --browser-url <ws-url> Explicit browser WebSocket URL (preferred)
2191
+ --page-url <url> Page URL to open in the attached tab/new tab (preferred)
2192
+ --url <value> Compatibility shorthand; browser URL, or page URL with --new-tab
2193
+ --channel <name> Local Chrome channel: stable | beta | dev | canary
2194
+ --user-data-dir <path> Explicit local Chrome user data dir for auto-discovery
1802
2195
  -n, --name <id> Custom session name (default: auto-generated)
1803
2196
  -r, --resume <id> Resume an existing session by ID
2197
+ -s, --session <id> Alias for --resume
1804
2198
  --new-tab Create and attach to a fresh tab instead of reusing an existing one
1805
2199
  --target-url <str> Filter targets to those whose URL contains this string
1806
2200
  --api-key <key> API key for cloud providers
@@ -1812,30 +2206,69 @@ Options:
1812
2206
  --no-highlights Disable visual highlights on screenshots
1813
2207
  --no-daemon Skip daemon creation (direct WebSocket only)
1814
2208
  --daemon-idle <mins> Daemon idle timeout in minutes (default: 60)
1815
- -s, --session <id> Alias for --resume
1816
- --trace Enable debug tracing
2209
+
2210
+ Global options:
2211
+ --json Output JSON
2212
+ --pretty Output readable text (default)
2213
+ --debug Enable CDP transport debugging
1817
2214
  -h, --help Show this help
1818
2215
 
1819
2216
  Examples:
1820
- bp connect # Auto-connect to local Chrome (port 9222)
1821
- bp connect --record # Connect with session-level recording
1822
- bp connect --provider generic --name dev # Connect with custom session name
1823
- bp connect --url ws://localhost:9222/devtools # Explicit WebSocket URL
1824
- bp connect --resume dev # Resume a previous session
2217
+ bp connect # Auto-connect to local Chrome
2218
+ bp connect --name dev # Auto-connect with a custom session name
2219
+ bp connect --resume dev # Resume a previous session
2220
+ bp connect --browser-url ws://localhost:9222/devtools/browser/abc123
2221
+ bp connect --channel beta # Narrow auto-discovery to Chrome Beta
2222
+ bp connect --user-data-dir ~/tmp/chrome-dev # Use a specific Chrome profile
1825
2223
  bp connect --target-url localhost:3000 # Attach to tab matching URL
1826
- bp connect --new-tab --url https://example.com # Create and attach to a fresh tab
1827
- bp connect --no-daemon # Connect without daemon (file-based only)
2224
+ bp connect --record # Connect with session-level recording
2225
+ bp connect --new-tab --page-url https://example.com
2226
+ bp connect --no-daemon # Connect without daemon (file-based only)
2227
+
2228
+ Likely next commands:
2229
+ bp exec -s dev '{"action":"goto","url":"https://example.com"}'
2230
+ bp snapshot -i -s dev
2231
+ bp text -s dev
1828
2232
  `.trimEnd();
2233
+ async function resolveInitialPageUrl(page, requestedUrl) {
2234
+ const initialUrl = await page.url();
2235
+ if (!requestedUrl || requestedUrl === "about:blank" || initialUrl !== "about:blank") {
2236
+ return initialUrl;
2237
+ }
2238
+ const deadline = Date.now() + 5e3;
2239
+ while (Date.now() < deadline) {
2240
+ await Bun.sleep(100);
2241
+ const currentUrl = await page.url();
2242
+ if (currentUrl !== "about:blank") {
2243
+ return currentUrl;
2244
+ }
2245
+ }
2246
+ return initialUrl;
2247
+ }
1829
2248
  function parseConnectArgs(args) {
1830
2249
  const options = {};
1831
2250
  for (let i = 0; i < args.length; i++) {
1832
2251
  const arg = args[i];
1833
2252
  if (arg === "--provider" || arg === "-p") {
1834
- options.provider = args[++i];
2253
+ const p = args[++i];
2254
+ if (p !== "browserbase" && p !== "browserless" && p !== "generic") {
2255
+ throw new Error(
2256
+ `Invalid provider: ${p}. Must be one of: browserbase, browserless, generic`
2257
+ );
2258
+ }
2259
+ options.provider = p;
1835
2260
  } else if (arg === "--url") {
1836
2261
  options.url = args[++i];
1837
2262
  } else if (arg === "--browser-url") {
1838
2263
  options.browserUrl = args[++i];
2264
+ } else if (arg === "--channel") {
2265
+ const channel = args[++i];
2266
+ if (channel !== "stable" && channel !== "beta" && channel !== "dev" && channel !== "canary") {
2267
+ throw new Error("--channel must be one of: stable, beta, dev, canary");
2268
+ }
2269
+ options.channel = channel;
2270
+ } else if (arg === "--user-data-dir") {
2271
+ options.userDataDir = args[++i];
1839
2272
  } else if (arg === "--page-url") {
1840
2273
  options.pageUrl = args[++i];
1841
2274
  } else if (arg === "--name" || arg === "-n") {
@@ -1911,6 +2344,9 @@ async function connectCommand(args, globalOptions) {
1911
2344
  const provider = options.provider ?? "generic";
1912
2345
  let wsUrl = options.browserUrl ?? options.url;
1913
2346
  let pageUrl = options.pageUrl;
2347
+ let connectionSource;
2348
+ let resolvedChannel;
2349
+ let resolvedUserDataDir;
1914
2350
  if (options.newTab && options.url && !options.url.startsWith("ws://") && !options.url.startsWith("wss://")) {
1915
2351
  pageUrl = options.url;
1916
2352
  if (!options.browserUrl) {
@@ -1919,17 +2355,31 @@ async function connectCommand(args, globalOptions) {
1919
2355
  }
1920
2356
  if (provider === "generic" && !wsUrl) {
1921
2357
  try {
1922
- wsUrl = await getBrowserWebSocketUrl("localhost:9222");
1923
- } catch {
2358
+ const resolved = await resolveCLIEndpoint({
2359
+ explicitWsUrl: wsUrl,
2360
+ channel: options.channel,
2361
+ userDataDir: options.userDataDir
2362
+ });
2363
+ wsUrl = resolved.wsUrl;
2364
+ connectionSource = resolved.source;
2365
+ resolvedChannel = resolved.channel;
2366
+ resolvedUserDataDir = resolved.userDataDir;
2367
+ } catch (error) {
1924
2368
  throw new Error(
1925
- "Could not auto-discover browser. Specify --url or start Chrome with --remote-debugging-port=9222"
2369
+ formatBrowserDiscoveryError(error, {
2370
+ explicitFlag: "--browser-url"
2371
+ })
1926
2372
  );
1927
2373
  }
2374
+ } else if (wsUrl) {
2375
+ connectionSource = "explicit-ws";
1928
2376
  }
1929
2377
  const connectOptions = {
1930
2378
  provider,
1931
2379
  debug: globalOptions.trace,
1932
2380
  wsUrl,
2381
+ channel: options.channel,
2382
+ userDataDir: options.userDataDir,
1933
2383
  apiKey: options.apiKey,
1934
2384
  projectId: options.projectId
1935
2385
  };
@@ -1938,7 +2388,7 @@ async function connectCommand(args, globalOptions) {
1938
2388
  void 0,
1939
2389
  options.targetUrl ? { targetUrl: options.targetUrl } : void 0
1940
2390
  );
1941
- const currentUrl = await page.url();
2391
+ const currentUrl = await resolveInitialPageUrl(page, pageUrl);
1942
2392
  const sessionId = options.name ?? generateSessionId();
1943
2393
  let recordSettings;
1944
2394
  if (options.record) {
@@ -1959,9 +2409,13 @@ async function connectCommand(args, globalOptions) {
1959
2409
  currentUrl,
1960
2410
  metadata: {
1961
2411
  ...browser.metadata,
2412
+ ...connectionSource ? { connectionSource } : {},
2413
+ ...resolvedChannel ? { resolvedChannel } : {},
2414
+ ...resolvedUserDataDir ? { resolvedUserDataDir } : {},
1962
2415
  ...recordSettings ? { record: recordSettings } : {}
1963
2416
  }
1964
2417
  };
2418
+ const outputMetadata = session.metadata;
1965
2419
  await saveSession(session);
1966
2420
  await browser.disconnect();
1967
2421
  let daemonResult;
@@ -1989,7 +2443,10 @@ async function connectCommand(args, globalOptions) {
1989
2443
  provider,
1990
2444
  currentUrl,
1991
2445
  recording: !!recordSettings,
1992
- metadata: browser.metadata,
2446
+ connectionSource,
2447
+ resolvedChannel,
2448
+ resolvedUserDataDir,
2449
+ metadata: outputMetadata,
1993
2450
  daemon: daemonResult
1994
2451
  },
1995
2452
  globalOptions.format
@@ -2013,10 +2470,12 @@ Subcommands:
2013
2470
  logs Show daemon log output
2014
2471
 
2015
2472
  Options:
2016
- -s, --session <id> Target session (default: most recent)
2017
- -f, --format <fmt> Output format: json | pretty (default: pretty)
2018
- --json Alias for -f json
2019
2473
  -n, --lines <n> Number of log lines to show (default: 50)
2474
+
2475
+ Global options:
2476
+ -s, --session <id> Target session (default: most recent)
2477
+ --json Output JSON
2478
+ --pretty Output readable text (default)
2020
2479
  -h, --help Show this help
2021
2480
 
2022
2481
  Examples:
@@ -2646,11 +3105,15 @@ Examples:
2646
3105
  bp diagnose "submit" Find elements matching "submit"
2647
3106
  bp diagnose "ref:e4" Diagnose by element ref
2648
3107
 
2649
- Options:
2650
- --json Output as JSON
3108
+ Local options:
2651
3109
  --max <n> Max candidates for fuzzy match (default: 5)
2652
- -s, --session <id> Use specific session
2653
- --help Show this help
3110
+
3111
+ Global options:
3112
+ -s, --session <id> Session to use (default: most recent)
3113
+ --json Output JSON
3114
+ --pretty Output readable text (default)
3115
+ --debug Enable CDP transport debugging
3116
+ -h, --help Show this help
2654
3117
 
2655
3118
  Likely next commands:
2656
3119
  bp exec '[{"action":"click","selector":"<suggested-selector>"}]'
@@ -2784,6 +3247,9 @@ async function diagnoseCommand(args, globalOptions) {
2784
3247
  }
2785
3248
  }
2786
3249
 
3250
+ // src/cli/commands/env.ts
3251
+ import { dirname as dirname5 } from "path";
3252
+
2787
3253
  // src/cli/env-state.ts
2788
3254
  function normalizeStoredPermission(name) {
2789
3255
  const value = String(name).trim().toLowerCase();
@@ -2821,7 +3287,9 @@ function originFromUrl(url) {
2821
3287
  }
2822
3288
  }
2823
3289
  function buildPermissionOverrideScript(granted) {
2824
- const normalized = [...new Set(granted.map((value) => normalizeStoredPermission(value)).filter(Boolean))];
3290
+ const normalized = [
3291
+ ...new Set(granted.map((value) => normalizeStoredPermission(value)).filter(Boolean))
3292
+ ];
2825
3293
  return `
2826
3294
  (() => {
2827
3295
  const granted = ${JSON.stringify(normalized)};
@@ -3058,7 +3526,9 @@ function buildNetworkOverrideScript(state) {
3058
3526
  `.trim();
3059
3527
  }
3060
3528
  async function applyPermissionState(cdp, origin, granted) {
3061
- const protocolPermissions = [...new Set(granted.map((value) => toProtocolPermission(value)).filter(Boolean))];
3529
+ const protocolPermissions = [
3530
+ ...new Set(granted.map((value) => toProtocolPermission(value)).filter(Boolean))
3531
+ ];
3062
3532
  if (protocolPermissions.length > 0) {
3063
3533
  await cdp.send("Browser.grantPermissions", {
3064
3534
  permissions: protocolPermissions,
@@ -3080,222 +3550,7 @@ async function applyNetworkOverride(cdp, state) {
3080
3550
  await cdp.send("Runtime.evaluate", { expression: script, awaitPromise: false });
3081
3551
  }
3082
3552
 
3083
- // src/cli/attach.ts
3084
- async function applySessionEnvironment(page, currentUrl, settings) {
3085
- if (!settings) {
3086
- return;
3087
- }
3088
- const origin = originFromUrl(currentUrl);
3089
- if (Array.isArray(settings.permissions)) {
3090
- await applyPermissionState(page.cdpClient, origin, settings.permissions);
3091
- }
3092
- if (settings.geolocation) {
3093
- await page.setGeolocation(settings.geolocation);
3094
- }
3095
- if (settings.visibility) {
3096
- await applyVisibilityState(page.cdpClient, settings.visibility);
3097
- }
3098
- if (settings.network) {
3099
- await applyNetworkOverride(page.cdpClient, settings.network);
3100
- }
3101
- }
3102
- async function resolveSession(sessionId) {
3103
- if (sessionId) {
3104
- return loadSession(sessionId);
3105
- }
3106
- const session = await getDefaultSession();
3107
- if (!session) {
3108
- throw new Error('No session found. Run "bp connect" first.');
3109
- }
3110
- return session;
3111
- }
3112
- function isDaemonHealthy(session) {
3113
- if (!session.daemon) return false;
3114
- const daemonAge = Date.now() - new Date(session.daemon.startedAt).getTime();
3115
- if (daemonAge > DAEMON_MAX_AGE_MS) {
3116
- return false;
3117
- }
3118
- if (session.daemon.lastHeartbeat) {
3119
- const heartbeatAge = Date.now() - new Date(session.daemon.lastHeartbeat).getTime();
3120
- if (heartbeatAge > 9e4) {
3121
- return false;
3122
- }
3123
- }
3124
- return isDaemonAlive(session.daemon.pid);
3125
- }
3126
- async function cleanupStaleDaemon(session, reason) {
3127
- console.warn(`[browser-pilot] Daemon unavailable (${reason}), falling back to direct WebSocket`);
3128
- const sessionFilePath = getSessionFilePath(session.id);
3129
- await clearDaemonFromSession(sessionFilePath);
3130
- if (session.daemon?.socketPath) {
3131
- try {
3132
- const fsPromises = await import("fs/promises");
3133
- await fsPromises.unlink(session.daemon.socketPath).catch(() => {
3134
- });
3135
- } catch {
3136
- }
3137
- }
3138
- }
3139
- async function attachSession(session, options = {}) {
3140
- if (session.daemon) {
3141
- if (!isDaemonHealthy(session)) {
3142
- const reason = !isDaemonAlive(session.daemon.pid) ? "PID not alive" : "daemon expired (>60min)";
3143
- await cleanupStaleDaemon(session, reason);
3144
- } else {
3145
- try {
3146
- const { createDaemonTransport } = await import("./transport-WHEBAZUP.mjs");
3147
- const { createCDPClientFromTransport } = await import("./client-JWWZWO6L.mjs");
3148
- const transport = await createDaemonTransport(session.daemon.socketPath);
3149
- const cdp = createCDPClientFromTransport(transport, {
3150
- debug: options.trace
3151
- });
3152
- const { Browser: BrowserClass } = await import("./browser-MEWT75IB.mjs");
3153
- const { Page: PageClass } = await import("./page-XPS6IC6V.mjs");
3154
- const browser2 = BrowserClass.fromCDP(cdp, session);
3155
- const page2 = session.daemon.cdpSessionId && session.targetId ? addBatchToPage(
3156
- await (async () => {
3157
- cdp.setSessionId(session.daemon?.cdpSessionId);
3158
- const attachedPage = new PageClass(cdp, session.targetId);
3159
- await attachedPage.init();
3160
- return attachedPage;
3161
- })()
3162
- ) : addBatchToPage(await browser2.page(void 0, { targetId: session.targetId }));
3163
- const currentUrl2 = await page2.url();
3164
- await applySessionEnvironment(page2, currentUrl2, session.metadata?.env);
3165
- const refCache2 = session.metadata?.refCache;
3166
- if (refCache2 && refCache2.url === currentUrl2) {
3167
- page2.importRefMap(refCache2.refMap);
3168
- }
3169
- return { session, browser: browser2, page: page2, viaDaemon: true };
3170
- } catch (err) {
3171
- const reason = err instanceof Error ? err.message : String(err);
3172
- await cleanupStaleDaemon(session, reason);
3173
- }
3174
- }
3175
- }
3176
- let browser;
3177
- try {
3178
- browser = await connect({
3179
- provider: session.provider,
3180
- wsUrl: session.wsUrl,
3181
- debug: options.trace
3182
- });
3183
- } catch {
3184
- await deleteSession(session.id);
3185
- throw new Error(
3186
- `Session "${session.id}" is no longer valid (browser may have closed).
3187
- Session file has been cleaned up. Run "bp connect" to create a new session.`
3188
- );
3189
- }
3190
- const page = addBatchToPage(await browser.page(void 0, { targetId: session.targetId }));
3191
- const currentUrl = await page.url();
3192
- await applySessionEnvironment(page, currentUrl, session.metadata?.env);
3193
- const refCache = session.metadata?.refCache;
3194
- if (refCache && refCache.url === currentUrl) {
3195
- page.importRefMap(refCache.refMap);
3196
- }
3197
- return { session, browser, page, viaDaemon: false };
3198
- }
3199
-
3200
- // src/cli/commands/eval.ts
3201
- var EVAL_HELP = `
3202
- bp eval - Evaluate JavaScript in the browser
3203
-
3204
- Convenience wrapper around exec's evaluate action.
3205
- No JSON escaping needed -- just pass a JS expression directly.
3206
-
3207
- Usage:
3208
- bp eval '<expression>' Evaluate inline JavaScript
3209
- bp eval -f <file> Evaluate JavaScript from a file
3210
- echo '<expr>' | bp eval Evaluate from stdin
3211
-
3212
- Options:
3213
- -f, --file <path> Read JavaScript from a file
3214
- --wrap Wrap the expression in an async IIFE
3215
- -s, --session <id> Session to use (default: most recent)
3216
- -f, --format <fmt> Output format: json | pretty (default: pretty)
3217
- --json Alias for -f json
3218
- --trace Enable debug tracing
3219
- -h, --help Show this help
3220
-
3221
- Examples:
3222
- bp eval 'document.title'
3223
- bp eval 'document.querySelectorAll("a").length'
3224
- bp eval -f scrape.js
3225
- `.trimEnd();
3226
- function parseEvalArgs(args) {
3227
- const options = {};
3228
- let expression;
3229
- for (let i = 0; i < args.length; i++) {
3230
- const arg = args[i];
3231
- if (arg === "-f" || arg === "--file") {
3232
- options.file = args[++i];
3233
- } else if (arg === "--wrap") {
3234
- options.wrap = true;
3235
- } else if (!expression && !arg.startsWith("-")) {
3236
- expression = arg;
3237
- }
3238
- }
3239
- return { expression, options };
3240
- }
3241
- function normalizeEvalExpression(expression, wrap = false) {
3242
- const trimmed = expression.trim();
3243
- const needsWrap = wrap || trimmed.includes("=>") || /\bawait\b/.test(trimmed);
3244
- if (!needsWrap) {
3245
- return trimmed;
3246
- }
3247
- if (wrap || /\bawait\b/.test(trimmed)) {
3248
- return `(async () => (${trimmed}))()`;
3249
- }
3250
- return `(() => (${trimmed}))()`;
3251
- }
3252
- async function evalCommand(args, globalOptions) {
3253
- if (globalOptions.help) {
3254
- console.log(EVAL_HELP);
3255
- return;
3256
- }
3257
- const { expression: argExpression, options: evalOptions } = parseEvalArgs(args);
3258
- let expression = argExpression;
3259
- if (evalOptions.file) {
3260
- const fs9 = await import("fs/promises");
3261
- expression = await fs9.readFile(evalOptions.file, "utf-8");
3262
- }
3263
- if (!expression && !process.stdin.isTTY) {
3264
- const chunks = [];
3265
- for await (const chunk of process.stdin) {
3266
- chunks.push(chunk);
3267
- }
3268
- expression = Buffer.concat(chunks).toString("utf-8").trim();
3269
- }
3270
- if (!expression) {
3271
- throw new Error(
3272
- "No expression provided.\n\nUsage:\n bp eval 'document.title'\n bp eval -f script.js\n echo 'document.title' | bp eval"
3273
- );
3274
- }
3275
- const session = await resolveSession(globalOptions.session);
3276
- const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
3277
- try {
3278
- const step = {
3279
- action: "evaluate",
3280
- value: normalizeEvalExpression(expression, evalOptions.wrap)
3281
- };
3282
- const result = await page.batch([step]);
3283
- const stepResult = result.steps[0];
3284
- if (!stepResult.success) {
3285
- throw new Error(stepResult.error ?? "Evaluation failed");
3286
- }
3287
- output(
3288
- globalOptions.format === "json" ? { success: true, result: stepResult.result } : stepResult.result,
3289
- globalOptions.format
3290
- );
3291
- await updateSession(session.id, { currentUrl: await page.url() });
3292
- } finally {
3293
- await browser.disconnect();
3294
- }
3295
- }
3296
-
3297
3553
  // src/cli/commands/env.ts
3298
- import { dirname as dirname5 } from "path";
3299
3554
  var ENV_HELP = `
3300
3555
  bp env - Browser/session environment controls
3301
3556
 
@@ -3453,7 +3708,6 @@ function parseEnvArgs(args) {
3453
3708
  }
3454
3709
  if (options.topCommand === "geolocation") {
3455
3710
  options.geoAction = arg;
3456
- continue;
3457
3711
  }
3458
3712
  }
3459
3713
  }
@@ -3484,15 +3738,22 @@ async function resolveConnection2(sessionId, useLatestSession = false) {
3484
3738
  if (!defaultSession) {
3485
3739
  throw new Error('No sessions found. Run "bp connect" first or use "-s" for latest session.');
3486
3740
  }
3487
- const browser2 = await connect({ provider: defaultSession.provider, wsUrl: defaultSession.wsUrl });
3741
+ const browser2 = await connect({
3742
+ provider: defaultSession.provider,
3743
+ wsUrl: defaultSession.wsUrl
3744
+ });
3488
3745
  return { browser: browser2, session: defaultSession };
3489
3746
  }
3490
3747
  let wsUrl;
3491
3748
  try {
3492
- wsUrl = await getBrowserWebSocketUrl("localhost:9222");
3493
- } catch {
3749
+ wsUrl = (await resolveCLIEndpoint()).wsUrl;
3750
+ } catch (error) {
3494
3751
  throw new Error(
3495
- "Could not auto-discover browser.\nEither:\n 1) Start Chrome with: --remote-debugging-port=9222\n 2) Use an existing session: bp env -s <id> ...\n 3) Use latest session: bp env -s"
3752
+ formatBrowserDiscoveryError(error, {
3753
+ explicitHint: " - Create a session first: bp connect --browser-url <ws-url>",
3754
+ reuseSessionHint: "bp env -s <id> ...",
3755
+ latestSessionHint: "bp env -s"
3756
+ })
3496
3757
  );
3497
3758
  }
3498
3759
  const browser = await connect({ provider: "generic", wsUrl });
@@ -3509,7 +3770,9 @@ async function resolveConnection2(sessionId, useLatestSession = false) {
3509
3770
  };
3510
3771
  await saveSession(session);
3511
3772
  const sessionFile = getSessionFilePath(newSessionId);
3512
- await import("fs/promises").then((fs9) => fs9.mkdir(dirname5(sessionFile), { recursive: true }));
3773
+ await import("fs/promises").then(
3774
+ (fs9) => fs9.mkdir(dirname5(sessionFile), { recursive: true })
3775
+ );
3513
3776
  return { browser, session };
3514
3777
  }
3515
3778
  function clampRate(value) {
@@ -3537,7 +3800,7 @@ async function getPermissionStates(page) {
3537
3800
  }));
3538
3801
  })()
3539
3802
  `;
3540
- return await page.evaluate(expr);
3803
+ return page.evaluate(expr);
3541
3804
  }
3542
3805
  async function permissionCommand(action, nameInput, page) {
3543
3806
  const requested = nameInput && nameInput !== "all" ? coercePermissionArg(nameInput) : "all";
@@ -3545,12 +3808,16 @@ async function permissionCommand(action, nameInput, page) {
3545
3808
  const states = await getPermissionStates(page);
3546
3809
  if (requested !== "all") {
3547
3810
  const lower = String(requested).toLowerCase();
3548
- return states.filter((item) => item.name === lower || item.name === PermissionNames.NAVIGATION[requested]).map((item) => ({ action, name: item.name, state: item.state }));
3811
+ return states.filter(
3812
+ (item) => item.name === lower || item.name === PermissionNames.NAVIGATION[requested]
3813
+ ).map((item) => ({ action, name: item.name, state: item.state }));
3549
3814
  }
3550
3815
  return states.map((item) => ({ action, name: item.name, state: item.state }));
3551
3816
  }
3552
3817
  const permissionNames = requested === "all" ? Object.values(PermissionNames.NAVIGATION).filter((v) => v !== "all") : [PermissionNames.NAVIGATION[requested] ?? String(requested)];
3553
- const protocolNames = requested === "all" ? ["geolocation", "audioCapture", "videoCapture", "notifications"] : permissionNames.map((item) => PermissionNames.PROTOCOL[item] ?? String(item));
3818
+ const protocolNames = requested === "all" ? ["geolocation", "audioCapture", "videoCapture", "notifications"] : permissionNames.map(
3819
+ (item) => PermissionNames.PROTOCOL[item] ?? String(item)
3820
+ );
3554
3821
  if (action === "grant") {
3555
3822
  const origin2 = await page.evaluate("window.location.origin");
3556
3823
  await page.cdpClient.send("Browser.grantPermissions", {
@@ -3587,7 +3854,8 @@ async function permissionCommand(action, nameInput, page) {
3587
3854
  function formatPermissionOutput(session, data) {
3588
3855
  const lines = [`Session: ${session.id}`, ""];
3589
3856
  for (const row of data) {
3590
- lines.push(`${row.name}: ${row.state ?? "unknown"} (${row.action})`);
3857
+ const state = typeof row.state === "string" ? row.state : row.state === void 0 || row.state === null ? "unknown" : JSON.stringify(row.state);
3858
+ lines.push(`${row.name}: ${state} (${row.action})`);
3591
3859
  }
3592
3860
  return lines.join("\n");
3593
3861
  }
@@ -3644,7 +3912,9 @@ async function runNetworkCommand(action, options, page, session) {
3644
3912
  offline: false,
3645
3913
  latency
3646
3914
  });
3647
- console.log(`Session ${session.id}: network throttled | latency=${latency}ms down=${down}B/s up=${up}B/s`);
3915
+ console.log(
3916
+ `Session ${session.id}: network throttled | latency=${latency}ms down=${down}B/s up=${up}B/s`
3917
+ );
3648
3918
  }
3649
3919
  async function runVisibilityCommand(state, page, session) {
3650
3920
  await applyVisibilityState(page.cdpClient, state);
@@ -3664,7 +3934,9 @@ async function runGeolocationCommand(action, options, page, session) {
3664
3934
  longitude: options.lon,
3665
3935
  accuracy: options.accuracy ?? 1
3666
3936
  });
3667
- console.log(`Session ${session.id}: geolocation set to ${options.lat}, ${options.lon} (accuracy ${options.accuracy ?? 1})`);
3937
+ console.log(
3938
+ `Session ${session.id}: geolocation set to ${options.lat}, ${options.lon} (accuracy ${options.accuracy ?? 1})`
3939
+ );
3668
3940
  }
3669
3941
  async function envCommand(args, globalOptions) {
3670
3942
  const options = parseEnvArgs(args);
@@ -3672,7 +3944,10 @@ async function envCommand(args, globalOptions) {
3672
3944
  console.log(ENV_HELP);
3673
3945
  return;
3674
3946
  }
3675
- const { browser, session } = await resolveConnection2(globalOptions.session, options.useLatestSession ?? false);
3947
+ const { browser, session } = await resolveConnection2(
3948
+ globalOptions.session,
3949
+ options.useLatestSession ?? false
3950
+ );
3676
3951
  const page = await browser.page(void 0, { targetId: session.targetId });
3677
3952
  const outputAsJson = globalOptions.format === "json";
3678
3953
  const existingEnv = session.metadata?.env ?? {};
@@ -3694,7 +3969,9 @@ async function envCommand(args, globalOptions) {
3694
3969
  const current = new Set(
3695
3970
  (existingEnv.permissions ?? []).map((value) => normalizeStoredPermission(value)).filter(isStoredPermissionName)
3696
3971
  );
3697
- const requested = options.permissionName === "all" || !options.permissionName ? ["microphone", "camera", "notifications", "geolocation"] : [normalizeStoredPermission(options.permissionName)].filter(isStoredPermissionName);
3972
+ const requested = options.permissionName === "all" || !options.permissionName ? ["microphone", "camera", "notifications", "geolocation"] : [normalizeStoredPermission(options.permissionName)].filter(
3973
+ isStoredPermissionName
3974
+ );
3698
3975
  if (permissionMode === "grant") {
3699
3976
  for (const name of requested) current.add(name);
3700
3977
  } else {
@@ -3711,7 +3988,13 @@ async function envCommand(args, globalOptions) {
3711
3988
  await applyPermissionState(page.cdpClient, originFromUrl(currentUrl), nextPermissions);
3712
3989
  }
3713
3990
  if (outputAsJson) {
3714
- console.log(JSON.stringify({ session: session.id, action: permissionMode, permissions: result }, null, 2));
3991
+ console.log(
3992
+ JSON.stringify(
3993
+ { session: session.id, action: permissionMode, permissions: result },
3994
+ null,
3995
+ 2
3996
+ )
3997
+ );
3715
3998
  } else {
3716
3999
  console.log(formatPermissionOutput(session, result));
3717
4000
  }
@@ -3756,41 +4039,259 @@ async function envCommand(args, globalOptions) {
3756
4039
  }
3757
4040
  return;
3758
4041
  }
3759
- if (options.topCommand === "visibility") {
3760
- if (!options.visibility) {
3761
- throw new Error("visibility command requires: hidden or visible");
3762
- }
3763
- await runVisibilityCommand(options.visibility, page, session);
3764
- await updateSession(session.id, {
3765
- metadata: {
3766
- env: {
3767
- ...existingEnv,
3768
- visibility: options.visibility
3769
- }
3770
- }
3771
- });
3772
- return;
4042
+ if (options.topCommand === "visibility") {
4043
+ if (!options.visibility) {
4044
+ throw new Error("visibility command requires: hidden or visible");
4045
+ }
4046
+ await runVisibilityCommand(options.visibility, page, session);
4047
+ await updateSession(session.id, {
4048
+ metadata: {
4049
+ env: {
4050
+ ...existingEnv,
4051
+ visibility: options.visibility
4052
+ }
4053
+ }
4054
+ });
4055
+ return;
4056
+ }
4057
+ if (options.topCommand === "geolocation") {
4058
+ if (!options.geoAction) {
4059
+ throw new Error("geolocation command requires: set or clear");
4060
+ }
4061
+ await runGeolocationCommand(options.geoAction, options, page, session);
4062
+ await updateSession(session.id, {
4063
+ metadata: {
4064
+ env: {
4065
+ ...existingEnv,
4066
+ geolocation: options.geoAction === "clear" ? void 0 : {
4067
+ latitude: options.lat,
4068
+ longitude: options.lon,
4069
+ accuracy: options.accuracy ?? 1
4070
+ }
4071
+ }
4072
+ }
4073
+ });
4074
+ return;
4075
+ }
4076
+ throw new Error("Unknown env command. Run bp env --help for usage.");
4077
+ } finally {
4078
+ await browser.disconnect();
4079
+ }
4080
+ }
4081
+
4082
+ // src/cli/attach.ts
4083
+ async function applySessionEnvironment(page, currentUrl, settings) {
4084
+ if (!settings) {
4085
+ return;
4086
+ }
4087
+ const origin = originFromUrl(currentUrl);
4088
+ if (Array.isArray(settings.permissions)) {
4089
+ await applyPermissionState(page.cdpClient, origin, settings.permissions);
4090
+ }
4091
+ if (settings.geolocation) {
4092
+ await page.setGeolocation(settings.geolocation);
4093
+ }
4094
+ if (settings.visibility) {
4095
+ await applyVisibilityState(page.cdpClient, settings.visibility);
4096
+ }
4097
+ if (settings.network) {
4098
+ await applyNetworkOverride(page.cdpClient, settings.network);
4099
+ }
4100
+ }
4101
+ async function resolveSession(sessionId) {
4102
+ if (sessionId) {
4103
+ return loadSession(sessionId);
4104
+ }
4105
+ const session = await getDefaultSession();
4106
+ if (!session) {
4107
+ throw new Error('No session found. Run "bp connect" first.');
4108
+ }
4109
+ return session;
4110
+ }
4111
+ function isDaemonHealthy(session) {
4112
+ if (!session.daemon) return false;
4113
+ const daemonAge = Date.now() - new Date(session.daemon.startedAt).getTime();
4114
+ if (daemonAge > DAEMON_MAX_AGE_MS) {
4115
+ return false;
4116
+ }
4117
+ if (session.daemon.lastHeartbeat) {
4118
+ const heartbeatAge = Date.now() - new Date(session.daemon.lastHeartbeat).getTime();
4119
+ if (heartbeatAge > 9e4) {
4120
+ return false;
4121
+ }
4122
+ }
4123
+ return isDaemonAlive(session.daemon.pid);
4124
+ }
4125
+ async function cleanupStaleDaemon(session, reason) {
4126
+ console.warn(`[browser-pilot] Daemon unavailable (${reason}), falling back to direct WebSocket`);
4127
+ const sessionFilePath = getSessionFilePath(session.id);
4128
+ await clearDaemonFromSession(sessionFilePath);
4129
+ if (session.daemon?.socketPath) {
4130
+ try {
4131
+ const fsPromises = await import("fs/promises");
4132
+ await fsPromises.unlink(session.daemon.socketPath).catch(() => {
4133
+ });
4134
+ } catch {
4135
+ }
4136
+ }
4137
+ }
4138
+ async function attachSession(session, options = {}) {
4139
+ if (session.daemon) {
4140
+ if (!isDaemonHealthy(session)) {
4141
+ const reason = !isDaemonAlive(session.daemon.pid) ? "PID not alive" : "daemon expired (>60min)";
4142
+ await cleanupStaleDaemon(session, reason);
4143
+ } else {
4144
+ try {
4145
+ const { createDaemonTransport } = await import("./transport-WHEBAZUP.mjs");
4146
+ const { createCDPClientFromTransport } = await import("./client-JWWZWO6L.mjs");
4147
+ const transport = await createDaemonTransport(session.daemon.socketPath);
4148
+ const cdp = createCDPClientFromTransport(transport, {
4149
+ debug: options.trace
4150
+ });
4151
+ const { Browser: BrowserClass } = await import("./browser-4ZHNAQR5.mjs");
4152
+ const { Page: PageClass } = await import("./page-SD64DY3F.mjs");
4153
+ const browser2 = BrowserClass.fromCDP(cdp, session);
4154
+ const page2 = session.daemon.cdpSessionId && session.targetId ? addBatchToPage(
4155
+ await (async () => {
4156
+ cdp.setSessionId(session.daemon?.cdpSessionId);
4157
+ const attachedPage = new PageClass(cdp, session.targetId);
4158
+ await attachedPage.init();
4159
+ return attachedPage;
4160
+ })()
4161
+ ) : addBatchToPage(await browser2.page(void 0, { targetId: session.targetId }));
4162
+ const currentUrl2 = await page2.url();
4163
+ await applySessionEnvironment(page2, currentUrl2, session.metadata?.env);
4164
+ const refCache2 = session.metadata?.refCache;
4165
+ if (refCache2 && refCache2.url === currentUrl2) {
4166
+ page2.importRefMap(refCache2.refMap);
4167
+ }
4168
+ return { session, browser: browser2, page: page2, viaDaemon: true };
4169
+ } catch (err) {
4170
+ const reason = err instanceof Error ? err.message : String(err);
4171
+ await cleanupStaleDaemon(session, reason);
4172
+ }
4173
+ }
4174
+ }
4175
+ let browser;
4176
+ try {
4177
+ browser = await connect({
4178
+ provider: session.provider,
4179
+ wsUrl: session.wsUrl,
4180
+ debug: options.trace
4181
+ });
4182
+ } catch {
4183
+ await deleteSession(session.id);
4184
+ throw new Error(
4185
+ `Session "${session.id}" is no longer valid (browser may have closed).
4186
+ Session file has been cleaned up. Run "bp connect" to create a new session.`
4187
+ );
4188
+ }
4189
+ const page = addBatchToPage(await browser.page(void 0, { targetId: session.targetId }));
4190
+ const currentUrl = await page.url();
4191
+ await applySessionEnvironment(page, currentUrl, session.metadata?.env);
4192
+ const refCache = session.metadata?.refCache;
4193
+ if (refCache && refCache.url === currentUrl) {
4194
+ page.importRefMap(refCache.refMap);
4195
+ }
4196
+ return { session, browser, page, viaDaemon: false };
4197
+ }
4198
+
4199
+ // src/cli/commands/eval.ts
4200
+ var EVAL_HELP = `
4201
+ bp eval - Evaluate JavaScript in the browser
4202
+
4203
+ Convenience wrapper around exec's evaluate action.
4204
+ No JSON escaping needed -- just pass a JS expression directly.
4205
+ Use this as an escape hatch after higher-level commands like snapshot, text, review, and exec.
4206
+
4207
+ Usage:
4208
+ bp eval '<expression>' Evaluate inline JavaScript
4209
+ bp eval -f <file> Evaluate JavaScript from a file
4210
+ echo '<expr>' | bp eval Evaluate from stdin
4211
+
4212
+ Local options:
4213
+ -f, --file <path> Read JavaScript from a file
4214
+ --wrap Wrap the expression in an async IIFE
4215
+
4216
+ Global options:
4217
+ -s, --session <id> Session to use (default: most recent)
4218
+ --json Output JSON
4219
+ --pretty Output readable text (default)
4220
+ --debug Enable CDP transport debugging
4221
+ -h, --help Show this help
4222
+
4223
+ Examples:
4224
+ bp eval 'document.title'
4225
+ bp eval 'document.querySelectorAll("a").length'
4226
+ bp eval -f scrape.js
4227
+ bp eval --wrap 'await fetch("/health").then((r) => r.status)'
4228
+ `.trimEnd();
4229
+ function parseEvalArgs(args) {
4230
+ const options = {};
4231
+ let expression;
4232
+ for (let i = 0; i < args.length; i++) {
4233
+ const arg = args[i];
4234
+ if (arg === "-f" || arg === "--file") {
4235
+ options.file = args[++i];
4236
+ } else if (arg === "--wrap") {
4237
+ options.wrap = true;
4238
+ } else if (!expression && !arg.startsWith("-")) {
4239
+ expression = arg;
4240
+ }
4241
+ }
4242
+ return { expression, options };
4243
+ }
4244
+ function normalizeEvalExpression(expression, wrap = false) {
4245
+ const trimmed = expression.trim();
4246
+ const needsWrap = wrap || trimmed.includes("=>") || /\bawait\b/.test(trimmed);
4247
+ if (!needsWrap) {
4248
+ return trimmed;
4249
+ }
4250
+ if (wrap || /\bawait\b/.test(trimmed)) {
4251
+ return `(async () => (${trimmed}))()`;
4252
+ }
4253
+ return `(() => (${trimmed}))()`;
4254
+ }
4255
+ async function evalCommand(args, globalOptions) {
4256
+ if (globalOptions.help) {
4257
+ console.log(EVAL_HELP);
4258
+ return;
4259
+ }
4260
+ const { expression: argExpression, options: evalOptions } = parseEvalArgs(args);
4261
+ let expression = argExpression;
4262
+ if (evalOptions.file) {
4263
+ const fs9 = await import("fs/promises");
4264
+ expression = await fs9.readFile(evalOptions.file, "utf-8");
4265
+ }
4266
+ if (!expression && !process.stdin.isTTY) {
4267
+ const chunks = [];
4268
+ for await (const chunk of process.stdin) {
4269
+ chunks.push(chunk);
3773
4270
  }
3774
- if (options.topCommand === "geolocation") {
3775
- if (!options.geoAction) {
3776
- throw new Error("geolocation command requires: set or clear");
3777
- }
3778
- await runGeolocationCommand(options.geoAction, options, page, session);
3779
- await updateSession(session.id, {
3780
- metadata: {
3781
- env: {
3782
- ...existingEnv,
3783
- geolocation: options.geoAction === "clear" ? void 0 : {
3784
- latitude: options.lat,
3785
- longitude: options.lon,
3786
- accuracy: options.accuracy ?? 1
3787
- }
3788
- }
3789
- }
3790
- });
3791
- return;
4271
+ expression = Buffer.concat(chunks).toString("utf-8").trim();
4272
+ }
4273
+ if (!expression) {
4274
+ throw new Error(
4275
+ "No expression provided.\n\nUsage:\n bp eval 'document.title'\n bp eval -f script.js\n echo 'document.title' | bp eval"
4276
+ );
4277
+ }
4278
+ const session = await resolveSession(globalOptions.session);
4279
+ const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
4280
+ try {
4281
+ const step = {
4282
+ action: "evaluate",
4283
+ value: normalizeEvalExpression(expression, evalOptions.wrap)
4284
+ };
4285
+ const result = await page.batch([step]);
4286
+ const stepResult = result.steps[0];
4287
+ if (!stepResult.success) {
4288
+ throw new Error(stepResult.error ?? "Evaluation failed");
3792
4289
  }
3793
- throw new Error("Unknown env command. Run bp env --help for usage.");
4290
+ output(
4291
+ globalOptions.format === "json" ? { success: true, result: stepResult.result } : stepResult.result,
4292
+ globalOptions.format
4293
+ );
4294
+ await updateSession(session.id, { currentUrl: await page.url() });
3794
4295
  } finally {
3795
4296
  await browser.disconnect();
3796
4297
  }
@@ -3819,14 +4320,10 @@ Usage:
3819
4320
  bp exec -f <file> Execute action(s) from a JSON file
3820
4321
  echo '<json>' | bp exec Execute action(s) from stdin
3821
4322
 
3822
- Options:
3823
- -f, --file <path> Read actions from a JSON file
3824
- -o, --output <path> Write command output to a file instead of stdout
3825
- --dialog <mode> Handle native dialogs: accept | dismiss
3826
- -s, --session <id> Session to use (default: most recent)
3827
- -f, --format <fmt> Output format: json | pretty (default: pretty)
3828
- --json Alias for -f json
3829
- --debug Enable CDP transport debugging (global option)
4323
+ Local options:
4324
+ -f, --file <path> Read actions from a JSON file
4325
+ -o, --output <path> Write command output to a file instead of stdout
4326
+ --dialog <mode> Handle native dialogs: accept | dismiss
3830
4327
 
3831
4328
  Recording:
3832
4329
  --record Enable screenshot recording
@@ -3836,6 +4333,11 @@ Recording:
3836
4333
  --no-highlights Disable visual highlights on screenshots
3837
4334
  Sensitive fields (passwords, OTPs, card inputs) are redacted
3838
4335
 
4336
+ Global options:
4337
+ -s, --session <id> Session to use (default: most recent)
4338
+ --json Output JSON
4339
+ --pretty Output readable text (default)
4340
+ --debug Enable CDP transport debugging
3839
4341
  -h, --help Show this help
3840
4342
 
3841
4343
  Examples:
@@ -4062,6 +4564,22 @@ Run 'bp actions' for complete action reference.${evalTip}`
4062
4564
  data: stepResult.result
4063
4565
  });
4064
4566
  }
4567
+ if (stepResult.outcomeStatus) {
4568
+ logger.logTrace({
4569
+ channel: "action",
4570
+ event: `action.outcome.${stepResult.outcomeStatus}`,
4571
+ summary: `Outcome: ${stepResult.outcomeStatus}${stepResult.retrySafe === false ? " (unsafe to retry)" : ""}`,
4572
+ data: {
4573
+ outcomeStatus: stepResult.outcomeStatus,
4574
+ retrySafe: stepResult.retrySafe,
4575
+ matchedConditions: stepResult.matchedConditions?.map((mc) => ({
4576
+ kind: mc.condition.kind,
4577
+ matched: mc.matched,
4578
+ detail: mc.detail
4579
+ }))
4580
+ }
4581
+ });
4582
+ }
4065
4583
  }
4066
4584
  if (result.recordingManifest && session.exportLog) {
4067
4585
  mirrorRecordingToExport(result.recordingManifest, session.exportLog);
@@ -4103,7 +4621,10 @@ Run 'bp actions' for complete action reference.${evalTip}`
4103
4621
  selectorUsed: s.selectorUsed,
4104
4622
  error: s.error,
4105
4623
  text: s.text,
4106
- result: s.result
4624
+ result: s.result,
4625
+ ...s.outcomeStatus !== void 0 ? { outcomeStatus: s.outcomeStatus } : {},
4626
+ ...s.matchedConditions !== void 0 ? { matchedConditions: s.matchedConditions } : {},
4627
+ ...s.retrySafe !== void 0 ? { retrySafe: s.retrySafe } : {}
4107
4628
  }));
4108
4629
  const payload = {
4109
4630
  success: result.success,
@@ -4213,19 +4734,29 @@ function formatInteractiveElementsPretty(elements, limit = elements.length) {
4213
4734
  var FORMS_HELP = `
4214
4735
  bp forms - List form controls on the current page
4215
4736
 
4737
+ When to use:
4738
+ You need field names, types, values, or disabled state without the rest of the page.
4739
+
4740
+ When not to use:
4741
+ You need clickable refs or a broader page summary. Use \`bp snapshot -i\` or \`bp page\`.
4742
+
4216
4743
  Usage:
4217
4744
  bp forms [options]
4218
4745
 
4219
- Options:
4746
+ Global options:
4220
4747
  -s, --session <id> Session to use (default: most recent)
4221
- -f, --format <fmt> json | pretty (default: pretty)
4222
- --json Alias for -f json
4223
- --trace Enable debug tracing
4748
+ --json Output JSON
4749
+ --pretty Output readable text (default)
4750
+ --debug Enable CDP transport debugging
4224
4751
  -h, --help Show this help
4225
4752
 
4226
4753
  Examples:
4227
4754
  bp forms
4228
4755
  bp forms --json
4756
+
4757
+ Likely next commands:
4758
+ bp exec '[{"action":"fill","selector":"ref:e4","value":"..."}]'
4759
+ bp review --json
4229
4760
  `.trimEnd();
4230
4761
  async function formsCommand(_args, globalOptions) {
4231
4762
  if (globalOptions.help) {
@@ -4262,11 +4793,14 @@ Usage:
4262
4793
  bp list -s <id> --log-path Print path to session log file (for analysis)
4263
4794
 
4264
4795
  Options:
4265
- -s, --session <id> Target session (or uses default session)
4266
4796
  --info Show session details and log statistics
4267
4797
  --log-tail [n] Show last n action log entries (default: 20)
4268
4798
  --log-path Print absolute path to log.jsonl file
4269
- -f json, --json Machine-readable JSON output
4799
+
4800
+ Global options:
4801
+ -s, --session <id> Target session (or uses default session)
4802
+ --json Machine-readable JSON output
4803
+ --pretty Output readable text (default)
4270
4804
  -h, --help Show this help
4271
4805
 
4272
4806
  Examples:
@@ -4448,7 +4982,11 @@ When to use:
4448
4982
  You want a quick summary of URL, title, headings, forms, and interactive controls.
4449
4983
 
4450
4984
  When not to use:
4451
- You need the full accessibility tree or reusable refs for precise automation. Use \`bp snapshot\`.
4985
+ You need the full accessibility tree or the full ref inventory for precise automation. Use \`bp snapshot\`.
4986
+
4987
+ Common mistake:
4988
+ Treating \`bp page\` as exhaustive. It is a compact overview; the Actions section caches reusable refs,
4989
+ but use \`bp snapshot -i\` when you need the full actionable surface.
4452
4990
 
4453
4991
  Likely next commands:
4454
4992
  bp snapshot -i
@@ -4458,11 +4996,11 @@ Likely next commands:
4458
4996
  Usage:
4459
4997
  bp page [options]
4460
4998
 
4461
- Options:
4999
+ Global options:
4462
5000
  -s, --session <id> Session to use (default: most recent)
4463
- -f, --format <fmt> json | pretty (default: pretty)
4464
- --json Alias for -f json
4465
- --trace Enable debug tracing
5001
+ --json Output JSON
5002
+ --pretty Output readable text (default)
5003
+ --debug Enable CDP transport debugging
4466
5004
  -h, --help Show this help
4467
5005
 
4468
5006
  Examples:
@@ -4530,7 +5068,16 @@ async function pageCommand(_args, globalOptions) {
4530
5068
  globalOptions.format === "json" ? summary : formatPageSummary(summary),
4531
5069
  globalOptions.format
4532
5070
  );
4533
- await updateSession(session.id, { currentUrl: url });
5071
+ await updateSession(session.id, {
5072
+ currentUrl: url,
5073
+ metadata: {
5074
+ refCache: {
5075
+ url,
5076
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
5077
+ refMap: page.exportRefMap()
5078
+ }
5079
+ }
5080
+ });
4534
5081
  } finally {
4535
5082
  await browser.disconnect();
4536
5083
  }
@@ -4541,14 +5088,14 @@ var QUICKSTART = `
4541
5088
  browser-pilot CLI - Quick Start Guide
4542
5089
 
4543
5090
  STEP 1: CONNECT TO A BROWSER
4544
- bp connect --provider generic --name mysite
5091
+ bp connect --name mysite
4545
5092
 
4546
5093
  This creates a session. The CLI remembers it for subsequent commands.
4547
5094
 
4548
5095
  STEP 2: NAVIGATE
4549
- bp exec '{"action":"goto","url":"https://example.com"}'
5096
+ bp exec -s mysite '{"action":"goto","url":"https://example.com"}'
4550
5097
 
4551
- STEP 3: GET PAGE SNAPSHOT
5098
+ STEP 3: CHOOSE THE RIGHT INSPECTION COMMAND
4552
5099
  bp snapshot -i
4553
5100
 
4554
5101
  Shows only interactive elements (buttons, inputs, links) with refs:
@@ -4556,39 +5103,48 @@ STEP 3: GET PAGE SNAPSHOT
4556
5103
  textbox "Email" ref:e3
4557
5104
  link "Forgot password?" ref:e6
4558
5105
 
4559
- Other formats:
4560
- bp snapshot --format text # Full accessibility tree (all elements)
4561
- bp snapshot --json # Full snapshot as JSON
5106
+ Other inspection commands:
5107
+ bp page # Compact overview: URL, title, headings, forms, actions
5108
+ bp text # Readable page copy or policy text
5109
+ bp review --json # Structured business state after actions
5110
+ bp diagnose 'submit' # Debug selector or targeting failures
4562
5111
 
4563
5112
  STEP 4: INTERACT USING REFS
4564
- bp exec '{"action":"fill","selector":"ref:e3","value":"test@example.com"}'
4565
- bp exec '{"action":"click","selector":"ref:e2"}'
5113
+ bp exec -s mysite '{"action":"fill","selector":"ref:e3","value":"test@example.com"}'
5114
+ bp exec -s mysite '{"action":"click","selector":"ref:e2"}'
4566
5115
 
4567
5116
  STEP 5: BATCH MULTIPLE ACTIONS
4568
- bp exec '[
5117
+ bp exec -s mysite '[
4569
5118
  {"action":"fill","selector":"ref:e3","value":"user@test.com"},
4570
5119
  {"action":"click","selector":"ref:e2"},
4571
5120
  {"action":"snapshot"}
4572
5121
  ]'
4573
5122
 
4574
5123
  FOR AI AGENTS
4575
- Use bp snapshot -i for most workflows - shows only actionable elements.
5124
+ Start with:
5125
+ bp --help
5126
+ bp --version
5127
+
5128
+ Use bp snapshot -i for most workflows - it shows actionable elements.
4576
5129
  Add --json for machine-readable output:
4577
- bp snapshot -i --json
4578
- bp exec '{"action":"click","selector":"ref:e3"}' --json
5130
+ bp snapshot -i -s mysite --json
5131
+ bp exec -s mysite '{"action":"click","selector":"ref:e3"}' --json
4579
5132
 
4580
5133
  PAGE DISCOVERY SHORTCUTS
4581
- bp page # URL, title, headings, forms, and interactive controls
4582
- bp forms # Structured list of form fields only
4583
- bp targets # All available browser tabs
4584
- bp connect --new-tab --url https://example.com
4585
- # Start from a fresh tab instead of reusing one
5134
+ bp page # URL, title, headings, forms, and interactive controls
5135
+ bp forms # Structured list of form fields only
5136
+ bp text --selector '#main' # Focused readable text extraction
5137
+ bp review --json # Structured business state
5138
+ bp targets # All available browser tabs
5139
+ bp connect --new-tab --page-url https://example.com
5140
+ # Convenience: start from a fresh tab
4586
5141
 
4587
5142
  TIPS
4588
- \u2022 Refs (e1, e2...) are stable within a page - prefer them over CSS selectors
4589
- \u2022 After navigation, take a new snapshot to get updated refs
4590
- \u2022 Use multi-selectors for resilience: ["ref:e3", "#email", "input[type=email]"]
4591
- \u2022 Add "optional":true to skip elements that may not exist
5143
+ - Refs (e1, e2...) are stable within the current page state
5144
+ - After navigation or major DOM changes, take a new snapshot to refresh refs
5145
+ - Use multi-selectors for resilience: ["ref:e3", "#email", "input[type=email]"]
5146
+ - Add "optional":true to skip elements that may not exist
5147
+ - Use bp eval only as an escape hatch when higher-level commands are insufficient
4592
5148
 
4593
5149
  SELECTOR PRIORITY
4594
5150
  1. ref:e5 From snapshot - most reliable
@@ -5478,20 +6034,24 @@ var Recorder = class {
5478
6034
  awaitPromise: false
5479
6035
  });
5480
6036
  this.bindingHandler = (params) => {
6037
+ const payload = readString(params["payload"]);
6038
+ if (!payload) {
6039
+ return;
6040
+ }
5481
6041
  if (params["name"] === RECORDER_BINDING_NAME) {
5482
- this.handleBindingCall(params["payload"]);
6042
+ this.handleBindingCall(payload);
5483
6043
  } else if (params["name"] === TRACE_BINDING_NAME) {
5484
- this.handleTraceBindingCall(params["payload"]);
6044
+ this.handleTraceBindingCall(payload);
5485
6045
  }
5486
6046
  };
5487
6047
  this.cdp.on("Runtime.bindingCalled", this.bindingHandler);
5488
6048
  this.subscribeTrace("Runtime.consoleAPICalled", (params) => {
5489
- const type = String(params["type"] ?? "log");
6049
+ const type = readStringOr(params["type"], "log");
5490
6050
  if (type !== "log" && type !== "warn" && type !== "error") {
5491
6051
  return;
5492
6052
  }
5493
6053
  const args = Array.isArray(params["args"]) ? params["args"] : [];
5494
- const text = args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ");
6054
+ const text = args.map(formatConsoleArg).filter(Boolean).join(" ");
5495
6055
  this.traceEvents.push(
5496
6056
  normalizeTraceEvent({
5497
6057
  traceId: createTraceId("console"),
@@ -5517,7 +6077,7 @@ var Recorder = class {
5517
6077
  channel: "runtime",
5518
6078
  event: "runtime.exception",
5519
6079
  severity: "error",
5520
- summary: String(details["text"] ?? "Runtime exception"),
6080
+ summary: readString(details["text"]) ?? "Runtime exception",
5521
6081
  data: details,
5522
6082
  url: this.startUrl
5523
6083
  })
@@ -5758,6 +6318,7 @@ var Recorder = class {
5758
6318
  this.subscribeNetwork("Network.webSocketClosed", (params) => {
5759
6319
  const requestId = params["requestId"];
5760
6320
  if (!this.wsUrls.has(requestId)) return;
6321
+ const url = this.wsUrls.get(requestId);
5761
6322
  this.wsUrls.delete(requestId);
5762
6323
  const now = Date.now();
5763
6324
  this.wsEvents.push({
@@ -5775,10 +6336,10 @@ var Recorder = class {
5775
6336
  event: "ws.connection.closed",
5776
6337
  severity: "warn",
5777
6338
  summary: `WebSocket closed ${requestId}`,
5778
- data: { url: this.wsUrls.get(requestId) ?? null },
6339
+ data: { url: url ?? null },
5779
6340
  connectionId: requestId,
5780
6341
  requestId,
5781
- url: this.wsUrls.get(requestId)
6342
+ url
5782
6343
  })
5783
6344
  );
5784
6345
  });
@@ -5940,11 +6501,6 @@ var Recorder = class {
5940
6501
  return entries;
5941
6502
  }
5942
6503
  };
5943
- function globToRegex(pattern) {
5944
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
5945
- const withWildcards = escaped.replace(/\*/g, ".*");
5946
- return new RegExp(`^${withWildcards}$`);
5947
- }
5948
6504
 
5949
6505
  // src/cli/commands/record.ts
5950
6506
  var RECORD_HELP = `
@@ -6074,10 +6630,14 @@ async function resolveConnection3(sessionId, useLatestSession, debug) {
6074
6630
  }
6075
6631
  let wsUrl;
6076
6632
  try {
6077
- wsUrl = await getBrowserWebSocketUrl("localhost:9222");
6078
- } catch {
6633
+ wsUrl = (await resolveCLIEndpoint()).wsUrl;
6634
+ } catch (error) {
6079
6635
  throw new Error(
6080
- "Could not auto-discover browser.\nEither start Chrome with --remote-debugging-port=9222 or pass -s to reuse a session."
6636
+ formatBrowserDiscoveryError(error, {
6637
+ explicitHint: " - Create a session first: bp connect --browser-url <ws-url>",
6638
+ reuseSessionHint: "bp record -s <session-id>",
6639
+ latestSessionHint: "bp record -s"
6640
+ })
6081
6641
  );
6082
6642
  }
6083
6643
  const browser = await connect({
@@ -6216,7 +6776,9 @@ async function runRecordCapture(args, globalOptions) {
6216
6776
  const canonicalPath = join9(sessionDir, DEFAULT_ARTIFACT);
6217
6777
  const outputPath = resolve5(args.file ?? DEFAULT_ARTIFACT);
6218
6778
  nodeFs2.mkdirSync(screenshotDir, { recursive: true });
6219
- const existingArtifact = existsSync6(canonicalPath) ? canonicalizeRecordingArtifact(JSON.parse(nodeFs2.readFileSync(canonicalPath, "utf-8"))) : null;
6779
+ const existingArtifact = existsSync6(canonicalPath) ? canonicalizeRecordingArtifact(
6780
+ JSON.parse(nodeFs2.readFileSync(canonicalPath, "utf-8"))
6781
+ ) : null;
6220
6782
  const recordingFrames = existingArtifact ? artifactToFrames(existingArtifact) : [];
6221
6783
  let listenConfig = {
6222
6784
  mode: typeof args.listen === "string" ? args.listen : "all",
@@ -6340,7 +6902,9 @@ async function runRecordCapture(args, globalOptions) {
6340
6902
  console.log(`Use: bp record summary ${outputPath}`);
6341
6903
  }
6342
6904
  } catch (error) {
6343
- console.error(`Error saving recording: ${error instanceof Error ? error.message : String(error)}`);
6905
+ console.error(
6906
+ `Error saving recording: ${error instanceof Error ? error.message : String(error)}`
6907
+ );
6344
6908
  process.exit(1);
6345
6909
  }
6346
6910
  };
@@ -6457,6 +7021,114 @@ async function recordCommand(args, globalOptions) {
6457
7021
  }
6458
7022
  }
6459
7023
 
7024
+ // src/cli/commands/review.ts
7025
+ var REVIEW_HELP = `
7026
+ bp review - Extract structured business state from the current page
7027
+
7028
+ When to use:
7029
+ You want a structured summary of the page: headings, forms, alerts, tables,
7030
+ key-value pairs, and status labels. Useful for verifying business state after
7031
+ an action sequence, especially on detail, checkout, and confirmation pages.
7032
+
7033
+ When not to use:
7034
+ You need the full accessibility tree with refs. Use \`bp snapshot\`.
7035
+ You want a compact overview. Use \`bp page\`.
7036
+ You are on a dense catalog or marketing page with lots of nav chrome. Use \`bp text\` or \`bp page\`.
7037
+
7038
+ Likely next commands:
7039
+ bp snapshot -i
7040
+ bp exec '[{"action":"click","selector":"ref:e4"}]'
7041
+
7042
+ Usage:
7043
+ bp review [options]
7044
+
7045
+ Global options:
7046
+ -s, --session <id> Session to use (default: most recent)
7047
+ --json Output JSON
7048
+ --pretty Output readable text (default)
7049
+ --debug Enable CDP transport debugging
7050
+ -h, --help Show this help
7051
+
7052
+ Examples:
7053
+ bp review
7054
+ bp review --json
7055
+ bp review -s my-session
7056
+ `.trimEnd();
7057
+ function formatReviewPretty(review) {
7058
+ const lines = [];
7059
+ lines.push(`URL: ${review.url}`);
7060
+ lines.push(`Title: ${review.title}`);
7061
+ lines.push("", "Headings:");
7062
+ if (review.headings.length === 0) {
7063
+ lines.push(" (none)");
7064
+ } else {
7065
+ for (const h of review.headings) {
7066
+ lines.push(` ${h}`);
7067
+ }
7068
+ }
7069
+ if (review.alerts.length > 0) {
7070
+ lines.push("", "Alerts:");
7071
+ for (const a of review.alerts) {
7072
+ lines.push(` ${a}`);
7073
+ }
7074
+ }
7075
+ if (review.statusLabels.length > 0) {
7076
+ lines.push("", "Status:");
7077
+ for (const s of review.statusLabels) {
7078
+ lines.push(` ${s}`);
7079
+ }
7080
+ }
7081
+ if (review.keyValues.length > 0) {
7082
+ lines.push("", "Key-Value Pairs:");
7083
+ for (const kv of review.keyValues) {
7084
+ lines.push(` ${kv.key}: ${kv.value}`);
7085
+ }
7086
+ }
7087
+ if (review.tables.length > 0) {
7088
+ lines.push("", "Tables:");
7089
+ for (const table of review.tables) {
7090
+ if (table.headers.length > 0) {
7091
+ lines.push(` | ${table.headers.join(" | ")} |`);
7092
+ lines.push(` | ${table.headers.map(() => "---").join(" | ")} |`);
7093
+ }
7094
+ for (const row of table.rows) {
7095
+ lines.push(` | ${row.join(" | ")} |`);
7096
+ }
7097
+ lines.push("");
7098
+ }
7099
+ }
7100
+ lines.push("", "Forms:");
7101
+ if (review.forms.length === 0) {
7102
+ lines.push(" (none)");
7103
+ } else {
7104
+ for (const f of review.forms) {
7105
+ const disabled = f.disabled ? " (disabled)" : "";
7106
+ const label = f.label ?? "(unlabeled)";
7107
+ lines.push(` ${label} [${f.type}]: ${f.value ?? ""}${disabled}`);
7108
+ }
7109
+ }
7110
+ return lines.join("\n");
7111
+ }
7112
+ async function reviewCommand(_args, globalOptions) {
7113
+ if (globalOptions.help) {
7114
+ process.stdout.write(`${REVIEW_HELP}
7115
+ `);
7116
+ return;
7117
+ }
7118
+ const session = await resolveSession(globalOptions.session);
7119
+ const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
7120
+ try {
7121
+ const review = await page.review();
7122
+ output(
7123
+ globalOptions.format === "json" ? review : formatReviewPretty(review),
7124
+ globalOptions.format
7125
+ );
7126
+ await updateSession(session.id, { currentUrl: review.url });
7127
+ } finally {
7128
+ await browser.disconnect();
7129
+ }
7130
+ }
7131
+
6460
7132
  // src/cli/commands/run.ts
6461
7133
  import { readFile } from "fs/promises";
6462
7134
  import { resolve as resolve6 } from "path";
@@ -6599,17 +7271,26 @@ ${result.success ? "Workflow passed" : "Workflow failed"} in ${result.totalDurat
6599
7271
  var SCREENSHOT_HELP = `
6600
7272
  bp screenshot - Take a screenshot of the current page
6601
7273
 
7274
+ When to use:
7275
+ You need a visual artifact, regression evidence, or a screenshot to attach elsewhere.
7276
+
7277
+ When not to use:
7278
+ You need readable copy or structured business data. Use \`bp text\`, \`bp page\`, or \`bp review\`.
7279
+
6602
7280
  Usage:
6603
7281
  bp screenshot [options]
6604
7282
 
6605
- Options:
7283
+ Local options:
6606
7284
  -o, --output <path> Save screenshot to file (default: print base64 to stdout)
6607
7285
  -f, --format <type> Image format: png | jpeg | webp (default: png)
6608
7286
  -q, --quality <n> Image quality 0-100 (jpeg/webp only)
6609
7287
  --full-page Capture the full scrollable page
7288
+
7289
+ Global options:
6610
7290
  -s, --session <id> Session to use (default: most recent)
6611
- --json Output as JSON (base64 data + metadata)
6612
- --trace Enable debug tracing
7291
+ --json Output JSON (base64 data + metadata)
7292
+ --pretty Output readable text for file writes (default)
7293
+ --debug Enable CDP transport debugging
6613
7294
  -h, --help Show this help
6614
7295
 
6615
7296
  Examples:
@@ -6955,23 +7636,27 @@ Common mistake:
6955
7636
  Usage:
6956
7637
  bp snapshot [options]
6957
7638
 
6958
- Options:
6959
- -i, --interactive Show only interactive elements (buttons, inputs, links)
6960
- -f, --format <type> Output format: full | interactive | text (default: text)
6961
- --role <roles> Filter snapshot to accessibility roles (for example: radio,checkbox)
6962
- -o, --output <path> Write command output to a file instead of stdout
6963
- -d, --diff <file> Compare current page against a saved snapshot JSON
6964
- --inspect Inject visual ref labels onto the page (auto-removes after 10s)
6965
- --keep Keep visual ref labels visible (use with --inspect)
6966
- -s, --session <id> Session to use (default: most recent)
6967
- -f, --format <fmt> Output format: json | pretty (default: pretty)
6968
- --json Alias for -f json
6969
- --debug Enable CDP transport debugging (global option)
6970
- -h, --help Show this help
7639
+ Local options:
7640
+ -i, --interactive Shortcut for --view interactive
7641
+ --view <type> Snapshot view: full | interactive | text (default: text)
7642
+ -f, --format <type> Backward-compatible alias for --view
7643
+ --role <roles> Filter snapshot to accessibility roles (for example: radio,checkbox)
7644
+ -o, --output <path> Write command output to a file instead of stdout
7645
+ -d, --diff <file> Compare current page against a saved snapshot JSON
7646
+ --inspect Inject visual ref labels onto the page (auto-removes after 10s)
7647
+ --keep Keep visual ref labels visible (use with --inspect)
7648
+
7649
+ Global options:
7650
+ -s, --session <id> Session to use (default: most recent)
7651
+ --json Output JSON; without --view this returns the full snapshot payload
7652
+ --pretty Output readable text (default)
7653
+ --debug Enable CDP transport debugging
7654
+ -h, --help Show this help
6971
7655
 
6972
7656
  Examples:
6973
7657
  bp snapshot # Full accessibility tree as readable text
6974
7658
  bp snapshot -i # Interactive elements only; best default for automation
7659
+ bp snapshot --view full # Full structured snapshot
6975
7660
  bp snapshot --role radio,checkbox # Focus on specific control roles
6976
7661
  bp snapshot --json > page.json # Save full snapshot to file
6977
7662
  bp snapshot --diff before.json # Show what changed since before.json
@@ -6988,7 +7673,7 @@ function parseSnapshotArgs(args) {
6988
7673
  };
6989
7674
  for (let i = 0; i < args.length; i++) {
6990
7675
  const arg = args[i];
6991
- if (arg === "--format" || arg === "-f") {
7676
+ if (arg === "--view" || arg === "--format" || arg === "-f") {
6992
7677
  options.format = args[++i];
6993
7678
  options.formatExplicit = true;
6994
7679
  } else if (arg === "--diff" || arg === "-d") {
@@ -7121,11 +7806,11 @@ bp targets - List page tabs available in the connected browser
7121
7806
  Usage:
7122
7807
  bp targets [options]
7123
7808
 
7124
- Options:
7809
+ Global options:
7125
7810
  -s, --session <id> Session to use (default: most recent)
7126
- -f, --format <fmt> json | pretty (default: pretty)
7127
- --json Alias for -f json
7128
- --trace Enable debug tracing
7811
+ --json Output JSON
7812
+ --pretty Output readable text (default)
7813
+ --debug Enable CDP transport debugging
7129
7814
  -h, --help Show this help
7130
7815
 
7131
7816
  Examples:
@@ -7190,28 +7875,31 @@ Common mistake:
7190
7875
  Usage:
7191
7876
  bp text [options]
7192
7877
 
7193
- Options:
7194
- --selector <sel> Extract text from a specific element (default: entire page)
7195
- -s, --session <id> Session to use (default: most recent)
7196
- -f, --format <fmt> Output format: json | pretty (default: pretty)
7197
- --json Alias for -f json
7198
- --debug Enable CDP transport debugging (global option)
7199
- -h, --help Show this help
7878
+ Local options:
7879
+ --selector <selector> Extract text from a specific element (default: entire page)
7880
+
7881
+ Global options:
7882
+ -s, --session <id> Session to use (default: most recent)
7883
+ --json Output JSON with text, URL, and selector
7884
+ --pretty Output readable text only (default)
7885
+ --debug Enable CDP transport debugging
7886
+ -h, --help Show this help
7200
7887
 
7201
7888
  Examples:
7202
7889
  bp text # Extract all text from the page
7203
7890
  bp text --selector '#main' # Extract text from #main element only
7204
- bp text --json # Output as JSON with URL and selector info
7891
+ bp text -s dev --json # Output JSON with URL and selector info
7205
7892
 
7206
7893
  Likely next commands:
7207
7894
  bp snapshot -i
7895
+ bp review --json
7208
7896
  bp exec '[{"action":"assertText","expect":"..."}]'
7209
7897
  `.trimEnd();
7210
7898
  function parseTextArgs(args) {
7211
7899
  const options = {};
7212
7900
  for (let i = 0; i < args.length; i++) {
7213
7901
  const arg = args[i];
7214
- if (arg === "--selector" || arg === "-s") {
7902
+ if (arg === "--selector") {
7215
7903
  options.selector = args[++i];
7216
7904
  } else if (arg === "-h" || arg === "--help") {
7217
7905
  options.help = true;
@@ -7219,6 +7907,9 @@ function parseTextArgs(args) {
7219
7907
  }
7220
7908
  return options;
7221
7909
  }
7910
+ function looksLikeSelector(value) {
7911
+ return value.startsWith("#") || value.startsWith(".") || value.startsWith("[") || value.startsWith("/") || value.startsWith("ref:") || value.includes(">");
7912
+ }
7222
7913
  async function textCommand(args, globalOptions) {
7223
7914
  const options = parseTextArgs(args);
7224
7915
  if (options.help || globalOptions.help) {
@@ -7227,7 +7918,18 @@ async function textCommand(args, globalOptions) {
7227
7918
  }
7228
7919
  let session;
7229
7920
  if (globalOptions.session) {
7230
- session = await loadSession(globalOptions.session);
7921
+ try {
7922
+ session = await loadSession(globalOptions.session);
7923
+ } catch (error) {
7924
+ if (!options.selector && looksLikeSelector(globalOptions.session)) {
7925
+ throw new Error(
7926
+ `bp text uses --selector for element targeting. "-s" is reserved for sessions.
7927
+
7928
+ Try: bp text --selector ${JSON.stringify(globalOptions.session)}`
7929
+ );
7930
+ }
7931
+ throw error;
7932
+ }
7231
7933
  } else {
7232
7934
  session = await getDefaultSession();
7233
7935
  if (!session) {
@@ -7464,7 +8166,12 @@ function evaluateWatchAssertion(events, assertion, view) {
7464
8166
  };
7465
8167
  }
7466
8168
  async function traceStart(options, globalOptions) {
7467
- const events = await runLiveTrace(globalOptions.session, options, globalOptions.trace ?? false, "start");
8169
+ const events = await runLiveTrace(
8170
+ globalOptions.session,
8171
+ options,
8172
+ globalOptions.trace ?? false,
8173
+ "start"
8174
+ );
7468
8175
  output(
7469
8176
  {
7470
8177
  success: true,
@@ -7545,7 +8252,10 @@ async function traceMerge(options, globalOptions) {
7545
8252
  };
7546
8253
  fs8.mkdirSync(dirname8(resolve7(options.output)), { recursive: true });
7547
8254
  fs8.writeFileSync(resolve7(options.output), JSON.stringify(payload, null, 2));
7548
- output({ success: true, output: resolve7(options.output), events: merged.length }, globalOptions.format ?? "pretty");
8255
+ output(
8256
+ { success: true, output: resolve7(options.output), events: merged.length },
8257
+ globalOptions.format ?? "pretty"
8258
+ );
7549
8259
  }
7550
8260
  async function traceCommand(args, globalOptions) {
7551
8261
  const options = parseTraceArgs(args);
@@ -7577,74 +8287,77 @@ async function traceCommand(args, globalOptions) {
7577
8287
  }
7578
8288
  }
7579
8289
 
8290
+ // src/cli/version.ts
8291
+ import { readFileSync as readFileSync9 } from "fs";
8292
+ var cachedCliVersion;
8293
+ function getCliVersion() {
8294
+ if (cachedCliVersion) {
8295
+ return cachedCliVersion;
8296
+ }
8297
+ cachedCliVersion = "0.0.17";
8298
+ return cachedCliVersion;
8299
+ }
8300
+
7580
8301
  // src/cli/index.ts
7581
- var HELP2 = `
8302
+ function buildRootHelp() {
8303
+ const routeLabelWidth = Math.max(...CLI_ROUTE_GROUPS.map((group) => group.label.length)) + 2;
8304
+ const routeLines = CLI_ROUTE_GROUPS.map((group) => {
8305
+ const note = group.note ? ` ${group.note}` : "";
8306
+ return ` ${group.label.padEnd(routeLabelWidth)}${group.commands.join(", ")}${note}`;
8307
+ });
8308
+ const commandLabelWidth = Math.max(...ROOT_HELP_COMMANDS.map((command) => command.name.length)) + 2;
8309
+ const commandLines = ROOT_HELP_COMMANDS.map((command) => {
8310
+ return ` ${command.name.padEnd(commandLabelWidth)}${command.description}`;
8311
+ });
8312
+ return `
7582
8313
  bp - automation-first browser CLI for agents
7583
8314
 
7584
8315
  Route the job first:
7585
- Inspect page state snapshot, page, forms, text, targets, diagnose
7586
- Act in the browser exec, run
7587
- Capture a human demo record
7588
- Analyze behavior over time trace (listen is a compatibility alias)
7589
- Exercise voice/media audio
7590
- Change browser conditions env
8316
+ ${routeLines.join("\n")}
7591
8317
 
7592
8318
  Usage:
7593
8319
  bp <command> [options]
7594
8320
 
7595
8321
  Commands:
7596
- quickstart Getting started guide
7597
- connect Create a browser session
7598
- exec Execute high-level actions
7599
- snapshot Inspect current page with refs
7600
- record Record a human workflow and derive replayable output
7601
- trace Inspect and analyze behavior over time (listen alias for live stream)
7602
- audio Set up/validate/inject/capture voice pipelines
7603
- env Session and browser-environment controls
7604
- run Run a workflow file
7605
- page Compact page overview
7606
- forms List form controls
7607
- targets List available browser tabs
7608
- daemon Manage session daemon
7609
- list List sessions
7610
- close Close session
7611
- clean Clean old sessions and artifacts
7612
- actions Complete action reference
8322
+ ${commandLines.join("\n")}
7613
8323
 
7614
8324
  Golden paths:
7615
- 1. Automate a page
7616
- bp connect --provider generic --name dev
8325
+ 1. Connect, open a page, inspect it, then act
8326
+ bp connect --name dev
8327
+ bp exec -s dev '{"action":"goto","url":"https://example.com"}'
7617
8328
  bp snapshot -i -s dev
7618
8329
  bp exec -s dev '[{"action":"click","selector":"ref:e4"}]'
7619
8330
 
7620
- 2. Capture a manual workflow and derive automation
8331
+ 2. Read content or verify business state
8332
+ bp text -s dev --selector main
8333
+ bp review -s dev --json
8334
+
8335
+ 3. Capture a manual workflow and derive automation
7621
8336
  bp record -s demo --profile automation
7622
8337
  bp record summary demo/recording.json
7623
8338
  bp record derive demo/recording.json -o workflow.json
7624
8339
  bp run workflow.json
7625
8340
 
7626
- 3. Debug a realtime or voice session
8341
+ 4. Debug a realtime or voice session
7627
8342
  bp trace start -s dev
7628
8343
  bp trace summary -s dev --view ws
7629
8344
  bp audio check -s dev
7630
8345
  bp trace summary -s dev --view voice
7631
8346
 
7632
- 4. Exercise failure modes
7633
- bp env network offline -s dev --duration 10000
7634
- bp trace watch -s dev --view ws --assert profile:reconnect --timeout 15000
7635
-
7636
8347
  Options:
7637
8348
  -s, --session <id> Session ID
7638
8349
  -f, --format <fmt> json | pretty (default: pretty)
7639
8350
  --json Alias for -f json
8351
+ --pretty Alias for -f pretty
7640
8352
  --debug Enable debug logs for CDP transport
7641
8353
  --trace Legacy alias for --debug
7642
- --dialog <mode> Handle dialogs: accept | dismiss
7643
8354
  -h, --help Show help
8355
+ --version Print CLI version
7644
8356
 
7645
8357
  Notes:
7646
8358
  Start with "record summary" or "trace summary" before opening raw artifacts.
7647
- `;
8359
+ `.trim();
8360
+ }
7648
8361
  function parseGlobalOptions(args) {
7649
8362
  const options = {
7650
8363
  format: "pretty"
@@ -7720,14 +8433,28 @@ function prettyPrint(obj, lines, indent = 0) {
7720
8433
  }
7721
8434
  async function main() {
7722
8435
  const args = process.argv.slice(2);
7723
- if (args.length === 0) {
7724
- console.log(HELP2);
8436
+ if (args.length === 0 || args.length === 1 && (args[0] === "--help" || args[0] === "-h" || args[0] === "help")) {
8437
+ console.log(buildRootHelp());
8438
+ process.exit(0);
8439
+ }
8440
+ if (args.length === 1 && (args[0] === "--version" || args[0] === "version")) {
8441
+ process.stdout.write(`${getCliVersion()}
8442
+ `);
7725
8443
  process.exit(0);
7726
8444
  }
7727
- const command = args[0];
7728
- const { options, remaining } = parseGlobalOptions(args.slice(1));
8445
+ let command = args[0];
8446
+ let commandArgs = args.slice(1);
8447
+ if (command === "help") {
8448
+ if (commandArgs.length === 0) {
8449
+ console.log(buildRootHelp());
8450
+ process.exit(0);
8451
+ }
8452
+ command = commandArgs[0];
8453
+ commandArgs = [...commandArgs.slice(1), "--help"];
8454
+ }
8455
+ const { options, remaining } = parseGlobalOptions(commandArgs);
7729
8456
  if (options.help && !command) {
7730
- console.log(HELP2);
8457
+ console.log(buildRootHelp());
7731
8458
  process.exit(0);
7732
8459
  }
7733
8460
  try {
@@ -7783,6 +8510,9 @@ async function main() {
7783
8510
  case "record":
7784
8511
  await recordCommand(remaining, options);
7785
8512
  break;
8513
+ case "review":
8514
+ await reviewCommand(remaining, options);
8515
+ break;
7786
8516
  case "trace":
7787
8517
  await traceCommand(remaining, options);
7788
8518
  break;
@@ -7801,11 +8531,12 @@ async function main() {
7801
8531
  case "help":
7802
8532
  case "--help":
7803
8533
  case "-h":
7804
- console.log(HELP2);
8534
+ console.log(buildRootHelp());
7805
8535
  break;
7806
8536
  default:
7807
8537
  console.error(`Unknown command: ${command}`);
7808
- console.log(HELP2);
8538
+ console.error('Run "bp --help" to see the available command tree.');
8539
+ console.log(buildRootHelp());
7809
8540
  process.exit(1);
7810
8541
  }
7811
8542
  } catch (error) {