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.
- package/README.md +1 -0
- package/dist/cli.mjs +357 -2
- 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
|
|
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
|
|
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":
|