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.
- package/README.md +38 -3
- package/dist/actions.cjs +848 -105
- package/dist/actions.d.cts +101 -4
- package/dist/actions.d.ts +101 -4
- package/dist/actions.mjs +17 -1
- package/dist/{browser-MEWT75IB.mjs → browser-4ZHNAQR5.mjs} +2 -2
- package/dist/browser.cjs +1684 -130
- package/dist/browser.d.cts +230 -6
- package/dist/browser.d.ts +230 -6
- package/dist/browser.mjs +37 -5
- package/dist/chunk-EZNZ72VA.mjs +563 -0
- package/dist/{chunk-ZAXQ5OTV.mjs → chunk-FEEGNSHB.mjs} +606 -12
- package/dist/{chunk-WPNW23CE.mjs → chunk-IRLHCVNH.mjs} +345 -7
- package/dist/chunk-MIJ7UIKB.mjs +96 -0
- package/dist/{chunk-USYSHCI3.mjs → chunk-MRY3HRFJ.mjs} +841 -370
- package/dist/chunk-OIHU7OFY.mjs +91 -0
- package/dist/{chunk-7YVCOL2W.mjs → chunk-ZDODXEBD.mjs} +637 -105
- package/dist/cli.mjs +1280 -549
- package/dist/combobox-RAKBA2BW.mjs +6 -0
- package/dist/index.cjs +1976 -144
- package/dist/index.d.cts +57 -6
- package/dist/index.d.ts +57 -6
- package/dist/index.mjs +206 -7
- package/dist/{page-XPS6IC6V.mjs → page-SD64DY3F.mjs} +1 -1
- package/dist/providers.cjs +637 -2
- package/dist/providers.d.cts +2 -2
- package/dist/providers.d.ts +2 -2
- package/dist/providers.mjs +17 -3
- package/dist/{types-Cvvf0oGu.d.ts → types-B_v62K7C.d.ts} +147 -3
- package/dist/types-DeVSWhXj.d.cts +142 -0
- package/dist/types-DeVSWhXj.d.ts +142 -0
- package/dist/{types-C9ySEdOX.d.cts → types-Yuybzq53.d.cts} +147 -3
- package/dist/upload-E6MCC2OF.mjs +6 -0
- package/package.json +10 -3
- package/dist/chunk-BRAFQUMG.mjs +0 -229
- package/dist/types--wXNHUwt.d.cts +0 -56
- 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
|
-
|
|
5
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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/
|
|
389
|
-
|
|
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 {
|
|
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 =
|
|
401
|
-
this.logPath =
|
|
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
|
|
898
|
-
} catch {
|
|
1271
|
+
wsUrl = (await resolveCLIEndpoint()).wsUrl;
|
|
1272
|
+
} catch (error) {
|
|
899
1273
|
throw new Error(
|
|
900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
2065
|
+
Global options:
|
|
1686
2066
|
-s, --session <id> Session to close (default: most recent)
|
|
1687
|
-
|
|
1688
|
-
--
|
|
1689
|
-
--
|
|
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
|
-
|
|
2188
|
+
Local options:
|
|
1798
2189
|
-p, --provider <type> Provider: generic | browserbase | browserless (default: generic)
|
|
1799
|
-
--url <
|
|
1800
|
-
--
|
|
1801
|
-
--
|
|
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
|
-
|
|
1816
|
-
|
|
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
|
|
1821
|
-
bp connect --
|
|
1822
|
-
bp connect --
|
|
1823
|
-
bp connect --url ws://localhost:9222/devtools
|
|
1824
|
-
bp connect --
|
|
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 --
|
|
1827
|
-
bp connect --
|
|
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
|
-
|
|
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
|
-
|
|
1923
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2650
|
-
--json Output as JSON
|
|
3108
|
+
Local options:
|
|
2651
3109
|
--max <n> Max candidates for fuzzy match (default: 5)
|
|
2652
|
-
|
|
2653
|
-
|
|
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 = [
|
|
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 = [
|
|
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({
|
|
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
|
|
3493
|
-
} catch {
|
|
3749
|
+
wsUrl = (await resolveCLIEndpoint()).wsUrl;
|
|
3750
|
+
} catch (error) {
|
|
3494
3751
|
throw new Error(
|
|
3495
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3823
|
-
-f, --file <path>
|
|
3824
|
-
-o, --output <path>
|
|
3825
|
-
--dialog <mode>
|
|
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
|
-
|
|
4746
|
+
Global options:
|
|
4220
4747
|
-s, --session <id> Session to use (default: most recent)
|
|
4221
|
-
|
|
4222
|
-
--
|
|
4223
|
-
--
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4999
|
+
Global options:
|
|
4462
5000
|
-s, --session <id> Session to use (default: most recent)
|
|
4463
|
-
|
|
4464
|
-
--
|
|
4465
|
-
--
|
|
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, {
|
|
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 --
|
|
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:
|
|
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
|
|
4560
|
-
bp
|
|
4561
|
-
bp
|
|
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
|
-
|
|
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
|
|
4582
|
-
bp forms
|
|
4583
|
-
bp
|
|
4584
|
-
bp
|
|
4585
|
-
|
|
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
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
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(
|
|
6042
|
+
this.handleBindingCall(payload);
|
|
5483
6043
|
} else if (params["name"] === TRACE_BINDING_NAME) {
|
|
5484
|
-
this.handleTraceBindingCall(
|
|
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 =
|
|
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(
|
|
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:
|
|
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:
|
|
6339
|
+
data: { url: url ?? null },
|
|
5779
6340
|
connectionId: requestId,
|
|
5780
6341
|
requestId,
|
|
5781
|
-
url
|
|
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
|
|
6078
|
-
} catch {
|
|
6633
|
+
wsUrl = (await resolveCLIEndpoint()).wsUrl;
|
|
6634
|
+
} catch (error) {
|
|
6079
6635
|
throw new Error(
|
|
6080
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
6612
|
-
--
|
|
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
|
-
|
|
6959
|
-
-i, --interactive
|
|
6960
|
-
|
|
6961
|
-
--
|
|
6962
|
-
|
|
6963
|
-
-
|
|
6964
|
-
--
|
|
6965
|
-
--
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
--
|
|
6970
|
-
|
|
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
|
-
|
|
7809
|
+
Global options:
|
|
7125
7810
|
-s, --session <id> Session to use (default: most recent)
|
|
7126
|
-
|
|
7127
|
-
--
|
|
7128
|
-
--
|
|
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
|
-
|
|
7194
|
-
--selector <
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
--
|
|
7198
|
-
--
|
|
7199
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
7616
|
-
bp connect --
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
7728
|
-
|
|
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(
|
|
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(
|
|
8534
|
+
console.log(buildRootHelp());
|
|
7805
8535
|
break;
|
|
7806
8536
|
default:
|
|
7807
8537
|
console.error(`Unknown command: ${command}`);
|
|
7808
|
-
console.
|
|
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) {
|