browser-pilot 0.0.9 → 0.0.10

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 (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.mjs +357 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -376,6 +376,7 @@ bp exec --dialog accept '{"action":"click","selector":"#delete-btn"}'
376
376
  # Other commands
377
377
  bp text -s my-session --selector ".main-content"
378
378
  bp screenshot -s my-session --output page.png
379
+ bp listen ws -m "*voice*" # monitor WebSocket traffic
379
380
  bp list # list all sessions
380
381
  bp close -s my-session # close session
381
382
  bp actions # show complete action reference
package/dist/cli.mjs CHANGED
@@ -7487,6 +7487,357 @@ function getAge(date) {
7487
7487
  return `${days}d ago`;
7488
7488
  }
7489
7489
 
7490
+ // src/cli/commands/listen.ts
7491
+ var LISTEN_HELP = `
7492
+ bp listen - Monitor network traffic (WebSocket/HTTP)
7493
+
7494
+ Attach to a browser session and stream network events as JSONL.
7495
+ Status messages go to stderr; stdout is clean JSONL (pipeable to jq).
7496
+
7497
+ Usage:
7498
+ bp listen <mode> [options]
7499
+
7500
+ Modes:
7501
+ ws WebSocket traffic only
7502
+ http HTTP requests/responses only
7503
+ all Both WebSocket and HTTP
7504
+
7505
+ Options:
7506
+ -s, --session [id] Session to use (omit: auto-connect, -s: latest, -s <id>: specific)
7507
+ -m, --match <glob> Filter by URL glob pattern (e.g. "*realtime*")
7508
+ -o, --output <file> Write JSONL to file instead of stdout
7509
+ --max-payload <n> Max text payload preview length (default: 256)
7510
+ --timeout <ms> Auto-stop after N milliseconds
7511
+ -q, --quiet Suppress stderr status messages
7512
+ -h, --help Show this help
7513
+
7514
+ Output Format (JSONL):
7515
+ {"ts":"...","type":"ws:created","requestId":"1.2","url":"wss://..."}
7516
+ {"ts":"...","type":"ws:frame:sent","requestId":"1.2","opcode":1,"length":142,"payload":"..."}
7517
+ {"ts":"...","type":"ws:frame:recv","requestId":"1.2","opcode":2,"length":24000,"payload":"[binary: 18000 bytes]"}
7518
+ {"ts":"...","type":"ws:closed","requestId":"1.2"}
7519
+ {"ts":"...","type":"http:request","requestId":"3.1","method":"POST","url":"https://..."}
7520
+ {"ts":"...","type":"http:response","requestId":"3.1","status":200,"mimeType":"application/json"}
7521
+
7522
+ Examples:
7523
+ # Debug a voice agent's WebSocket protocol
7524
+ bp listen ws -m "*voice*" -o voice-traffic.jsonl
7525
+
7526
+ # Watch all API calls during a session
7527
+ bp listen http -m "*/api/*" --max-payload 1024
7528
+
7529
+ # Capture everything for 60 seconds
7530
+ bp listen all -o full-trace.jsonl --timeout 60000
7531
+
7532
+ # Pipe to jq for live filtering
7533
+ bp listen ws | jq 'select(.type == "ws:frame:recv")'
7534
+ `;
7535
+ function parseListenArgs(args) {
7536
+ const options = {};
7537
+ for (let i = 0; i < args.length; i++) {
7538
+ const arg = args[i];
7539
+ if (arg === "-m" || arg === "--match") {
7540
+ options.match = args[++i];
7541
+ } else if (arg === "-o" || arg === "--output") {
7542
+ options.output = args[++i];
7543
+ } else if (arg === "--max-payload") {
7544
+ options.maxPayload = Number.parseInt(args[++i] ?? "", 10);
7545
+ } else if (arg === "--timeout") {
7546
+ options.timeout = Number.parseInt(args[++i] ?? "", 10);
7547
+ } else if (arg === "-q" || arg === "--quiet") {
7548
+ options.quiet = true;
7549
+ } else if (arg === "-h" || arg === "--help") {
7550
+ options.help = true;
7551
+ } else if (arg === "-s" || arg === "--session") {
7552
+ const nextArg = args[i + 1];
7553
+ if (!nextArg || nextArg.startsWith("-")) {
7554
+ options.useLatestSession = true;
7555
+ }
7556
+ } else if (!arg.startsWith("-") && !options.mode) {
7557
+ if (arg === "ws" || arg === "http" || arg === "all") {
7558
+ options.mode = arg;
7559
+ }
7560
+ }
7561
+ }
7562
+ return options;
7563
+ }
7564
+ function globToRegex(pattern) {
7565
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
7566
+ const withWildcards = escaped.replace(/\*/g, ".*");
7567
+ return new RegExp(`^${withWildcards}$`);
7568
+ }
7569
+ var TrafficMonitor = class {
7570
+ cdp;
7571
+ opts;
7572
+ matchRegex;
7573
+ wsUrls = /* @__PURE__ */ new Map();
7574
+ httpUrls = /* @__PURE__ */ new Map();
7575
+ handlers = [];
7576
+ lineCount = 0;
7577
+ constructor(cdp, opts) {
7578
+ this.cdp = cdp;
7579
+ this.opts = opts;
7580
+ this.matchRegex = opts.match ? globToRegex(opts.match) : null;
7581
+ }
7582
+ emit(record) {
7583
+ this.opts.write(JSON.stringify(record));
7584
+ this.lineCount++;
7585
+ }
7586
+ matchesUrl(url) {
7587
+ if (!this.matchRegex) return true;
7588
+ return this.matchRegex.test(url);
7589
+ }
7590
+ formatPayload(payloadData, opcode) {
7591
+ const data = payloadData ?? "";
7592
+ if (opcode === 2) {
7593
+ const byteLength = Math.floor(data.length * 3 / 4);
7594
+ return { payload: `[binary: ${byteLength} bytes]`, length: data.length };
7595
+ }
7596
+ const length = data.length;
7597
+ if (length > this.opts.maxPayload) {
7598
+ return {
7599
+ payload: `${data.slice(0, this.opts.maxPayload)}... [truncated, ${length} total]`,
7600
+ length
7601
+ };
7602
+ }
7603
+ return { payload: data, length };
7604
+ }
7605
+ subscribe(event, handler) {
7606
+ this.cdp.on(event, handler);
7607
+ this.handlers.push({ event, handler });
7608
+ }
7609
+ start() {
7610
+ const mode = this.opts.mode;
7611
+ if (mode === "ws" || mode === "all") {
7612
+ this.subscribe("Network.webSocketCreated", (params) => {
7613
+ const url = params["url"];
7614
+ const requestId = params["requestId"];
7615
+ if (!this.matchesUrl(url)) return;
7616
+ this.wsUrls.set(requestId, url);
7617
+ this.emit({
7618
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7619
+ type: "ws:created",
7620
+ requestId,
7621
+ url
7622
+ });
7623
+ });
7624
+ this.subscribe("Network.webSocketFrameSent", (params) => {
7625
+ const requestId = params["requestId"];
7626
+ if (!this.wsUrls.has(requestId)) return;
7627
+ const response = params["response"];
7628
+ const opcode = response?.opcode ?? 1;
7629
+ const { payload, length } = this.formatPayload(response?.payloadData, opcode);
7630
+ this.emit({
7631
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7632
+ type: "ws:frame:sent",
7633
+ requestId,
7634
+ opcode,
7635
+ length,
7636
+ payload
7637
+ });
7638
+ });
7639
+ this.subscribe("Network.webSocketFrameReceived", (params) => {
7640
+ const requestId = params["requestId"];
7641
+ if (!this.wsUrls.has(requestId)) return;
7642
+ const response = params["response"];
7643
+ const opcode = response?.opcode ?? 1;
7644
+ const { payload, length } = this.formatPayload(response?.payloadData, opcode);
7645
+ this.emit({
7646
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7647
+ type: "ws:frame:recv",
7648
+ requestId,
7649
+ opcode,
7650
+ length,
7651
+ payload
7652
+ });
7653
+ });
7654
+ this.subscribe("Network.webSocketClosed", (params) => {
7655
+ const requestId = params["requestId"];
7656
+ if (!this.wsUrls.has(requestId)) return;
7657
+ this.wsUrls.delete(requestId);
7658
+ this.emit({
7659
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7660
+ type: "ws:closed",
7661
+ requestId
7662
+ });
7663
+ });
7664
+ }
7665
+ if (mode === "http" || mode === "all") {
7666
+ this.subscribe("Network.requestWillBeSent", (params) => {
7667
+ const request = params["request"];
7668
+ const url = request?.url ?? "";
7669
+ const requestId = params["requestId"];
7670
+ if (!this.matchesUrl(url)) return;
7671
+ this.httpUrls.set(requestId, url);
7672
+ this.emit({
7673
+ ts: params["wallTime"] ? new Date(params["wallTime"] * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
7674
+ type: "http:request",
7675
+ requestId,
7676
+ method: request?.method ?? "GET",
7677
+ url
7678
+ });
7679
+ });
7680
+ this.subscribe("Network.responseReceived", (params) => {
7681
+ const requestId = params["requestId"];
7682
+ if (!this.httpUrls.has(requestId)) return;
7683
+ const response = params["response"];
7684
+ this.emit({
7685
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7686
+ type: "http:response",
7687
+ requestId,
7688
+ status: response?.status ?? 0,
7689
+ mimeType: response?.mimeType ?? ""
7690
+ });
7691
+ });
7692
+ this.subscribe("Network.loadingFailed", (params) => {
7693
+ const requestId = params["requestId"];
7694
+ if (!this.httpUrls.has(requestId)) return;
7695
+ this.emit({
7696
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7697
+ type: "http:failed",
7698
+ requestId,
7699
+ errorText: params["errorText"] ?? ""
7700
+ });
7701
+ });
7702
+ }
7703
+ }
7704
+ stop() {
7705
+ for (const { event, handler } of this.handlers) {
7706
+ this.cdp.off(event, handler);
7707
+ }
7708
+ this.handlers = [];
7709
+ }
7710
+ };
7711
+ async function resolveConnection2(sessionId, useLatestSession, trace) {
7712
+ if (sessionId) {
7713
+ const session2 = await loadSession(sessionId);
7714
+ const browser2 = await connect({
7715
+ provider: session2.provider,
7716
+ wsUrl: session2.wsUrl,
7717
+ debug: trace
7718
+ });
7719
+ return { browser: browser2, session: session2 };
7720
+ }
7721
+ if (useLatestSession) {
7722
+ const session2 = await getDefaultSession();
7723
+ if (!session2) {
7724
+ throw new Error('No sessions found. Run "bp connect" first or omit -s to auto-connect.');
7725
+ }
7726
+ const browser2 = await connect({
7727
+ provider: session2.provider,
7728
+ wsUrl: session2.wsUrl,
7729
+ debug: trace
7730
+ });
7731
+ return { browser: browser2, session: session2 };
7732
+ }
7733
+ let wsUrl;
7734
+ try {
7735
+ wsUrl = await getBrowserWebSocketUrl("localhost:9222");
7736
+ } catch {
7737
+ throw new Error(
7738
+ "Could not auto-discover browser.\nEither:\n 1. Start Chrome with: --remote-debugging-port=9222\n 2. Use an existing session: bp listen -s <session-id>\n 3. Use latest session: bp listen -s"
7739
+ );
7740
+ }
7741
+ const browser = await connect({ provider: "generic", wsUrl, debug: trace });
7742
+ const page = await browser.page();
7743
+ const currentUrl = await page.url();
7744
+ const newSessionId = generateSessionId();
7745
+ const session = {
7746
+ id: newSessionId,
7747
+ provider: "generic",
7748
+ wsUrl: browser.wsUrl,
7749
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7750
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
7751
+ currentUrl
7752
+ };
7753
+ await saveSession(session);
7754
+ return { browser, session };
7755
+ }
7756
+ async function listenCommand(args, globalOptions) {
7757
+ const options = parseListenArgs(args);
7758
+ if (options.help || globalOptions.help || !options.mode) {
7759
+ console.log(LISTEN_HELP);
7760
+ return;
7761
+ }
7762
+ const log = options.quiet ? () => {
7763
+ } : (msg) => process.stderr.write(`${msg}
7764
+ `);
7765
+ const { browser, session } = await resolveConnection2(
7766
+ globalOptions.session,
7767
+ options.useLatestSession ?? false,
7768
+ globalOptions.trace ?? false
7769
+ );
7770
+ let outputStream;
7771
+ if (options.output) {
7772
+ const fs3 = await import("fs");
7773
+ const fileStream = fs3.createWriteStream(options.output, { flags: "w" });
7774
+ outputStream = {
7775
+ write: (line) => fileStream.write(`${line}
7776
+ `),
7777
+ close: () => fileStream.end()
7778
+ };
7779
+ log(`Writing to ${options.output}`);
7780
+ } else {
7781
+ outputStream = {
7782
+ write: (line) => {
7783
+ try {
7784
+ process.stdout.write(`${line}
7785
+ `);
7786
+ } catch {
7787
+ process.exit(0);
7788
+ }
7789
+ }
7790
+ };
7791
+ process.stdout.on("error", (err) => {
7792
+ if (err.code === "EPIPE") {
7793
+ process.exit(0);
7794
+ }
7795
+ });
7796
+ }
7797
+ try {
7798
+ const page = await browser.page(void 0, { targetId: session.targetId });
7799
+ const cdp = page.cdpClient;
7800
+ await cdp.send("Network.enable");
7801
+ const monitor = new TrafficMonitor(cdp, {
7802
+ mode: options.mode,
7803
+ match: options.match,
7804
+ maxPayload: options.maxPayload ?? 256,
7805
+ write: (line) => outputStream.write(line)
7806
+ });
7807
+ monitor.start();
7808
+ const matchLabel = options.match ? ` matching "${options.match}"` : "";
7809
+ log(`Listening for ${options.mode} traffic${matchLabel} (session: ${session.id})`);
7810
+ log("Press Ctrl+C to stop.");
7811
+ let cleaned = false;
7812
+ const cleanup = () => {
7813
+ if (cleaned) return;
7814
+ cleaned = true;
7815
+ monitor.stop();
7816
+ log(`
7817
+ Stopped. ${monitor.lineCount} events captured.`);
7818
+ outputStream.close?.();
7819
+ browser.disconnect().catch(() => {
7820
+ });
7821
+ process.exit(0);
7822
+ };
7823
+ process.on("SIGINT", cleanup);
7824
+ process.on("SIGTERM", cleanup);
7825
+ if (options.timeout && options.timeout > 0) {
7826
+ setTimeout(() => {
7827
+ log(`
7828
+ Timeout reached (${options.timeout}ms).`);
7829
+ cleanup();
7830
+ }, options.timeout);
7831
+ }
7832
+ await new Promise(() => {
7833
+ });
7834
+ } catch (error) {
7835
+ outputStream.close?.();
7836
+ await browser.disconnect();
7837
+ throw error;
7838
+ }
7839
+ }
7840
+
7490
7841
  // src/cli/commands/quickstart.ts
7491
7842
  var QUICKSTART = `
7492
7843
  browser-pilot CLI - Quick Start Guide
@@ -8465,7 +8816,7 @@ function parseRecordArgs(args) {
8465
8816
  }
8466
8817
  return options;
8467
8818
  }
8468
- async function resolveConnection2(sessionId, useLatestSession, trace) {
8819
+ async function resolveConnection3(sessionId, useLatestSession, trace) {
8469
8820
  if (sessionId) {
8470
8821
  const session2 = await loadSession(sessionId);
8471
8822
  const browser2 = await connect({
@@ -8523,7 +8874,7 @@ async function recordCommand(args, globalOptions) {
8523
8874
  return;
8524
8875
  }
8525
8876
  const outputFile = options.file ?? "recording.json";
8526
- const { browser, session, isNewSession } = await resolveConnection2(
8877
+ const { browser, session, isNewSession } = await resolveConnection3(
8527
8878
  globalOptions.session,
8528
8879
  options.useLatestSession ?? false,
8529
8880
  globalOptions.trace ?? false
@@ -9123,6 +9474,7 @@ Commands:
9123
9474
  eval Evaluate JavaScript expression
9124
9475
  record Record browser actions to JSON
9125
9476
  audio Audio I/O for voice agent testing
9477
+ listen Monitor network traffic (WebSocket/HTTP)
9126
9478
  snapshot Get page with element refs
9127
9479
  diagnose Debug element selection issues
9128
9480
  text Extract text content
@@ -9271,6 +9623,9 @@ async function main() {
9271
9623
  case "audio":
9272
9624
  await audioCommand(remaining, options);
9273
9625
  break;
9626
+ case "listen":
9627
+ await listenCommand(remaining, options);
9628
+ break;
9274
9629
  case "help":
9275
9630
  case "--help":
9276
9631
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-pilot",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Lightweight CDP-based browser automation for Node.js, Bun, and Cloudflare Workers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",