browser-pilot 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -667
- package/dist/actions.cjs +1073 -41
- package/dist/actions.d.cts +11 -3
- package/dist/actions.d.ts +11 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser-ZCR6AA4D.mjs +11 -0
- package/dist/browser.cjs +1431 -62
- package/dist/browser.d.cts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.mjs +4 -4
- package/dist/cdp.cjs +5 -1
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +1 -1
- package/dist/{chunk-7NDR6V7S.mjs → chunk-6GBYX7C2.mjs} +1405 -528
- package/dist/{chunk-KIFB526Y.mjs → chunk-BVZALQT4.mjs} +5 -1
- package/dist/chunk-DTVRFXKI.mjs +35 -0
- package/dist/chunk-EZNZ72VA.mjs +563 -0
- package/dist/{chunk-SPSZZH22.mjs → chunk-LCNFBXB5.mjs} +9 -33
- package/dist/{chunk-IN5HPAPB.mjs → chunk-NNEHWWHL.mjs} +28 -10
- package/dist/chunk-TJ5B56NV.mjs +804 -0
- package/dist/{chunk-XMJABKCF.mjs → chunk-V3VLBQAM.mjs} +1073 -41
- package/dist/cli.mjs +2799 -1176
- package/dist/{client-Ck2nQksT.d.cts → client-B5QBRgIy.d.cts} +2 -0
- package/dist/{client-Ck2nQksT.d.ts → client-B5QBRgIy.d.ts} +2 -0
- package/dist/{client-3AFV2IAF.mjs → client-JWWZWO6L.mjs} +4 -2
- package/dist/index.cjs +1441 -52
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +19 -7
- package/dist/page-IUUTJ3SW.mjs +7 -0
- 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-CjT0vClo.d.ts → types-BflRmiDz.d.cts} +17 -3
- package/dist/{types-BSoh5v1Y.d.cts → types-BzM-IfsL.d.ts} +17 -3
- package/dist/types-DeVSWhXj.d.cts +142 -0
- package/dist/types-DeVSWhXj.d.ts +142 -0
- package/package.json +1 -1
- package/dist/browser-LZTEHUDI.mjs +0 -9
- 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,18 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
BrowserEndpointResolutionError,
|
|
4
|
+
connect,
|
|
5
|
+
resolveBrowserEndpoint
|
|
6
|
+
} from "./chunk-TJ5B56NV.mjs";
|
|
7
|
+
import "./chunk-LCNFBXB5.mjs";
|
|
2
8
|
import {
|
|
3
9
|
DEEP_QUERY_SCRIPT,
|
|
10
|
+
LiveTraceCollector,
|
|
4
11
|
SENSITIVE_AUTOCOMPLETE_TOKENS,
|
|
12
|
+
TRACE_BINDING_NAME,
|
|
13
|
+
TRACE_SCRIPT,
|
|
5
14
|
addBatchToPage,
|
|
6
|
-
|
|
15
|
+
buildTraceSummaries,
|
|
16
|
+
buildTraceSummary,
|
|
17
|
+
canonicalizeRecordingArtifact,
|
|
18
|
+
createRecordingManifest,
|
|
19
|
+
createTraceId,
|
|
7
20
|
fuzzyMatchElements,
|
|
8
|
-
|
|
21
|
+
grantAudioPermissions,
|
|
22
|
+
normalizeTraceEvent,
|
|
9
23
|
pcmToWav,
|
|
10
24
|
redactValueForRecording,
|
|
11
25
|
validateSteps
|
|
12
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-6GBYX7C2.mjs";
|
|
13
27
|
import {
|
|
14
28
|
isRecord
|
|
15
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-DTVRFXKI.mjs";
|
|
16
30
|
import {
|
|
17
31
|
DAEMON_MAX_AGE_MS,
|
|
18
32
|
DAEMON_READY_TIMEOUT_MS
|
|
@@ -345,28 +359,99 @@ Content-Type: ${contentType}\r
|
|
|
345
359
|
parts.push(data);
|
|
346
360
|
}
|
|
347
361
|
|
|
348
|
-
// src/
|
|
362
|
+
// src/trace/store.ts
|
|
363
|
+
import * as fs from "fs";
|
|
349
364
|
import { homedir } from "os";
|
|
350
|
-
import { join } from "path";
|
|
365
|
+
import { dirname, join, resolve } from "path";
|
|
366
|
+
var TRACE_FILE_NAME = "trace.jsonl";
|
|
351
367
|
var SESSION_DIR = join(homedir(), ".browser-pilot", "sessions");
|
|
368
|
+
function getSessionTracePath(sessionId) {
|
|
369
|
+
return join(SESSION_DIR, sessionId, TRACE_FILE_NAME);
|
|
370
|
+
}
|
|
371
|
+
function readTraceEvents(path) {
|
|
372
|
+
const resolved = resolve(path);
|
|
373
|
+
if (!fs.existsSync(resolved)) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
const content = fs.readFileSync(resolved, "utf-8").trim();
|
|
377
|
+
if (!content) {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
return content.split("\n").map((line) => {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(line);
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}).filter((event) => event !== null);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/cli/browser-endpoint.ts
|
|
390
|
+
async function resolveCLIEndpoint(options = {}) {
|
|
391
|
+
return resolveBrowserEndpoint({
|
|
392
|
+
explicitWsUrl: options.explicitWsUrl,
|
|
393
|
+
channel: options.channel,
|
|
394
|
+
userDataDir: options.userDataDir,
|
|
395
|
+
allowLocalDiscovery: true,
|
|
396
|
+
allowLegacyHostFallback: true
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function formatCandidateLabel(candidate) {
|
|
400
|
+
const channelLabel = candidate.channel ? `${candidate.channel}` : "unknown";
|
|
401
|
+
return `${channelLabel}: ${candidate.userDataDir}`;
|
|
402
|
+
}
|
|
403
|
+
function formatBrowserDiscoveryError(error, options = {}) {
|
|
404
|
+
const explicitFlag = options.explicitFlag ?? "--browser-url";
|
|
405
|
+
if (error instanceof BrowserEndpointResolutionError) {
|
|
406
|
+
if (error.code === "multiple-local-browsers") {
|
|
407
|
+
const candidates = error.details.candidates ?? [];
|
|
408
|
+
const foundLines = candidates.length > 0 ? candidates.map((candidate) => ` - ${formatCandidateLabel(candidate)}`).join("\n") : " - Multiple local Chrome profiles were found";
|
|
409
|
+
return `Multiple running Chrome profiles have remote debugging enabled.
|
|
410
|
+
${foundLines}
|
|
411
|
+
Pass --channel <stable|beta|dev|canary> or --user-data-dir <path>.`;
|
|
412
|
+
}
|
|
413
|
+
const lines = [
|
|
414
|
+
"Could not auto-discover browser.",
|
|
415
|
+
"Recommended for Chrome 144+:",
|
|
416
|
+
" 1. Open Chrome and enable remote debugging in chrome://inspect/#remote-debugging",
|
|
417
|
+
" 2. Keep Chrome running, then retry",
|
|
418
|
+
"Other options:",
|
|
419
|
+
options.explicitHint ?? ` - Pass ${explicitFlag} with a browser WebSocket URL`,
|
|
420
|
+
" - Launch Chrome with --remote-debugging-port=9222 and a custom --user-data-dir"
|
|
421
|
+
];
|
|
422
|
+
if (options.reuseSessionHint) {
|
|
423
|
+
lines.push(` - Reuse an existing session: ${options.reuseSessionHint}`);
|
|
424
|
+
}
|
|
425
|
+
if (options.latestSessionHint) {
|
|
426
|
+
lines.push(` - Use latest session: ${options.latestSessionHint}`);
|
|
427
|
+
}
|
|
428
|
+
return lines.join("\n");
|
|
429
|
+
}
|
|
430
|
+
return error instanceof Error ? error.message : String(error);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/cli/session.ts
|
|
434
|
+
import { homedir as homedir2 } from "os";
|
|
435
|
+
import { join as join2 } from "path";
|
|
436
|
+
var SESSION_DIR2 = join2(homedir2(), ".browser-pilot", "sessions");
|
|
352
437
|
function getSessionFilePath(id) {
|
|
353
|
-
return
|
|
438
|
+
return join2(SESSION_DIR2, `${id}.json`);
|
|
354
439
|
}
|
|
355
440
|
async function ensureSessionDir() {
|
|
356
|
-
const
|
|
357
|
-
await
|
|
441
|
+
const fs9 = await import("fs/promises");
|
|
442
|
+
await fs9.mkdir(SESSION_DIR2, { recursive: true });
|
|
358
443
|
}
|
|
359
444
|
async function saveSession(session) {
|
|
360
445
|
await ensureSessionDir();
|
|
361
|
-
const
|
|
362
|
-
const filePath =
|
|
363
|
-
await
|
|
446
|
+
const fs9 = await import("fs/promises");
|
|
447
|
+
const filePath = join2(SESSION_DIR2, `${session.id}.json`);
|
|
448
|
+
await fs9.writeFile(filePath, JSON.stringify(session, null, 2));
|
|
364
449
|
}
|
|
365
450
|
async function loadSession(id) {
|
|
366
|
-
const
|
|
367
|
-
const filePath =
|
|
451
|
+
const fs9 = await import("fs/promises");
|
|
452
|
+
const filePath = join2(SESSION_DIR2, `${id}.json`);
|
|
368
453
|
try {
|
|
369
|
-
const content = await
|
|
454
|
+
const content = await fs9.readFile(filePath, "utf-8");
|
|
370
455
|
return JSON.parse(content);
|
|
371
456
|
} catch (error) {
|
|
372
457
|
if (error.code === "ENOENT") {
|
|
@@ -388,10 +473,10 @@ async function updateSession(id, updates) {
|
|
|
388
473
|
return updated;
|
|
389
474
|
}
|
|
390
475
|
async function deleteSession(id) {
|
|
391
|
-
const
|
|
392
|
-
const filePath =
|
|
476
|
+
const fs9 = await import("fs/promises");
|
|
477
|
+
const filePath = join2(SESSION_DIR2, `${id}.json`);
|
|
393
478
|
try {
|
|
394
|
-
await
|
|
479
|
+
await fs9.unlink(filePath);
|
|
395
480
|
} catch (error) {
|
|
396
481
|
if (error.code !== "ENOENT") {
|
|
397
482
|
throw error;
|
|
@@ -399,18 +484,18 @@ async function deleteSession(id) {
|
|
|
399
484
|
}
|
|
400
485
|
}
|
|
401
486
|
async function deleteSessionFull(id) {
|
|
402
|
-
const
|
|
403
|
-
const filePath =
|
|
487
|
+
const fs9 = await import("fs/promises");
|
|
488
|
+
const filePath = join2(SESSION_DIR2, `${id}.json`);
|
|
404
489
|
try {
|
|
405
|
-
await
|
|
490
|
+
await fs9.unlink(filePath);
|
|
406
491
|
} catch (error) {
|
|
407
492
|
if (error.code !== "ENOENT") {
|
|
408
493
|
throw error;
|
|
409
494
|
}
|
|
410
495
|
}
|
|
411
|
-
const dirPath =
|
|
496
|
+
const dirPath = join2(SESSION_DIR2, id);
|
|
412
497
|
try {
|
|
413
|
-
await
|
|
498
|
+
await fs9.rm(dirPath, { recursive: true });
|
|
414
499
|
} catch (error) {
|
|
415
500
|
if (error.code !== "ENOENT") {
|
|
416
501
|
throw error;
|
|
@@ -419,14 +504,14 @@ async function deleteSessionFull(id) {
|
|
|
419
504
|
}
|
|
420
505
|
async function listSessions() {
|
|
421
506
|
await ensureSessionDir();
|
|
422
|
-
const
|
|
507
|
+
const fs9 = await import("fs/promises");
|
|
423
508
|
try {
|
|
424
|
-
const files = await
|
|
509
|
+
const files = await fs9.readdir(SESSION_DIR2);
|
|
425
510
|
const sessions = [];
|
|
426
511
|
for (const file of files) {
|
|
427
512
|
if (file.endsWith(".json")) {
|
|
428
513
|
try {
|
|
429
|
-
const content = await
|
|
514
|
+
const content = await fs9.readFile(join2(SESSION_DIR2, file), "utf-8");
|
|
430
515
|
sessions.push(JSON.parse(content));
|
|
431
516
|
} catch {
|
|
432
517
|
}
|
|
@@ -449,9 +534,271 @@ async function getDefaultSession() {
|
|
|
449
534
|
return sessions[0] ?? null;
|
|
450
535
|
}
|
|
451
536
|
|
|
537
|
+
// src/cli/session-logger.ts
|
|
538
|
+
import * as fs2 from "fs";
|
|
539
|
+
import { homedir as homedir3 } from "os";
|
|
540
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
541
|
+
var SESSION_DIR3 = join3(homedir3(), ".browser-pilot", "sessions");
|
|
542
|
+
var SessionLogger = class {
|
|
543
|
+
logPath;
|
|
544
|
+
exportLogPath = null;
|
|
545
|
+
seq = 0;
|
|
546
|
+
sessionId;
|
|
547
|
+
constructor(sessionId, exportLogPath) {
|
|
548
|
+
this.sessionId = sessionId;
|
|
549
|
+
const sessionDir = join3(SESSION_DIR3, sessionId);
|
|
550
|
+
this.logPath = join3(sessionDir, TRACE_FILE_NAME);
|
|
551
|
+
if (!fs2.existsSync(sessionDir)) {
|
|
552
|
+
fs2.mkdirSync(sessionDir, { recursive: true });
|
|
553
|
+
}
|
|
554
|
+
if (exportLogPath) {
|
|
555
|
+
this.exportLogPath = resolve2(exportLogPath);
|
|
556
|
+
const exportDir = dirname2(this.exportLogPath);
|
|
557
|
+
if (!fs2.existsSync(exportDir)) {
|
|
558
|
+
fs2.mkdirSync(exportDir, { recursive: true });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (fs2.existsSync(this.logPath)) {
|
|
562
|
+
this.seq = this.countEntries();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Log a raw compatibility entry through the canonical trace substrate.
|
|
567
|
+
*/
|
|
568
|
+
log(entry) {
|
|
569
|
+
const fullEntry = {
|
|
570
|
+
seq: ++this.seq,
|
|
571
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
572
|
+
...entry
|
|
573
|
+
};
|
|
574
|
+
this.logTrace(this.compatibilityEntryToTrace(fullEntry));
|
|
575
|
+
}
|
|
576
|
+
logTrace(event) {
|
|
577
|
+
const normalized = normalizeTraceEvent({
|
|
578
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
579
|
+
sessionId: this.sessionId,
|
|
580
|
+
...event
|
|
581
|
+
});
|
|
582
|
+
const line = `${JSON.stringify(normalized)}
|
|
583
|
+
`;
|
|
584
|
+
fs2.appendFileSync(this.logPath, line, "utf-8");
|
|
585
|
+
if (this.exportLogPath) {
|
|
586
|
+
try {
|
|
587
|
+
fs2.appendFileSync(this.exportLogPath, line, "utf-8");
|
|
588
|
+
} catch (err) {
|
|
589
|
+
console.warn(`[browser-pilot] Failed to write to export log: ${err}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Get the export log path (if configured)
|
|
595
|
+
*/
|
|
596
|
+
getExportLogPath() {
|
|
597
|
+
return this.exportLogPath;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Log a command execution
|
|
601
|
+
*/
|
|
602
|
+
logCommand(cmd, args, result, durationMs, screenshotFile) {
|
|
603
|
+
this.log({
|
|
604
|
+
type: "command",
|
|
605
|
+
cmd,
|
|
606
|
+
args,
|
|
607
|
+
status: result.success ? "success" : "failed",
|
|
608
|
+
durationMs,
|
|
609
|
+
error: result.error,
|
|
610
|
+
hints: result.hints,
|
|
611
|
+
screenshotFile
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Log an error
|
|
616
|
+
*/
|
|
617
|
+
logError(error, context) {
|
|
618
|
+
this.log({
|
|
619
|
+
type: "error",
|
|
620
|
+
error: error.message,
|
|
621
|
+
args: context
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get the log file path
|
|
626
|
+
*/
|
|
627
|
+
getLogPath() {
|
|
628
|
+
return this.logPath;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get log statistics
|
|
632
|
+
*/
|
|
633
|
+
getLogStats() {
|
|
634
|
+
if (!fs2.existsSync(this.logPath)) {
|
|
635
|
+
return { entries: 0, size: 0 };
|
|
636
|
+
}
|
|
637
|
+
const stat = fs2.statSync(this.logPath);
|
|
638
|
+
const entries = this.countEntries();
|
|
639
|
+
let first;
|
|
640
|
+
let last;
|
|
641
|
+
if (entries > 0) {
|
|
642
|
+
const lines = fs2.readFileSync(this.logPath, "utf-8").trim().split("\n");
|
|
643
|
+
const firstEntry = this.parseLine(lines[0]);
|
|
644
|
+
const lastEntry = this.parseLine(lines[lines.length - 1]);
|
|
645
|
+
first = firstEntry?.ts;
|
|
646
|
+
last = lastEntry?.ts;
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
entries,
|
|
650
|
+
size: stat.size,
|
|
651
|
+
first,
|
|
652
|
+
last
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Get the last n log entries
|
|
657
|
+
*/
|
|
658
|
+
tailLog(n) {
|
|
659
|
+
if (!fs2.existsSync(this.logPath)) {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
const content = fs2.readFileSync(this.logPath, "utf-8").trim();
|
|
663
|
+
if (!content) {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
const lines = content.split("\n");
|
|
667
|
+
const startIndex = Math.max(0, lines.length - n);
|
|
668
|
+
const result = [];
|
|
669
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
670
|
+
const entry = this.parseLine(lines[i]);
|
|
671
|
+
if (entry) {
|
|
672
|
+
result.push(entry);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Count entries in the log file
|
|
679
|
+
*/
|
|
680
|
+
countEntries() {
|
|
681
|
+
if (!fs2.existsSync(this.logPath)) {
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
const content = fs2.readFileSync(this.logPath, "utf-8").trim();
|
|
685
|
+
if (!content) {
|
|
686
|
+
return 0;
|
|
687
|
+
}
|
|
688
|
+
return content.split("\n").length;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Parse a single log line
|
|
692
|
+
*/
|
|
693
|
+
parseLine(line) {
|
|
694
|
+
if (!line) {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const event = JSON.parse(line);
|
|
699
|
+
return this.traceToCompatibilityEntry(event);
|
|
700
|
+
} catch {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
compatibilityEntryToTrace(entry) {
|
|
705
|
+
if (entry.type === "command") {
|
|
706
|
+
return normalizeTraceEvent({
|
|
707
|
+
traceId: `cmd-${entry.seq}`,
|
|
708
|
+
sessionId: this.sessionId,
|
|
709
|
+
ts: entry.ts,
|
|
710
|
+
channel: "action",
|
|
711
|
+
event: entry.status === "failed" ? "action.failed" : "action.succeeded",
|
|
712
|
+
severity: entry.status === "failed" ? "error" : "info",
|
|
713
|
+
summary: `${entry.cmd ?? "command"}${entry.selectorUsed ? ` ${entry.selectorUsed}` : ""}`,
|
|
714
|
+
data: {
|
|
715
|
+
cmd: entry.cmd ?? null,
|
|
716
|
+
args: entry.args ?? {},
|
|
717
|
+
durationMs: entry.durationMs ?? null,
|
|
718
|
+
error: entry.error ?? null,
|
|
719
|
+
hints: entry.hints ?? [],
|
|
720
|
+
screenshotFile: entry.screenshotFile ?? null,
|
|
721
|
+
legacy: entry
|
|
722
|
+
},
|
|
723
|
+
selectorUsed: entry.selectorUsed,
|
|
724
|
+
url: entry.urlAfter ?? entry.urlBefore
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (entry.type === "error") {
|
|
728
|
+
return normalizeTraceEvent({
|
|
729
|
+
traceId: `err-${entry.seq}`,
|
|
730
|
+
sessionId: this.sessionId,
|
|
731
|
+
ts: entry.ts,
|
|
732
|
+
channel: "runtime",
|
|
733
|
+
event: "runtime.exception",
|
|
734
|
+
severity: "error",
|
|
735
|
+
summary: entry.error ?? "Session error",
|
|
736
|
+
data: {
|
|
737
|
+
args: entry.args ?? {},
|
|
738
|
+
legacy: entry
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return normalizeTraceEvent({
|
|
743
|
+
traceId: `evt-${entry.seq}`,
|
|
744
|
+
sessionId: this.sessionId,
|
|
745
|
+
ts: entry.ts,
|
|
746
|
+
channel: "session",
|
|
747
|
+
event: entry.cmd ?? "session.event",
|
|
748
|
+
severity: entry.status === "failed" ? "error" : "info",
|
|
749
|
+
summary: entry.cmd ?? "Session event",
|
|
750
|
+
data: {
|
|
751
|
+
args: entry.args ?? {},
|
|
752
|
+
status: entry.status ?? null,
|
|
753
|
+
legacy: entry
|
|
754
|
+
},
|
|
755
|
+
url: entry.urlAfter ?? entry.urlBefore
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
traceToCompatibilityEntry(event) {
|
|
759
|
+
const legacy = event.data["legacy"];
|
|
760
|
+
if (legacy && typeof legacy === "object") {
|
|
761
|
+
return legacy;
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
seq: this.seq,
|
|
765
|
+
ts: event.ts,
|
|
766
|
+
type: event.severity === "error" ? "error" : event.channel === "action" ? "command" : "event",
|
|
767
|
+
cmd: event.event,
|
|
768
|
+
args: event.data,
|
|
769
|
+
status: event.event === "action.failed" ? "failed" : event.event === "action.succeeded" ? "success" : void 0,
|
|
770
|
+
selectorUsed: event.selectorUsed,
|
|
771
|
+
urlAfter: event.url,
|
|
772
|
+
error: event.severity === "error" ? event.summary : void 0
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
var loggerCache = /* @__PURE__ */ new Map();
|
|
777
|
+
function getSessionLogger(sessionId, exportLogPath) {
|
|
778
|
+
const cacheKey = exportLogPath ? `${sessionId}:${exportLogPath}` : sessionId;
|
|
779
|
+
let logger = loggerCache.get(cacheKey);
|
|
780
|
+
if (!logger) {
|
|
781
|
+
logger = new SessionLogger(sessionId, exportLogPath);
|
|
782
|
+
loggerCache.set(cacheKey, logger);
|
|
783
|
+
}
|
|
784
|
+
return logger;
|
|
785
|
+
}
|
|
786
|
+
|
|
452
787
|
// src/cli/commands/audio.ts
|
|
453
788
|
var AUDIO_HELP = `
|
|
454
|
-
bp audio -
|
|
789
|
+
bp audio - Actively exercise voice and audio pipelines
|
|
790
|
+
|
|
791
|
+
When to use:
|
|
792
|
+
You need to inject microphone input, capture spoken output, or quickly validate a voice stack.
|
|
793
|
+
|
|
794
|
+
When not to use:
|
|
795
|
+
You are investigating cross-cutting failure causes over time. Use \`bp trace summary --view voice\` after capture.
|
|
796
|
+
|
|
797
|
+
Default flow:
|
|
798
|
+
setup -> goto or activate the app -> check -> roundtrip or capture -> trace summary
|
|
799
|
+
|
|
800
|
+
Common mistake:
|
|
801
|
+
Injecting overrides after the app already created its audio pipeline.
|
|
455
802
|
|
|
456
803
|
Feed audio as microphone input, capture the agent's spoken response,
|
|
457
804
|
and optionally transcribe it. Designed for AI apps that respond with
|
|
@@ -461,11 +808,11 @@ Usage:
|
|
|
461
808
|
bp audio <subcommand> [options]
|
|
462
809
|
|
|
463
810
|
Subcommands:
|
|
811
|
+
setup Inject audio hooks into the session
|
|
812
|
+
check Validate the current audio pipeline
|
|
464
813
|
roundtrip Play input + capture response (full voice round-trip)
|
|
465
814
|
play Feed audio file into the page's fake microphone
|
|
466
815
|
capture Capture audio output from the page
|
|
467
|
-
setup Set up audio I/O on the session (auto-runs if needed)
|
|
468
|
-
check Validate audio pipeline and report status
|
|
469
816
|
|
|
470
817
|
Common Options:
|
|
471
818
|
-s, --session [id] Session to use (omit: auto-connect, -s: latest, -s <id>: specific)
|
|
@@ -494,51 +841,32 @@ Roundtrip Options:
|
|
|
494
841
|
--timeout <ms> Max total round-trip time (default: 120000)
|
|
495
842
|
--send-selector <sel> Click this selector after input finishes (push-to-talk)
|
|
496
843
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
4. bp audio check -s my-test (expect READY)
|
|
517
|
-
If capture returns empty, reload the page so the agent re-initializes
|
|
518
|
-
after overrides are in place.
|
|
519
|
-
|
|
520
|
-
Diagnosing Issues:
|
|
521
|
-
bp audio check -s SESSION Validate the full audio pipeline
|
|
522
|
-
bp audio check -s SESSION --json Machine-readable pipeline status
|
|
523
|
-
|
|
524
|
-
0 AudioContexts = agent not initialized (wait or reload)
|
|
525
|
-
NOT READY = overrides missing (run setup, then reload)
|
|
526
|
-
latencyMs: -1 = agent didn't respond (check bp audio check)
|
|
527
|
-
Garbage transcript = sample rate issue (use --verbose)
|
|
528
|
-
|
|
529
|
-
Quick Validation:
|
|
530
|
-
1. bp audio check -> shows "READY" with agent AudioContext detected
|
|
531
|
-
2. bp audio roundtrip -> shows non-zero latencyMs and response duration
|
|
532
|
-
3. If latencyMs is -1, agent didn't respond -- check audio check output
|
|
844
|
+
Typical workflows:
|
|
845
|
+
Click-to-start app:
|
|
846
|
+
bp audio setup -s vt
|
|
847
|
+
bp exec -s vt '{"action":"goto","url":"https://my-voice-app.com"}'
|
|
848
|
+
bp snapshot -i -s vt
|
|
849
|
+
bp exec -s vt '{"action":"click","selector":"ref:e4"}'
|
|
850
|
+
bp audio check -s vt
|
|
851
|
+
bp audio roundtrip -s vt -i prompt.wav --transcribe -o response.wav
|
|
852
|
+
|
|
853
|
+
Auto-start app:
|
|
854
|
+
bp audio setup -s vt
|
|
855
|
+
bp exec -s vt '{"action":"goto","url":"https://my-voice-app.com"}'
|
|
856
|
+
bp audio check -s vt
|
|
857
|
+
bp audio roundtrip -s vt -i prompt.wav --transcribe
|
|
858
|
+
|
|
859
|
+
Likely next commands:
|
|
860
|
+
bp trace summary -s vt --view voice
|
|
861
|
+
bp record -s vt --profile voice
|
|
862
|
+
bp env permissions grant -s vt microphone
|
|
533
863
|
|
|
534
864
|
Tips:
|
|
535
|
-
-
|
|
536
|
-
|
|
537
|
-
-
|
|
538
|
-
-
|
|
539
|
-
- --
|
|
540
|
-
- Use --verbose to see per-chunk RMS and silence detection.
|
|
541
|
-
- Use --json for structured output in CI/scripting.
|
|
865
|
+
- \`bp audio check\` is the first diagnostic command.
|
|
866
|
+
- If you see 0 AudioContexts, the app has not initialized yet.
|
|
867
|
+
- If you see NOT READY, run \`bp audio setup\` and reload or re-open the app flow.
|
|
868
|
+
- Use \`--send-selector\` for push-to-talk UIs.
|
|
869
|
+
- Use \`bp trace summary --view voice\` when the question is causal, not just operational.
|
|
542
870
|
|
|
543
871
|
Environment:
|
|
544
872
|
OPENAI_API_KEY Required for --transcribe. Validated immediately on use.
|
|
@@ -611,10 +939,14 @@ async function resolveConnection(sessionId, useLatestSession, trace) {
|
|
|
611
939
|
}
|
|
612
940
|
let wsUrl;
|
|
613
941
|
try {
|
|
614
|
-
wsUrl = await
|
|
615
|
-
} catch {
|
|
942
|
+
wsUrl = (await resolveCLIEndpoint()).wsUrl;
|
|
943
|
+
} catch (error) {
|
|
616
944
|
throw new Error(
|
|
617
|
-
|
|
945
|
+
formatBrowserDiscoveryError(error, {
|
|
946
|
+
explicitHint: " - Create a session first: bp connect --browser-url <ws-url>",
|
|
947
|
+
reuseSessionHint: "bp audio -s <session-id>",
|
|
948
|
+
latestSessionHint: "bp audio -s"
|
|
949
|
+
})
|
|
618
950
|
);
|
|
619
951
|
}
|
|
620
952
|
const browser = await connect({ provider: "generic", wsUrl, debug: trace });
|
|
@@ -633,13 +965,13 @@ async function resolveConnection(sessionId, useLatestSession, trace) {
|
|
|
633
965
|
return { browser, session, isNewSession: true };
|
|
634
966
|
}
|
|
635
967
|
async function readInputFile(filePath) {
|
|
636
|
-
const
|
|
637
|
-
const buffer = await
|
|
968
|
+
const fs9 = await import("fs/promises");
|
|
969
|
+
const buffer = await fs9.readFile(filePath);
|
|
638
970
|
return new Uint8Array(buffer);
|
|
639
971
|
}
|
|
640
972
|
async function getFileSize(filePath) {
|
|
641
|
-
const
|
|
642
|
-
const stat = await
|
|
973
|
+
const fs9 = await import("fs/promises");
|
|
974
|
+
const stat = await fs9.stat(filePath);
|
|
643
975
|
return stat.size;
|
|
644
976
|
}
|
|
645
977
|
function formatMs(ms) {
|
|
@@ -654,8 +986,8 @@ function basename(filePath) {
|
|
|
654
986
|
return filePath.split("/").pop()?.split("\\").pop() ?? filePath;
|
|
655
987
|
}
|
|
656
988
|
async function writeWavFile(filePath, data) {
|
|
657
|
-
const
|
|
658
|
-
await
|
|
989
|
+
const fs9 = await import("fs/promises");
|
|
990
|
+
await fs9.writeFile(filePath, new Uint8Array(data));
|
|
659
991
|
}
|
|
660
992
|
var CHECK_DIAGNOSTICS_EXPRESSION = `
|
|
661
993
|
(function() {
|
|
@@ -854,6 +1186,7 @@ async function audioCommand(args, globalOptions) {
|
|
|
854
1186
|
options.useLatestSession ?? false,
|
|
855
1187
|
globalOptions.trace ?? false
|
|
856
1188
|
);
|
|
1189
|
+
const logger = getSessionLogger(session.id, session.exportLog);
|
|
857
1190
|
if (isNewSession) {
|
|
858
1191
|
console.log(`Created new session: ${session.id}`);
|
|
859
1192
|
}
|
|
@@ -863,6 +1196,12 @@ async function audioCommand(args, globalOptions) {
|
|
|
863
1196
|
case "setup": {
|
|
864
1197
|
await page.setupAudio();
|
|
865
1198
|
const msg = "Audio I/O set up (microphone override + output capture ready)";
|
|
1199
|
+
logger.logTrace({
|
|
1200
|
+
channel: "voice",
|
|
1201
|
+
event: "voice.pipeline.ready",
|
|
1202
|
+
summary: msg,
|
|
1203
|
+
data: { subcommand: "setup" }
|
|
1204
|
+
});
|
|
866
1205
|
output(
|
|
867
1206
|
globalOptions.format === "json" ? { success: true, message: msg } : msg,
|
|
868
1207
|
globalOptions.format
|
|
@@ -875,8 +1214,24 @@ async function audioCommand(args, globalOptions) {
|
|
|
875
1214
|
const parsedDiag = JSON.parse(rawDiag);
|
|
876
1215
|
if (!isRecord(parsedDiag)) throw new Error("Invalid audio diagnostics payload");
|
|
877
1216
|
const diag = parsedDiag;
|
|
1217
|
+
const checkJson = buildCheckJson(diag);
|
|
1218
|
+
logger.logTrace({
|
|
1219
|
+
channel: "voice",
|
|
1220
|
+
event: checkJson.ready ? "voice.pipeline.ready" : "voice.pipeline.notReady",
|
|
1221
|
+
severity: checkJson.ready ? "info" : "error",
|
|
1222
|
+
summary: checkJson.ready ? "Audio pipeline ready" : "Audio pipeline not ready",
|
|
1223
|
+
data: checkJson
|
|
1224
|
+
});
|
|
1225
|
+
if (checkJson.agentDetected) {
|
|
1226
|
+
logger.logTrace({
|
|
1227
|
+
channel: "media",
|
|
1228
|
+
event: "media.track.started",
|
|
1229
|
+
summary: "Audio track detected during audio check",
|
|
1230
|
+
data: { kind: "audio", sampleRate: checkJson.agentSampleRate }
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
878
1233
|
if (globalOptions.format === "json") {
|
|
879
|
-
output(
|
|
1234
|
+
output(checkJson, "json");
|
|
880
1235
|
} else {
|
|
881
1236
|
console.log(formatCheckPretty(diag));
|
|
882
1237
|
}
|
|
@@ -889,8 +1244,20 @@ async function audioCommand(args, globalOptions) {
|
|
|
889
1244
|
const audioData = await readInputFile(options.input);
|
|
890
1245
|
console.log(`Playing ${options.input} (${audioData.length} bytes)...`);
|
|
891
1246
|
const start = Date.now();
|
|
1247
|
+
logger.logTrace({
|
|
1248
|
+
channel: "voice",
|
|
1249
|
+
event: "voice.capture.started",
|
|
1250
|
+
summary: "Audio playback started",
|
|
1251
|
+
data: { file: options.input }
|
|
1252
|
+
});
|
|
892
1253
|
await page.audioInput.play(audioData, { waitForEnd: !options.noWait });
|
|
893
1254
|
const durationMs = Date.now() - start;
|
|
1255
|
+
logger.logTrace({
|
|
1256
|
+
channel: "voice",
|
|
1257
|
+
event: "voice.capture.stopped",
|
|
1258
|
+
summary: "Audio playback finished",
|
|
1259
|
+
data: { file: options.input, durationMs }
|
|
1260
|
+
});
|
|
894
1261
|
const result = { success: true, file: options.input, durationMs };
|
|
895
1262
|
output(
|
|
896
1263
|
globalOptions.format === "json" ? result : `Playback complete (${durationMs}ms)`,
|
|
@@ -908,6 +1275,12 @@ async function audioCommand(args, globalOptions) {
|
|
|
908
1275
|
});
|
|
909
1276
|
}
|
|
910
1277
|
let capture;
|
|
1278
|
+
logger.logTrace({
|
|
1279
|
+
channel: "media",
|
|
1280
|
+
event: "media.playback.started",
|
|
1281
|
+
summary: "Audio capture started",
|
|
1282
|
+
data: { subcommand: "capture" }
|
|
1283
|
+
});
|
|
911
1284
|
if (options.duration && options.duration > 0) {
|
|
912
1285
|
await page.audioOutput.start();
|
|
913
1286
|
await sleep(options.duration);
|
|
@@ -919,6 +1292,15 @@ async function audioCommand(args, globalOptions) {
|
|
|
919
1292
|
maxDuration: options.maxDuration ?? 3e5
|
|
920
1293
|
});
|
|
921
1294
|
}
|
|
1295
|
+
logger.logTrace({
|
|
1296
|
+
channel: "media",
|
|
1297
|
+
event: "media.playback.stopped",
|
|
1298
|
+
summary: "Audio capture stopped",
|
|
1299
|
+
data: {
|
|
1300
|
+
durationMs: Math.round(capture.durationMs),
|
|
1301
|
+
samples: capture.left.length
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
922
1304
|
if (options.out) {
|
|
923
1305
|
const wav = pcmToWav({
|
|
924
1306
|
left: capture.left,
|
|
@@ -987,6 +1369,12 @@ async function audioCommand(args, globalOptions) {
|
|
|
987
1369
|
preDelay: options.preDelay,
|
|
988
1370
|
sendSelector: options.sendSelector
|
|
989
1371
|
});
|
|
1372
|
+
logger.logTrace({
|
|
1373
|
+
channel: "voice",
|
|
1374
|
+
event: "voice.capture.started",
|
|
1375
|
+
summary: "Voice roundtrip started",
|
|
1376
|
+
data: { file: options.input }
|
|
1377
|
+
});
|
|
990
1378
|
let savedFile;
|
|
991
1379
|
if (options.out) {
|
|
992
1380
|
if (result.audio.left.length === 0) {
|
|
@@ -1007,6 +1395,17 @@ async function audioCommand(args, globalOptions) {
|
|
|
1007
1395
|
transcript = tr.text;
|
|
1008
1396
|
}
|
|
1009
1397
|
const hasResponse = result.latencyMs !== -1 && result.audio.left.length > 0;
|
|
1398
|
+
logger.logTrace({
|
|
1399
|
+
channel: hasResponse ? "voice" : "media",
|
|
1400
|
+
event: hasResponse ? "voice.capture.detectedAudio" : "voice.pipeline.notReady",
|
|
1401
|
+
severity: hasResponse ? "info" : "error",
|
|
1402
|
+
summary: hasResponse ? "Voice response captured" : "Voice response missing",
|
|
1403
|
+
data: {
|
|
1404
|
+
latencyMs: result.latencyMs,
|
|
1405
|
+
durationMs: Math.round(result.audio.durationMs),
|
|
1406
|
+
samples: result.audio.left.length
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1010
1409
|
if (globalOptions.format === "json") {
|
|
1011
1410
|
const jsonResult = {
|
|
1012
1411
|
success: true,
|
|
@@ -1061,17 +1460,17 @@ Available: setup, play, capture, roundtrip, check`
|
|
|
1061
1460
|
}
|
|
1062
1461
|
}
|
|
1063
1462
|
function sleep(ms) {
|
|
1064
|
-
return new Promise((
|
|
1463
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
1065
1464
|
}
|
|
1066
1465
|
|
|
1067
1466
|
// src/cli/commands/clean.ts
|
|
1068
|
-
import * as
|
|
1069
|
-
import { homedir as
|
|
1070
|
-
import { join as
|
|
1467
|
+
import * as fs4 from "fs";
|
|
1468
|
+
import { homedir as homedir4 } from "os";
|
|
1469
|
+
import { join as join5 } from "path";
|
|
1071
1470
|
|
|
1072
1471
|
// src/daemon/lifecycle.ts
|
|
1073
|
-
import * as
|
|
1074
|
-
import { dirname, join as
|
|
1472
|
+
import * as fs3 from "fs";
|
|
1473
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1075
1474
|
function parseSessionRecord(raw) {
|
|
1076
1475
|
const parsed = JSON.parse(raw);
|
|
1077
1476
|
return isRecord(parsed) ? parsed : null;
|
|
@@ -1115,7 +1514,7 @@ async function clearDaemonFromSession(sessionFilePath) {
|
|
|
1115
1514
|
}
|
|
1116
1515
|
|
|
1117
1516
|
// src/cli/commands/clean.ts
|
|
1118
|
-
var
|
|
1517
|
+
var SESSION_DIR4 = join5(homedir4(), ".browser-pilot", "sessions");
|
|
1119
1518
|
var CLEAN_HELP = `
|
|
1120
1519
|
bp clean - Remove stale browser sessions
|
|
1121
1520
|
|
|
@@ -1161,12 +1560,12 @@ function formatBytes2(bytes) {
|
|
|
1161
1560
|
}
|
|
1162
1561
|
function getSessionSize(sessionId) {
|
|
1163
1562
|
let total = 0;
|
|
1164
|
-
const jsonPath =
|
|
1563
|
+
const jsonPath = join5(SESSION_DIR4, `${sessionId}.json`);
|
|
1165
1564
|
try {
|
|
1166
|
-
total +=
|
|
1565
|
+
total += fs4.statSync(jsonPath).size;
|
|
1167
1566
|
} catch {
|
|
1168
1567
|
}
|
|
1169
|
-
const dirPath =
|
|
1568
|
+
const dirPath = join5(SESSION_DIR4, sessionId);
|
|
1170
1569
|
try {
|
|
1171
1570
|
total += getDirSize(dirPath);
|
|
1172
1571
|
} catch {
|
|
@@ -1176,14 +1575,14 @@ function getSessionSize(sessionId) {
|
|
|
1176
1575
|
function getDirSize(dirPath) {
|
|
1177
1576
|
let total = 0;
|
|
1178
1577
|
try {
|
|
1179
|
-
const entries =
|
|
1578
|
+
const entries = fs4.readdirSync(dirPath, { withFileTypes: true });
|
|
1180
1579
|
for (const entry of entries) {
|
|
1181
|
-
const fullPath =
|
|
1580
|
+
const fullPath = join5(dirPath, entry.name);
|
|
1182
1581
|
if (entry.isDirectory()) {
|
|
1183
1582
|
total += getDirSize(fullPath);
|
|
1184
1583
|
} else if (entry.isFile()) {
|
|
1185
1584
|
try {
|
|
1186
|
-
total +=
|
|
1585
|
+
total += fs4.statSync(fullPath).size;
|
|
1187
1586
|
} catch {
|
|
1188
1587
|
}
|
|
1189
1588
|
}
|
|
@@ -1398,12 +1797,12 @@ async function closeCommand(args, globalOptions) {
|
|
|
1398
1797
|
|
|
1399
1798
|
// src/cli/daemon-spawn.ts
|
|
1400
1799
|
import { spawn } from "child_process";
|
|
1401
|
-
import * as
|
|
1402
|
-
import { dirname as
|
|
1800
|
+
import * as fs5 from "fs";
|
|
1801
|
+
import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
|
|
1403
1802
|
import { fileURLToPath } from "url";
|
|
1404
1803
|
function spawnDaemon(sessionId, idleTimeoutMs) {
|
|
1405
|
-
const thisDir =
|
|
1406
|
-
const daemonScript =
|
|
1804
|
+
const thisDir = dirname4(fileURLToPath(import.meta.url));
|
|
1805
|
+
const daemonScript = resolve3(join6(thisDir, "..", "daemon", "index.ts"));
|
|
1407
1806
|
const args = [daemonScript, sessionId];
|
|
1408
1807
|
if (idleTimeoutMs) {
|
|
1409
1808
|
args.push("--idle-timeout", String(idleTimeoutMs));
|
|
@@ -1423,7 +1822,7 @@ async function waitForDaemonReady(sessionFilePath, expectedPid, timeoutMs = DAEM
|
|
|
1423
1822
|
const pollInterval = 100;
|
|
1424
1823
|
while (Date.now() < deadline) {
|
|
1425
1824
|
try {
|
|
1426
|
-
const raw =
|
|
1825
|
+
const raw = fs5.readFileSync(sessionFilePath, "utf-8");
|
|
1427
1826
|
const parsed = JSON.parse(raw);
|
|
1428
1827
|
if (!isRecord(parsed)) continue;
|
|
1429
1828
|
const daemon = isRecord(parsed["daemon"]) ? parsed["daemon"] : null;
|
|
@@ -1448,6 +1847,8 @@ Options:
|
|
|
1448
1847
|
-p, --provider <type> Provider: generic | browserbase | browserless (default: generic)
|
|
1449
1848
|
--url <value> Browser WebSocket URL, or page URL when used with --new-tab
|
|
1450
1849
|
--browser-url <ws-url> Explicit browser WebSocket URL
|
|
1850
|
+
--channel <name> Local Chrome channel: stable | beta | dev | canary
|
|
1851
|
+
--user-data-dir <path> Explicit local Chrome user data dir for auto-discovery
|
|
1451
1852
|
--page-url <url> URL to open in the attached page/new tab
|
|
1452
1853
|
-n, --name <id> Custom session name (default: auto-generated)
|
|
1453
1854
|
-r, --resume <id> Resume an existing session by ID
|
|
@@ -1467,10 +1868,12 @@ Options:
|
|
|
1467
1868
|
-h, --help Show this help
|
|
1468
1869
|
|
|
1469
1870
|
Examples:
|
|
1470
|
-
bp connect # Auto-connect to local Chrome
|
|
1871
|
+
bp connect # Auto-connect to local Chrome
|
|
1872
|
+
bp connect --channel beta # Narrow auto-discovery to Chrome Beta
|
|
1873
|
+
bp connect --user-data-dir ~/tmp/chrome-dev # Use a specific Chrome profile
|
|
1471
1874
|
bp connect --record # Connect with session-level recording
|
|
1472
|
-
bp connect --
|
|
1473
|
-
bp connect --url ws://localhost:9222/devtools # Explicit WebSocket URL
|
|
1875
|
+
bp connect --name dev # Auto-connect with a custom session name
|
|
1876
|
+
bp connect --url ws://localhost:9222/devtools/browser/abc123 # Explicit WebSocket URL
|
|
1474
1877
|
bp connect --resume dev # Resume a previous session
|
|
1475
1878
|
bp connect --target-url localhost:3000 # Attach to tab matching URL
|
|
1476
1879
|
bp connect --new-tab --url https://example.com # Create and attach to a fresh tab
|
|
@@ -1486,6 +1889,14 @@ function parseConnectArgs(args) {
|
|
|
1486
1889
|
options.url = args[++i];
|
|
1487
1890
|
} else if (arg === "--browser-url") {
|
|
1488
1891
|
options.browserUrl = args[++i];
|
|
1892
|
+
} else if (arg === "--channel") {
|
|
1893
|
+
const channel = args[++i];
|
|
1894
|
+
if (channel !== "stable" && channel !== "beta" && channel !== "dev" && channel !== "canary") {
|
|
1895
|
+
throw new Error("--channel must be one of: stable, beta, dev, canary");
|
|
1896
|
+
}
|
|
1897
|
+
options.channel = channel;
|
|
1898
|
+
} else if (arg === "--user-data-dir") {
|
|
1899
|
+
options.userDataDir = args[++i];
|
|
1489
1900
|
} else if (arg === "--page-url") {
|
|
1490
1901
|
options.pageUrl = args[++i];
|
|
1491
1902
|
} else if (arg === "--name" || arg === "-n") {
|
|
@@ -1561,6 +1972,9 @@ async function connectCommand(args, globalOptions) {
|
|
|
1561
1972
|
const provider = options.provider ?? "generic";
|
|
1562
1973
|
let wsUrl = options.browserUrl ?? options.url;
|
|
1563
1974
|
let pageUrl = options.pageUrl;
|
|
1975
|
+
let connectionSource;
|
|
1976
|
+
let resolvedChannel;
|
|
1977
|
+
let resolvedUserDataDir;
|
|
1564
1978
|
if (options.newTab && options.url && !options.url.startsWith("ws://") && !options.url.startsWith("wss://")) {
|
|
1565
1979
|
pageUrl = options.url;
|
|
1566
1980
|
if (!options.browserUrl) {
|
|
@@ -1569,17 +1983,31 @@ async function connectCommand(args, globalOptions) {
|
|
|
1569
1983
|
}
|
|
1570
1984
|
if (provider === "generic" && !wsUrl) {
|
|
1571
1985
|
try {
|
|
1572
|
-
|
|
1573
|
-
|
|
1986
|
+
const resolved = await resolveCLIEndpoint({
|
|
1987
|
+
explicitWsUrl: wsUrl,
|
|
1988
|
+
channel: options.channel,
|
|
1989
|
+
userDataDir: options.userDataDir
|
|
1990
|
+
});
|
|
1991
|
+
wsUrl = resolved.wsUrl;
|
|
1992
|
+
connectionSource = resolved.source;
|
|
1993
|
+
resolvedChannel = resolved.channel;
|
|
1994
|
+
resolvedUserDataDir = resolved.userDataDir;
|
|
1995
|
+
} catch (error) {
|
|
1574
1996
|
throw new Error(
|
|
1575
|
-
|
|
1997
|
+
formatBrowserDiscoveryError(error, {
|
|
1998
|
+
explicitFlag: "--browser-url"
|
|
1999
|
+
})
|
|
1576
2000
|
);
|
|
1577
2001
|
}
|
|
2002
|
+
} else if (wsUrl) {
|
|
2003
|
+
connectionSource = "explicit-ws";
|
|
1578
2004
|
}
|
|
1579
2005
|
const connectOptions = {
|
|
1580
2006
|
provider,
|
|
1581
2007
|
debug: globalOptions.trace,
|
|
1582
2008
|
wsUrl,
|
|
2009
|
+
channel: options.channel,
|
|
2010
|
+
userDataDir: options.userDataDir,
|
|
1583
2011
|
apiKey: options.apiKey,
|
|
1584
2012
|
projectId: options.projectId
|
|
1585
2013
|
};
|
|
@@ -1609,9 +2037,13 @@ async function connectCommand(args, globalOptions) {
|
|
|
1609
2037
|
currentUrl,
|
|
1610
2038
|
metadata: {
|
|
1611
2039
|
...browser.metadata,
|
|
2040
|
+
...connectionSource ? { connectionSource } : {},
|
|
2041
|
+
...resolvedChannel ? { resolvedChannel } : {},
|
|
2042
|
+
...resolvedUserDataDir ? { resolvedUserDataDir } : {},
|
|
1612
2043
|
...recordSettings ? { record: recordSettings } : {}
|
|
1613
2044
|
}
|
|
1614
2045
|
};
|
|
2046
|
+
const outputMetadata = session.metadata;
|
|
1615
2047
|
await saveSession(session);
|
|
1616
2048
|
await browser.disconnect();
|
|
1617
2049
|
let daemonResult;
|
|
@@ -1639,7 +2071,10 @@ async function connectCommand(args, globalOptions) {
|
|
|
1639
2071
|
provider,
|
|
1640
2072
|
currentUrl,
|
|
1641
2073
|
recording: !!recordSettings,
|
|
1642
|
-
|
|
2074
|
+
connectionSource,
|
|
2075
|
+
resolvedChannel,
|
|
2076
|
+
resolvedUserDataDir,
|
|
2077
|
+
metadata: outputMetadata,
|
|
1643
2078
|
daemon: daemonResult
|
|
1644
2079
|
},
|
|
1645
2080
|
globalOptions.format
|
|
@@ -1647,10 +2082,10 @@ async function connectCommand(args, globalOptions) {
|
|
|
1647
2082
|
}
|
|
1648
2083
|
|
|
1649
2084
|
// src/cli/commands/daemon.ts
|
|
1650
|
-
import * as
|
|
1651
|
-
import { homedir as
|
|
1652
|
-
import { join as
|
|
1653
|
-
var
|
|
2085
|
+
import * as fs6 from "fs";
|
|
2086
|
+
import { homedir as homedir5 } from "os";
|
|
2087
|
+
import { join as join7 } from "path";
|
|
2088
|
+
var SESSION_DIR5 = join7(homedir5(), ".browser-pilot", "sessions");
|
|
1654
2089
|
var DAEMON_HELP = `
|
|
1655
2090
|
bp daemon - Manage daemon processes for browser sessions
|
|
1656
2091
|
|
|
@@ -1766,12 +2201,12 @@ async function daemonCommand(args, globalOptions) {
|
|
|
1766
2201
|
}
|
|
1767
2202
|
case "logs": {
|
|
1768
2203
|
const session = await getSession(globalOptions);
|
|
1769
|
-
const logPath =
|
|
1770
|
-
if (!
|
|
2204
|
+
const logPath = join7(SESSION_DIR5, session.id, "daemon.log");
|
|
2205
|
+
if (!fs6.existsSync(logPath)) {
|
|
1771
2206
|
output({ sessionId: session.id, message: "No daemon log found" }, globalOptions.format);
|
|
1772
2207
|
return;
|
|
1773
2208
|
}
|
|
1774
|
-
const content =
|
|
2209
|
+
const content = fs6.readFileSync(logPath, "utf-8");
|
|
1775
2210
|
const lines = content.trim().split("\n");
|
|
1776
2211
|
const n = daemonOptions.lines ?? 50;
|
|
1777
2212
|
const tail = lines.slice(-n);
|
|
@@ -2278,6 +2713,15 @@ async function diagnoseElement(page, selector, options = {}) {
|
|
|
2278
2713
|
var DIAGNOSE_HELP = `
|
|
2279
2714
|
bp diagnose - Debug element selection and find alternatives
|
|
2280
2715
|
|
|
2716
|
+
When to use:
|
|
2717
|
+
A selector or ref failed and you need to understand why.
|
|
2718
|
+
|
|
2719
|
+
When not to use:
|
|
2720
|
+
You have not inspected the page yet. Start with \`bp snapshot -i\`.
|
|
2721
|
+
|
|
2722
|
+
Common mistake:
|
|
2723
|
+
Jumping to raw JavaScript before checking browser-pilot's selector diagnostics and suggested alternatives.
|
|
2724
|
+
|
|
2281
2725
|
Usage:
|
|
2282
2726
|
bp diagnose <selector> Diagnose specific selector
|
|
2283
2727
|
bp diagnose "<fuzzy query>" Fuzzy search for elements
|
|
@@ -2288,19 +2732,14 @@ Examples:
|
|
|
2288
2732
|
bp diagnose "ref:e4" Diagnose by element ref
|
|
2289
2733
|
|
|
2290
2734
|
Options:
|
|
2291
|
-
--json
|
|
2292
|
-
--max <n>
|
|
2293
|
-
-s, --session <id>
|
|
2294
|
-
--help
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
- Alternative selectors
|
|
2300
|
-
|
|
2301
|
-
Output (fuzzy match):
|
|
2302
|
-
- Top N candidates ranked by similarity
|
|
2303
|
-
- Role, name, visibility for each
|
|
2735
|
+
--json Output as JSON
|
|
2736
|
+
--max <n> Max candidates for fuzzy match (default: 5)
|
|
2737
|
+
-s, --session <id> Use specific session
|
|
2738
|
+
--help Show this help
|
|
2739
|
+
|
|
2740
|
+
Likely next commands:
|
|
2741
|
+
bp exec '[{"action":"click","selector":"<suggested-selector>"}]'
|
|
2742
|
+
bp snapshot -i
|
|
2304
2743
|
`;
|
|
2305
2744
|
function parseDiagnoseArgs(args) {
|
|
2306
2745
|
const options = {};
|
|
@@ -2430,367 +2869,1069 @@ async function diagnoseCommand(args, globalOptions) {
|
|
|
2430
2869
|
}
|
|
2431
2870
|
}
|
|
2432
2871
|
|
|
2433
|
-
// src/cli/
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2872
|
+
// src/cli/commands/env.ts
|
|
2873
|
+
import { dirname as dirname5 } from "path";
|
|
2874
|
+
|
|
2875
|
+
// src/cli/env-state.ts
|
|
2876
|
+
function normalizeStoredPermission(name) {
|
|
2877
|
+
const value = String(name).trim().toLowerCase();
|
|
2878
|
+
if (value === "microphone" || value === "audio" || value === "audiocapture" || value === "audio-capture") {
|
|
2879
|
+
return "microphone";
|
|
2437
2880
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
throw new Error('No session found. Run "bp connect" first.');
|
|
2881
|
+
if (value === "camera" || value === "videocapture" || value === "video-capture") {
|
|
2882
|
+
return "camera";
|
|
2441
2883
|
}
|
|
2442
|
-
|
|
2884
|
+
if (value === "notifications") {
|
|
2885
|
+
return "notifications";
|
|
2886
|
+
}
|
|
2887
|
+
if (value === "geolocation") {
|
|
2888
|
+
return "geolocation";
|
|
2889
|
+
}
|
|
2890
|
+
return null;
|
|
2443
2891
|
}
|
|
2444
|
-
function
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2892
|
+
function toProtocolPermission(name) {
|
|
2893
|
+
const normalized = normalizeStoredPermission(name);
|
|
2894
|
+
if (normalized === "microphone") return "audioCapture";
|
|
2895
|
+
if (normalized === "camera") return "videoCapture";
|
|
2896
|
+
if (normalized === "notifications") return "notifications";
|
|
2897
|
+
if (normalized === "geolocation") return "geolocation";
|
|
2898
|
+
return null;
|
|
2899
|
+
}
|
|
2900
|
+
function originFromUrl(url) {
|
|
2901
|
+
try {
|
|
2902
|
+
const parsed = new URL(url ?? "");
|
|
2903
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
2904
|
+
return parsed.origin;
|
|
2454
2905
|
}
|
|
2906
|
+
return void 0;
|
|
2907
|
+
} catch {
|
|
2908
|
+
return void 0;
|
|
2455
2909
|
}
|
|
2456
|
-
return isDaemonAlive(session.daemon.pid);
|
|
2457
2910
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2911
|
+
function buildPermissionOverrideScript(granted) {
|
|
2912
|
+
const normalized = [
|
|
2913
|
+
...new Set(granted.map((value) => normalizeStoredPermission(value)).filter(Boolean))
|
|
2914
|
+
];
|
|
2915
|
+
return `
|
|
2916
|
+
(() => {
|
|
2917
|
+
const granted = ${JSON.stringify(normalized)};
|
|
2918
|
+
globalThis.__bpGrantedPermissions = granted;
|
|
2919
|
+
|
|
2920
|
+
if (!navigator.permissions || !navigator.permissions.query) {
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
if (!globalThis.__bpPermissionOverrideInstalled) {
|
|
2925
|
+
globalThis.__bpPermissionOverrideInstalled = true;
|
|
2926
|
+
const originalQuery = navigator.permissions.query.bind(navigator.permissions);
|
|
2927
|
+
navigator.permissions.query = function(desc) {
|
|
2928
|
+
const rawName = desc && desc.name ? String(desc.name) : '';
|
|
2929
|
+
const normalizedName =
|
|
2930
|
+
rawName === 'audio-capture' || rawName === 'audioCapture'
|
|
2931
|
+
? 'microphone'
|
|
2932
|
+
: rawName === 'video-capture' || rawName === 'videoCapture'
|
|
2933
|
+
? 'camera'
|
|
2934
|
+
: rawName;
|
|
2935
|
+
if (Array.isArray(globalThis.__bpGrantedPermissions) && globalThis.__bpGrantedPermissions.includes(normalizedName)) {
|
|
2936
|
+
return Promise.resolve({
|
|
2937
|
+
state: 'granted',
|
|
2938
|
+
onchange: null,
|
|
2939
|
+
addEventListener() {},
|
|
2940
|
+
removeEventListener() {},
|
|
2941
|
+
dispatchEvent() { return true; },
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
return originalQuery(desc);
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
})();
|
|
2948
|
+
`.trim();
|
|
2949
|
+
}
|
|
2950
|
+
function buildVisibilityOverrideScript(state) {
|
|
2951
|
+
return `
|
|
2952
|
+
(() => {
|
|
2953
|
+
const nextState = ${JSON.stringify(state)};
|
|
2954
|
+
|
|
2955
|
+
if (!globalThis.__bpApplyVisibilityState) {
|
|
2956
|
+
globalThis.__bpApplyVisibilityState = function(value) {
|
|
2957
|
+
Object.defineProperty(document, 'visibilityState', {
|
|
2958
|
+
configurable: true,
|
|
2959
|
+
get() { return value; },
|
|
2466
2960
|
});
|
|
2467
|
-
|
|
2468
|
-
|
|
2961
|
+
Object.defineProperty(document, 'hidden', {
|
|
2962
|
+
configurable: true,
|
|
2963
|
+
get() { return value === 'hidden'; },
|
|
2964
|
+
});
|
|
2965
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
2966
|
+
};
|
|
2469
2967
|
}
|
|
2968
|
+
|
|
2969
|
+
globalThis.__bpForcedVisibilityState = nextState;
|
|
2970
|
+
globalThis.__bpApplyVisibilityState(nextState);
|
|
2971
|
+
})();
|
|
2972
|
+
`.trim();
|
|
2470
2973
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2974
|
+
function buildNetworkOverrideScript(state) {
|
|
2975
|
+
const normalized = {
|
|
2976
|
+
offline: Boolean(state?.offline),
|
|
2977
|
+
latency: Math.max(0, Math.round(state?.latency ?? 0))
|
|
2978
|
+
};
|
|
2979
|
+
return `
|
|
2980
|
+
(() => {
|
|
2981
|
+
const config = ${JSON.stringify(normalized)};
|
|
2982
|
+
globalThis.__bpNetworkOverrideState = config;
|
|
2983
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
2984
|
+
|
|
2985
|
+
if (!globalThis.__bpNetworkOverrideInstalled) {
|
|
2986
|
+
globalThis.__bpNetworkOverrideInstalled = true;
|
|
2987
|
+
|
|
2988
|
+
const originalFetch = globalThis.fetch ? globalThis.fetch.bind(globalThis) : null;
|
|
2989
|
+
if (originalFetch) {
|
|
2990
|
+
globalThis.fetch = function(...args) {
|
|
2991
|
+
const current = globalThis.__bpNetworkOverrideState || { offline: false, latency: 0 };
|
|
2992
|
+
if (current.offline) {
|
|
2993
|
+
return Promise.reject(new TypeError('Failed to fetch'));
|
|
2994
|
+
}
|
|
2995
|
+
return new Promise((resolve, reject) => {
|
|
2996
|
+
setTimeout(() => {
|
|
2997
|
+
originalFetch(...args).then(resolve, reject);
|
|
2998
|
+
}, current.latency || 0);
|
|
2483
2999
|
});
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
if (globalThis.XMLHttpRequest && !globalThis.__bpNetworkXhrPatched) {
|
|
3004
|
+
globalThis.__bpNetworkXhrPatched = true;
|
|
3005
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
3006
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
3007
|
+
const current = globalThis.__bpNetworkOverrideState || { offline: false, latency: 0 };
|
|
3008
|
+
if (current.offline) {
|
|
3009
|
+
setTimeout(() => {
|
|
3010
|
+
this.dispatchEvent(new ProgressEvent('error'));
|
|
3011
|
+
if (typeof this.onerror === 'function') {
|
|
3012
|
+
this.onerror(new ProgressEvent('error'));
|
|
3013
|
+
}
|
|
3014
|
+
}, 0);
|
|
3015
|
+
return;
|
|
2491
3016
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
3017
|
+
const invoke = () => originalSend.call(this, body);
|
|
3018
|
+
if (current.latency > 0) {
|
|
3019
|
+
setTimeout(invoke, current.latency);
|
|
3020
|
+
} else {
|
|
3021
|
+
invoke();
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
if (globalThis.WebSocket && !globalThis.__bpNetworkWebSocketPatched) {
|
|
3027
|
+
globalThis.__bpNetworkWebSocketPatched = true;
|
|
3028
|
+
const NativeWebSocket = globalThis.WebSocket;
|
|
3029
|
+
const notifyOfflineSocket = (socket, code = 1012, reason = 'offline') => {
|
|
3030
|
+
if (!socket || socket.__bpOfflineNotified) {
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
try {
|
|
3034
|
+
socket.__bpOfflineNotified = true;
|
|
3035
|
+
} catch {}
|
|
3036
|
+
try {
|
|
3037
|
+
socket.readyState = 3;
|
|
3038
|
+
} catch {}
|
|
3039
|
+
const errorEvent = new Event('error');
|
|
3040
|
+
const closeEvent =
|
|
3041
|
+
typeof CloseEvent === 'function'
|
|
3042
|
+
? new CloseEvent('close', { code, reason, wasClean: false })
|
|
3043
|
+
: new Event('close');
|
|
3044
|
+
try {
|
|
3045
|
+
socket.dispatchEvent(errorEvent);
|
|
3046
|
+
} catch {}
|
|
3047
|
+
try {
|
|
3048
|
+
if (typeof socket.onerror === 'function') {
|
|
3049
|
+
socket.onerror(errorEvent);
|
|
3050
|
+
}
|
|
3051
|
+
} catch {}
|
|
3052
|
+
try {
|
|
3053
|
+
socket.dispatchEvent(closeEvent);
|
|
3054
|
+
} catch {}
|
|
3055
|
+
try {
|
|
3056
|
+
if (typeof socket.onclose === 'function') {
|
|
3057
|
+
socket.onclose(closeEvent);
|
|
3058
|
+
}
|
|
3059
|
+
} catch {}
|
|
3060
|
+
};
|
|
3061
|
+
const createOfflineSocket = (url) => {
|
|
3062
|
+
const target = new EventTarget();
|
|
3063
|
+
const socket = {
|
|
3064
|
+
url: String(url),
|
|
3065
|
+
readyState: 0,
|
|
3066
|
+
bufferedAmount: 0,
|
|
3067
|
+
extensions: '',
|
|
3068
|
+
protocol: '',
|
|
3069
|
+
binaryType: 'blob',
|
|
3070
|
+
onopen: null,
|
|
3071
|
+
onerror: null,
|
|
3072
|
+
onclose: null,
|
|
3073
|
+
onmessage: null,
|
|
3074
|
+
addEventListener: target.addEventListener.bind(target),
|
|
3075
|
+
removeEventListener: target.removeEventListener.bind(target),
|
|
3076
|
+
dispatchEvent: target.dispatchEvent.bind(target),
|
|
3077
|
+
send() {
|
|
3078
|
+
throw new DOMException('WebSocket is offline', 'InvalidStateError');
|
|
3079
|
+
},
|
|
3080
|
+
close(code = 1012, reason = 'offline') {
|
|
3081
|
+
socket.readyState = 3;
|
|
3082
|
+
notifyOfflineSocket(socket, code, reason);
|
|
3083
|
+
},
|
|
3084
|
+
};
|
|
3085
|
+
setTimeout(() => {
|
|
3086
|
+
notifyOfflineSocket(socket, 1012, 'offline');
|
|
3087
|
+
}, 0);
|
|
3088
|
+
return socket;
|
|
3089
|
+
};
|
|
3090
|
+
const WrappedWebSocket = function(url, protocols) {
|
|
3091
|
+
const current = globalThis.__bpNetworkOverrideState || { offline: false };
|
|
3092
|
+
if (current.offline) {
|
|
3093
|
+
return createOfflineSocket(url);
|
|
3094
|
+
}
|
|
3095
|
+
const socket =
|
|
3096
|
+
arguments.length > 1 ? new NativeWebSocket(url, protocols) : new NativeWebSocket(url);
|
|
3097
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
3098
|
+
const nativeClose = socket.close.bind(socket);
|
|
3099
|
+
socket.close = function(code = 1000, reason = '') {
|
|
3100
|
+
const current = globalThis.__bpNetworkOverrideState || { offline: false };
|
|
3101
|
+
if (current.offline) {
|
|
3102
|
+
notifyOfflineSocket(socket, code || 1012, reason || 'offline');
|
|
3103
|
+
}
|
|
3104
|
+
return nativeClose(code, reason);
|
|
3105
|
+
};
|
|
3106
|
+
const failOffline = () => {
|
|
3107
|
+
const current = globalThis.__bpNetworkOverrideState || { offline: false };
|
|
3108
|
+
if (current.offline) {
|
|
3109
|
+
try {
|
|
3110
|
+
socket.close(1012, 'offline');
|
|
3111
|
+
} catch {}
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
socket.addEventListener('open', failOffline);
|
|
3115
|
+
socket.addEventListener('close', () => {
|
|
3116
|
+
try {
|
|
3117
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
3118
|
+
} catch {}
|
|
3119
|
+
});
|
|
3120
|
+
setTimeout(failOffline, 25);
|
|
3121
|
+
return socket;
|
|
3122
|
+
};
|
|
3123
|
+
WrappedWebSocket.prototype = NativeWebSocket.prototype;
|
|
3124
|
+
Object.setPrototypeOf(WrappedWebSocket, NativeWebSocket);
|
|
3125
|
+
globalThis.WebSocket = WrappedWebSocket;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
if (globalThis.__bpTrackedWebSockets) {
|
|
3130
|
+
for (const socket of globalThis.__bpTrackedWebSockets) {
|
|
3131
|
+
if (socket && socket.readyState === 1 && config.offline) {
|
|
3132
|
+
try {
|
|
3133
|
+
socket.close(1012, 'offline');
|
|
3134
|
+
} catch {}
|
|
2496
3135
|
}
|
|
2497
3136
|
}
|
|
2498
3137
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
await deleteSession(session.id);
|
|
2508
|
-
throw new Error(
|
|
2509
|
-
`Session "${session.id}" is no longer valid (browser may have closed).
|
|
2510
|
-
Session file has been cleaned up. Run "bp connect" to create a new session.`
|
|
2511
|
-
);
|
|
3138
|
+
|
|
3139
|
+
if (globalThis.navigator && typeof globalThis.navigator === 'object') {
|
|
3140
|
+
try {
|
|
3141
|
+
Object.defineProperty(globalThis.navigator, 'onLine', {
|
|
3142
|
+
configurable: true,
|
|
3143
|
+
get() { return !config.offline; },
|
|
3144
|
+
});
|
|
3145
|
+
} catch {}
|
|
2512
3146
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
3147
|
+
})();
|
|
3148
|
+
`.trim();
|
|
3149
|
+
}
|
|
3150
|
+
async function applyPermissionState(cdp, origin, granted) {
|
|
3151
|
+
const protocolPermissions = [
|
|
3152
|
+
...new Set(granted.map((value) => toProtocolPermission(value)).filter(Boolean))
|
|
3153
|
+
];
|
|
3154
|
+
if (protocolPermissions.length > 0) {
|
|
3155
|
+
await cdp.send("Browser.grantPermissions", {
|
|
3156
|
+
permissions: protocolPermissions,
|
|
3157
|
+
origin: origin ?? ""
|
|
3158
|
+
});
|
|
2518
3159
|
}
|
|
2519
|
-
|
|
3160
|
+
const script = buildPermissionOverrideScript(granted);
|
|
3161
|
+
await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: script });
|
|
3162
|
+
await cdp.send("Runtime.evaluate", { expression: script, awaitPromise: false });
|
|
3163
|
+
}
|
|
3164
|
+
async function applyVisibilityState(cdp, state) {
|
|
3165
|
+
const script = buildVisibilityOverrideScript(state);
|
|
3166
|
+
await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: script });
|
|
3167
|
+
await cdp.send("Runtime.evaluate", { expression: script, awaitPromise: false });
|
|
3168
|
+
}
|
|
3169
|
+
async function applyNetworkOverride(cdp, state) {
|
|
3170
|
+
const script = buildNetworkOverrideScript(state);
|
|
3171
|
+
await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: script });
|
|
3172
|
+
await cdp.send("Runtime.evaluate", { expression: script, awaitPromise: false });
|
|
2520
3173
|
}
|
|
2521
3174
|
|
|
2522
|
-
// src/cli/commands/
|
|
2523
|
-
var
|
|
2524
|
-
bp
|
|
3175
|
+
// src/cli/commands/env.ts
|
|
3176
|
+
var ENV_HELP = `
|
|
3177
|
+
bp env - Browser/session environment controls
|
|
2525
3178
|
|
|
2526
|
-
|
|
2527
|
-
|
|
3179
|
+
When to use:
|
|
3180
|
+
You need deterministic permission, network, visibility, or geolocation changes without dropping to raw CDP or eval.
|
|
3181
|
+
|
|
3182
|
+
When not to use:
|
|
3183
|
+
You are inspecting or automating DOM interactions. Use \`bp snapshot\`, \`bp exec\`, \`bp record\`, or \`bp trace\`.
|
|
3184
|
+
|
|
3185
|
+
Default flow:
|
|
3186
|
+
change environment -> run exec or audio flow -> inspect with trace summary or watch
|
|
3187
|
+
|
|
3188
|
+
Common mistake:
|
|
3189
|
+
Treating \`env\` as a generic utilities bucket. It is only for browser and session state controls.
|
|
3190
|
+
|
|
3191
|
+
Use this namespace when you need deterministic controls over browser permissions,
|
|
3192
|
+
network, visibility, or geolocation during investigation and automation.
|
|
2528
3193
|
|
|
2529
3194
|
Usage:
|
|
2530
|
-
bp
|
|
2531
|
-
bp
|
|
2532
|
-
|
|
3195
|
+
bp env permissions <action> [permission] [options]
|
|
3196
|
+
bp env network <action> [options]
|
|
3197
|
+
bp env visibility <state> [options]
|
|
3198
|
+
bp env geolocation <action> [options]
|
|
2533
3199
|
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
-
|
|
3200
|
+
Subcommands:
|
|
3201
|
+
permissions grant, revoke, reset, get
|
|
3202
|
+
network offline, online, throttle
|
|
3203
|
+
visibility hidden, visible
|
|
3204
|
+
geolocation set, clear
|
|
3205
|
+
|
|
3206
|
+
Common options:
|
|
3207
|
+
-s, --session <id> Session to use (omit: auto-connect, -s: latest, -s <id>: specific)
|
|
3208
|
+
-h, --help Show help
|
|
2542
3209
|
|
|
2543
3210
|
Examples:
|
|
2544
|
-
|
|
2545
|
-
bp
|
|
2546
|
-
bp
|
|
2547
|
-
|
|
2548
|
-
|
|
3211
|
+
# Browser permissions
|
|
3212
|
+
bp env permissions get -s my-session microphone
|
|
3213
|
+
bp env permissions grant -s my-session microphone
|
|
3214
|
+
bp env permissions reset -s my-session
|
|
3215
|
+
|
|
3216
|
+
# Network control
|
|
3217
|
+
bp env network offline -s my-session
|
|
3218
|
+
bp env network online -s my-session
|
|
3219
|
+
bp env network throttle -s my-session --latency 200 --down 128kbps --up 64kbps
|
|
3220
|
+
|
|
3221
|
+
# Visibility
|
|
3222
|
+
bp env visibility hidden -s my-session
|
|
3223
|
+
bp env visibility visible -s my-session
|
|
3224
|
+
|
|
3225
|
+
# Geolocation
|
|
3226
|
+
bp env geolocation set -s my-session --lat 37.7749 --lon -122.4194
|
|
3227
|
+
bp env geolocation clear -s my-session
|
|
3228
|
+
|
|
3229
|
+
Likely next commands:
|
|
3230
|
+
bp trace watch -s my-session --view ws --assert profile:reconnect
|
|
3231
|
+
bp exec -s my-session '[{"action":"assertPermission","name":"microphone","state":"granted"}]'
|
|
3232
|
+
bp trace summary -s my-session --view permissions
|
|
3233
|
+
`;
|
|
3234
|
+
var PermissionNames;
|
|
3235
|
+
((PermissionNames2) => {
|
|
3236
|
+
PermissionNames2.NAVIGATION = {
|
|
3237
|
+
microphone: "microphone",
|
|
3238
|
+
camera: "camera",
|
|
3239
|
+
notifications: "notifications",
|
|
3240
|
+
geolocation: "geolocation",
|
|
3241
|
+
audio: "audio",
|
|
3242
|
+
audioCapture: "audio-capture",
|
|
3243
|
+
all: "all"
|
|
3244
|
+
};
|
|
3245
|
+
PermissionNames2.PROTOCOL = {
|
|
3246
|
+
microphone: "audioCapture",
|
|
3247
|
+
camera: "videoCapture",
|
|
3248
|
+
notifications: "notifications",
|
|
3249
|
+
geolocation: "geolocation",
|
|
3250
|
+
audio: "audioCapture",
|
|
3251
|
+
audioCapture: "audioCapture",
|
|
3252
|
+
all: "all"
|
|
3253
|
+
};
|
|
3254
|
+
})(PermissionNames || (PermissionNames = {}));
|
|
3255
|
+
function parseEnvArgs(args) {
|
|
2549
3256
|
const options = {};
|
|
2550
|
-
let
|
|
2551
|
-
for (
|
|
3257
|
+
let i = 0;
|
|
3258
|
+
for (; i < args.length; i++) {
|
|
2552
3259
|
const arg = args[i];
|
|
2553
|
-
if (arg === "
|
|
2554
|
-
options.
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
3260
|
+
if (!options.topCommand && (arg === "permissions" || arg === "network" || arg === "visibility" || arg === "geolocation")) {
|
|
3261
|
+
options.topCommand = arg;
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
if (arg === "-h" || arg === "--help") {
|
|
3265
|
+
options.help = true;
|
|
3266
|
+
continue;
|
|
3267
|
+
}
|
|
3268
|
+
if (arg === "-s" || arg === "--session") {
|
|
3269
|
+
const next = args[i + 1];
|
|
3270
|
+
if (next && !next.startsWith("-")) {
|
|
3271
|
+
}
|
|
3272
|
+
const after = args[i + 1];
|
|
3273
|
+
if (!after || after.startsWith("-")) {
|
|
3274
|
+
options.useLatestSession = true;
|
|
3275
|
+
}
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
if (arg === "--lat") {
|
|
3279
|
+
options.lat = Number.parseFloat(args[++i] ?? "0");
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
3282
|
+
if (arg === "--lon") {
|
|
3283
|
+
options.lon = Number.parseFloat(args[++i] ?? "0");
|
|
3284
|
+
continue;
|
|
3285
|
+
}
|
|
3286
|
+
if (arg === "--accuracy" || arg === "--acc") {
|
|
3287
|
+
options.accuracy = Number.parseFloat(args[++i] ?? "1");
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
if (arg === "--duration") {
|
|
3291
|
+
const value = Number.parseInt(args[++i] ?? "0", 10);
|
|
3292
|
+
if (Number.isFinite(value) && value > 0) options.duration = value;
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
if (arg === "--latency") {
|
|
3296
|
+
const value = Number.parseInt(args[++i] ?? "0", 10);
|
|
3297
|
+
if (Number.isFinite(value) && value >= 0) options.latency = value;
|
|
3298
|
+
continue;
|
|
3299
|
+
}
|
|
3300
|
+
if (arg === "--down") {
|
|
3301
|
+
options.down = args[++i];
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
if (arg === "--up") {
|
|
3305
|
+
options.up = args[++i];
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
if (!arg.startsWith("-") && options.topCommand) {
|
|
3309
|
+
if (options.topCommand === "permissions") {
|
|
3310
|
+
if (!options.permissionMode) {
|
|
3311
|
+
options.permissionMode = arg;
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
3314
|
+
if (!options.permissionName && options.permissionMode !== "get" && options.permissionMode !== "reset") {
|
|
3315
|
+
options.permissionName = arg;
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
if (!options.permissionName && options.permissionMode === "get") {
|
|
3319
|
+
options.permissionName = arg;
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
if (options.topCommand === "network") {
|
|
3324
|
+
options.networkAction = arg;
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
if (options.topCommand === "visibility") {
|
|
3328
|
+
options.visibility = arg;
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
if (options.topCommand === "geolocation") {
|
|
3332
|
+
options.geoAction = arg;
|
|
3333
|
+
}
|
|
2559
3334
|
}
|
|
2560
3335
|
}
|
|
2561
|
-
return
|
|
2562
|
-
}
|
|
2563
|
-
function normalizeEvalExpression(expression, wrap = false) {
|
|
2564
|
-
const trimmed = expression.trim();
|
|
2565
|
-
const needsWrap = wrap || trimmed.includes("=>") || /\bawait\b/.test(trimmed);
|
|
2566
|
-
if (!needsWrap) {
|
|
2567
|
-
return trimmed;
|
|
2568
|
-
}
|
|
2569
|
-
if (wrap || /\bawait\b/.test(trimmed)) {
|
|
2570
|
-
return `(async () => (${trimmed}))()`;
|
|
2571
|
-
}
|
|
2572
|
-
return `(() => (${trimmed}))()`;
|
|
3336
|
+
return options;
|
|
2573
3337
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2581
|
-
if (
|
|
2582
|
-
|
|
2583
|
-
|
|
3338
|
+
function coercePermissionArg(value) {
|
|
3339
|
+
return value;
|
|
3340
|
+
}
|
|
3341
|
+
function toBytesPerSecond(raw) {
|
|
3342
|
+
if (!raw) return void 0;
|
|
3343
|
+
const text = raw.trim().toLowerCase();
|
|
3344
|
+
const match = text.match(/^([0-9]*\.?[0-9]+)\s*(kbps|mbps|k|m)?$/);
|
|
3345
|
+
if (!match || !match[1]) return void 0;
|
|
3346
|
+
const value = Number.parseFloat(match[1]);
|
|
3347
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
3348
|
+
const unit = match[2] ?? "kbps";
|
|
3349
|
+
if (unit === "mbps" || unit === "m") return Math.round(value * 1e6 / 8);
|
|
3350
|
+
return Math.round(value * 1e3 / 8);
|
|
3351
|
+
}
|
|
3352
|
+
async function resolveConnection2(sessionId, useLatestSession = false) {
|
|
3353
|
+
if (sessionId) {
|
|
3354
|
+
const session2 = await loadSession(sessionId);
|
|
3355
|
+
const browser2 = await connect({ provider: session2.provider, wsUrl: session2.wsUrl });
|
|
3356
|
+
return { browser: browser2, session: session2 };
|
|
2584
3357
|
}
|
|
2585
|
-
if (
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
|
|
3358
|
+
if (useLatestSession) {
|
|
3359
|
+
const defaultSession = await getDefaultSession();
|
|
3360
|
+
if (!defaultSession) {
|
|
3361
|
+
throw new Error('No sessions found. Run "bp connect" first or use "-s" for latest session.');
|
|
2589
3362
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
);
|
|
3363
|
+
const browser2 = await connect({
|
|
3364
|
+
provider: defaultSession.provider,
|
|
3365
|
+
wsUrl: defaultSession.wsUrl
|
|
3366
|
+
});
|
|
3367
|
+
return { browser: browser2, session: defaultSession };
|
|
2596
3368
|
}
|
|
2597
|
-
|
|
2598
|
-
const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
|
|
3369
|
+
let wsUrl;
|
|
2599
3370
|
try {
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
}
|
|
2609
|
-
output(
|
|
2610
|
-
globalOptions.format === "json" ? { success: true, result: stepResult.result } : stepResult.result,
|
|
2611
|
-
globalOptions.format
|
|
3371
|
+
wsUrl = (await resolveCLIEndpoint()).wsUrl;
|
|
3372
|
+
} catch (error) {
|
|
3373
|
+
throw new Error(
|
|
3374
|
+
formatBrowserDiscoveryError(error, {
|
|
3375
|
+
explicitHint: " - Create a session first: bp connect --browser-url <ws-url>",
|
|
3376
|
+
reuseSessionHint: "bp env -s <id> ...",
|
|
3377
|
+
latestSessionHint: "bp env -s"
|
|
3378
|
+
})
|
|
2612
3379
|
);
|
|
2613
|
-
await updateSession(session.id, { currentUrl: await page.url() });
|
|
2614
|
-
} finally {
|
|
2615
|
-
await browser.disconnect();
|
|
2616
3380
|
}
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
3381
|
+
const browser = await connect({ provider: "generic", wsUrl });
|
|
3382
|
+
const page = await browser.page();
|
|
3383
|
+
const currentUrl = await page.url();
|
|
3384
|
+
const newSessionId = generateSessionId();
|
|
3385
|
+
const session = {
|
|
3386
|
+
id: newSessionId,
|
|
3387
|
+
provider: "generic",
|
|
3388
|
+
wsUrl: browser.wsUrl,
|
|
3389
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3390
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3391
|
+
currentUrl
|
|
3392
|
+
};
|
|
3393
|
+
await saveSession(session);
|
|
3394
|
+
const sessionFile = getSessionFilePath(newSessionId);
|
|
3395
|
+
await import("fs/promises").then(
|
|
3396
|
+
(fs9) => fs9.mkdir(dirname5(sessionFile), { recursive: true })
|
|
3397
|
+
);
|
|
3398
|
+
return { browser, session };
|
|
3399
|
+
}
|
|
3400
|
+
function clampRate(value) {
|
|
3401
|
+
if (value === void 0) return void 0;
|
|
3402
|
+
if (!Number.isFinite(value) || value < 0) return void 0;
|
|
3403
|
+
return Math.round(value);
|
|
3404
|
+
}
|
|
3405
|
+
function isStoredPermissionName(value) {
|
|
3406
|
+
return value !== null;
|
|
3407
|
+
}
|
|
3408
|
+
async function getPermissionStates(page) {
|
|
3409
|
+
const expr = `
|
|
3410
|
+
(() => {
|
|
3411
|
+
const names = ['geolocation', 'microphone', 'audio-capture', 'camera', 'notifications', 'clipboard-read', 'clipboard-write'];
|
|
3412
|
+
return Promise.all(names.map(async (name) => {
|
|
3413
|
+
if (!navigator.permissions || !navigator.permissions.query) {
|
|
3414
|
+
return { name, state: 'unsupported' };
|
|
3415
|
+
}
|
|
3416
|
+
try {
|
|
3417
|
+
const result = await navigator.permissions.query({ name });
|
|
3418
|
+
return { name, state: result.state };
|
|
3419
|
+
} catch {
|
|
3420
|
+
return { name, state: 'unsupported' };
|
|
3421
|
+
}
|
|
3422
|
+
}));
|
|
3423
|
+
})()
|
|
3424
|
+
`;
|
|
3425
|
+
return page.evaluate(expr);
|
|
3426
|
+
}
|
|
3427
|
+
async function permissionCommand(action, nameInput, page) {
|
|
3428
|
+
const requested = nameInput && nameInput !== "all" ? coercePermissionArg(nameInput) : "all";
|
|
3429
|
+
if (action === "get") {
|
|
3430
|
+
const states = await getPermissionStates(page);
|
|
3431
|
+
if (requested !== "all") {
|
|
3432
|
+
const lower = String(requested).toLowerCase();
|
|
3433
|
+
return states.filter(
|
|
3434
|
+
(item) => item.name === lower || item.name === PermissionNames.NAVIGATION[requested]
|
|
3435
|
+
).map((item) => ({ action, name: item.name, state: item.state }));
|
|
3436
|
+
}
|
|
3437
|
+
return states.map((item) => ({ action, name: item.name, state: item.state }));
|
|
3438
|
+
}
|
|
3439
|
+
const permissionNames = requested === "all" ? Object.values(PermissionNames.NAVIGATION).filter((v) => v !== "all") : [PermissionNames.NAVIGATION[requested] ?? String(requested)];
|
|
3440
|
+
const protocolNames = requested === "all" ? ["geolocation", "audioCapture", "videoCapture", "notifications"] : permissionNames.map(
|
|
3441
|
+
(item) => PermissionNames.PROTOCOL[item] ?? String(item)
|
|
3442
|
+
);
|
|
3443
|
+
if (action === "grant") {
|
|
3444
|
+
const origin2 = await page.evaluate("window.location.origin");
|
|
3445
|
+
await page.cdpClient.send("Browser.grantPermissions", {
|
|
3446
|
+
permissions: protocolNames.filter((value) => value !== "all" && value !== "audio"),
|
|
3447
|
+
origin: origin2
|
|
3448
|
+
});
|
|
3449
|
+
if (permissionNames.includes("microphone")) {
|
|
3450
|
+
await grantAudioPermissions(page.cdpClient, origin2);
|
|
2647
3451
|
}
|
|
3452
|
+
const result = await getPermissionStates(page);
|
|
3453
|
+
return result.map((item) => ({ action, name: item.name, state: item.state }));
|
|
2648
3454
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
const fullEntry = {
|
|
2654
|
-
seq: ++this.seq,
|
|
2655
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2656
|
-
...entry
|
|
2657
|
-
};
|
|
2658
|
-
const line = `${JSON.stringify(fullEntry)}
|
|
2659
|
-
`;
|
|
2660
|
-
fs5.appendFileSync(this.logPath, line, "utf-8");
|
|
2661
|
-
if (this.exportLogPath) {
|
|
3455
|
+
const origin = await page.evaluate("window.location.origin");
|
|
3456
|
+
if (action === "revoke" || action === "reset") {
|
|
3457
|
+
for (const permission of protocolNames) {
|
|
3458
|
+
if (permission === "all") continue;
|
|
2662
3459
|
try {
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
3460
|
+
await page.cdpClient.send("Browser.resetPermissions", {
|
|
3461
|
+
permissions: [permission],
|
|
3462
|
+
origin
|
|
3463
|
+
});
|
|
3464
|
+
} catch {
|
|
3465
|
+
await page.cdpClient.send("Browser.revokePermissions", {
|
|
3466
|
+
permissions: [permission],
|
|
3467
|
+
origin
|
|
3468
|
+
});
|
|
2666
3469
|
}
|
|
2667
3470
|
}
|
|
3471
|
+
const result = await getPermissionStates(page);
|
|
3472
|
+
return result.map((item) => ({ action, name: item.name, state: item.state }));
|
|
2668
3473
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
3474
|
+
throw new Error(`Unsupported permission action: ${action}`);
|
|
3475
|
+
}
|
|
3476
|
+
function formatPermissionOutput(session, data) {
|
|
3477
|
+
const lines = [`Session: ${session.id}`, ""];
|
|
3478
|
+
for (const row of data) {
|
|
3479
|
+
const state = typeof row.state === "string" ? row.state : row.state === void 0 || row.state === null ? "unknown" : JSON.stringify(row.state);
|
|
3480
|
+
lines.push(`${row.name}: ${state} (${row.action})`);
|
|
2674
3481
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3482
|
+
return lines.join("\n");
|
|
3483
|
+
}
|
|
3484
|
+
async function runNetworkCommand(action, options, page, session) {
|
|
3485
|
+
await page.cdpClient.send("Network.enable");
|
|
3486
|
+
const applyNetworkState = async (offline, latency2, downloadThroughput, uploadThroughput, connectionType) => {
|
|
3487
|
+
const state = {
|
|
3488
|
+
offline,
|
|
3489
|
+
latency: latency2,
|
|
3490
|
+
downloadThroughput,
|
|
3491
|
+
uploadThroughput,
|
|
3492
|
+
connectionType
|
|
3493
|
+
};
|
|
3494
|
+
try {
|
|
3495
|
+
await page.cdpClient.send("Network.emulateNetworkConditionsByRule", {
|
|
3496
|
+
offline,
|
|
3497
|
+
matchedNetworkConditions: [
|
|
3498
|
+
{
|
|
3499
|
+
urlPattern: "",
|
|
3500
|
+
latency: latency2,
|
|
3501
|
+
downloadThroughput,
|
|
3502
|
+
uploadThroughput
|
|
3503
|
+
}
|
|
3504
|
+
]
|
|
3505
|
+
});
|
|
3506
|
+
await page.cdpClient.send("Network.overrideNetworkState", state);
|
|
3507
|
+
} catch {
|
|
3508
|
+
await page.cdpClient.send("Network.emulateNetworkConditions", state);
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
if (action === "offline") {
|
|
3512
|
+
await applyNetworkState(true, options.latency ?? 0, 0, 0, "none");
|
|
3513
|
+
await applyNetworkOverride(page.cdpClient, {
|
|
3514
|
+
offline: true,
|
|
3515
|
+
latency: options.latency ?? 0
|
|
2688
3516
|
});
|
|
3517
|
+
console.log(`Session ${session.id}: network set to offline`);
|
|
3518
|
+
return;
|
|
2689
3519
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
type: "error",
|
|
2696
|
-
error: error.message,
|
|
2697
|
-
args: context
|
|
3520
|
+
if (action === "online") {
|
|
3521
|
+
await applyNetworkState(false, 0, -1, -1, "wifi");
|
|
3522
|
+
await applyNetworkOverride(page.cdpClient, {
|
|
3523
|
+
offline: false,
|
|
3524
|
+
latency: 0
|
|
2698
3525
|
});
|
|
3526
|
+
console.log(`Session ${session.id}: network set to online`);
|
|
3527
|
+
return;
|
|
2699
3528
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
3529
|
+
const latency = clampRate(options.latency) ?? 0;
|
|
3530
|
+
const down = clampRate(toBytesPerSecond(options.down)) ?? 1e6;
|
|
3531
|
+
const up = clampRate(toBytesPerSecond(options.up)) ?? 5e5;
|
|
3532
|
+
await applyNetworkState(false, latency, down, up, "wifi");
|
|
3533
|
+
await applyNetworkOverride(page.cdpClient, {
|
|
3534
|
+
offline: false,
|
|
3535
|
+
latency
|
|
3536
|
+
});
|
|
3537
|
+
console.log(
|
|
3538
|
+
`Session ${session.id}: network throttled | latency=${latency}ms down=${down}B/s up=${up}B/s`
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
async function runVisibilityCommand(state, page, session) {
|
|
3542
|
+
await applyVisibilityState(page.cdpClient, state);
|
|
3543
|
+
console.log(`Session ${session.id}: visibility set to ${state}`);
|
|
3544
|
+
}
|
|
3545
|
+
async function runGeolocationCommand(action, options, page, session) {
|
|
3546
|
+
if (action === "clear") {
|
|
3547
|
+
await page.clearGeolocation();
|
|
3548
|
+
console.log(`Session ${session.id}: geolocation override cleared`);
|
|
3549
|
+
return;
|
|
2705
3550
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
*/
|
|
2709
|
-
getLogStats() {
|
|
2710
|
-
if (!fs5.existsSync(this.logPath)) {
|
|
2711
|
-
return { entries: 0, size: 0 };
|
|
2712
|
-
}
|
|
2713
|
-
const stat = fs5.statSync(this.logPath);
|
|
2714
|
-
const entries = this.countEntries();
|
|
2715
|
-
let first;
|
|
2716
|
-
let last;
|
|
2717
|
-
if (entries > 0) {
|
|
2718
|
-
const lines = fs5.readFileSync(this.logPath, "utf-8").trim().split("\n");
|
|
2719
|
-
const firstEntry = this.parseLine(lines[0]);
|
|
2720
|
-
const lastEntry = this.parseLine(lines[lines.length - 1]);
|
|
2721
|
-
first = firstEntry?.ts;
|
|
2722
|
-
last = lastEntry?.ts;
|
|
2723
|
-
}
|
|
2724
|
-
return {
|
|
2725
|
-
entries,
|
|
2726
|
-
size: stat.size,
|
|
2727
|
-
first,
|
|
2728
|
-
last
|
|
2729
|
-
};
|
|
3551
|
+
if (options.lat === void 0 || options.lon === void 0) {
|
|
3552
|
+
throw new Error("geolocation set requires --lat and --lon");
|
|
2730
3553
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
3554
|
+
await page.setGeolocation({
|
|
3555
|
+
latitude: options.lat,
|
|
3556
|
+
longitude: options.lon,
|
|
3557
|
+
accuracy: options.accuracy ?? 1
|
|
3558
|
+
});
|
|
3559
|
+
console.log(
|
|
3560
|
+
`Session ${session.id}: geolocation set to ${options.lat}, ${options.lon} (accuracy ${options.accuracy ?? 1})`
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
async function envCommand(args, globalOptions) {
|
|
3564
|
+
const options = parseEnvArgs(args);
|
|
3565
|
+
if (options.help || globalOptions.help || !options.topCommand) {
|
|
3566
|
+
console.log(ENV_HELP);
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
const { browser, session } = await resolveConnection2(
|
|
3570
|
+
globalOptions.session,
|
|
3571
|
+
options.useLatestSession ?? false
|
|
3572
|
+
);
|
|
3573
|
+
const page = await browser.page(void 0, { targetId: session.targetId });
|
|
3574
|
+
const outputAsJson = globalOptions.format === "json";
|
|
3575
|
+
const existingEnv = session.metadata?.env ?? {};
|
|
3576
|
+
try {
|
|
3577
|
+
if (options.topCommand === "permissions") {
|
|
3578
|
+
const permissionMode = options.permissionMode ?? "get";
|
|
3579
|
+
if (permissionMode === "get" && !options.permissionName) {
|
|
3580
|
+
const result2 = await permissionCommand(permissionMode, "all", page);
|
|
3581
|
+
if (outputAsJson) {
|
|
3582
|
+
console.log(JSON.stringify({ session: session.id, permissions: result2 }, null, 2));
|
|
3583
|
+
} else {
|
|
3584
|
+
console.log(formatPermissionOutput(session, result2));
|
|
3585
|
+
}
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
const result = await permissionCommand(permissionMode, options.permissionName, page);
|
|
3589
|
+
if (permissionMode !== "get") {
|
|
3590
|
+
const nextPermissions = permissionMode === "reset" ? [] : (() => {
|
|
3591
|
+
const current = new Set(
|
|
3592
|
+
(existingEnv.permissions ?? []).map((value) => normalizeStoredPermission(value)).filter(isStoredPermissionName)
|
|
3593
|
+
);
|
|
3594
|
+
const requested = options.permissionName === "all" || !options.permissionName ? ["microphone", "camera", "notifications", "geolocation"] : [normalizeStoredPermission(options.permissionName)].filter(
|
|
3595
|
+
isStoredPermissionName
|
|
3596
|
+
);
|
|
3597
|
+
if (permissionMode === "grant") {
|
|
3598
|
+
for (const name of requested) current.add(name);
|
|
3599
|
+
} else {
|
|
3600
|
+
for (const name of requested) current.delete(name);
|
|
3601
|
+
}
|
|
3602
|
+
return [...current];
|
|
3603
|
+
})();
|
|
3604
|
+
const nextEnv = {
|
|
3605
|
+
...existingEnv,
|
|
3606
|
+
permissions: nextPermissions
|
|
3607
|
+
};
|
|
3608
|
+
await updateSession(session.id, { metadata: { env: nextEnv } });
|
|
3609
|
+
const currentUrl = await page.evaluate("window.location.href");
|
|
3610
|
+
await applyPermissionState(page.cdpClient, originFromUrl(currentUrl), nextPermissions);
|
|
3611
|
+
}
|
|
3612
|
+
if (outputAsJson) {
|
|
3613
|
+
console.log(
|
|
3614
|
+
JSON.stringify(
|
|
3615
|
+
{ session: session.id, action: permissionMode, permissions: result },
|
|
3616
|
+
null,
|
|
3617
|
+
2
|
|
3618
|
+
)
|
|
3619
|
+
);
|
|
3620
|
+
} else {
|
|
3621
|
+
console.log(formatPermissionOutput(session, result));
|
|
3622
|
+
}
|
|
3623
|
+
return;
|
|
2741
3624
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
3625
|
+
if (options.topCommand === "network") {
|
|
3626
|
+
const action = options.networkAction;
|
|
3627
|
+
if (!action) {
|
|
3628
|
+
throw new Error("network command requires action: offline, online, or throttle");
|
|
3629
|
+
}
|
|
3630
|
+
await runNetworkCommand(action, options, page, session);
|
|
3631
|
+
await updateSession(session.id, {
|
|
3632
|
+
metadata: {
|
|
3633
|
+
env: {
|
|
3634
|
+
...existingEnv,
|
|
3635
|
+
network: action === "online" ? {
|
|
3636
|
+
offline: false,
|
|
3637
|
+
latency: 0
|
|
3638
|
+
} : {
|
|
3639
|
+
offline: action === "offline",
|
|
3640
|
+
latency: action === "throttle" ? options.latency ?? 0 : options.latency ?? 0
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
});
|
|
3645
|
+
if (options.duration && options.duration > 0) {
|
|
3646
|
+
await new Promise((resolve8) => setTimeout(resolve8, options.duration));
|
|
3647
|
+
if (action === "offline" || action === "throttle") {
|
|
3648
|
+
await runNetworkCommand("online", {}, page, session);
|
|
3649
|
+
await updateSession(session.id, {
|
|
3650
|
+
metadata: {
|
|
3651
|
+
env: {
|
|
3652
|
+
...existingEnv,
|
|
3653
|
+
network: {
|
|
3654
|
+
offline: false,
|
|
3655
|
+
latency: 0
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
2749
3661
|
}
|
|
3662
|
+
return;
|
|
2750
3663
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3664
|
+
if (options.topCommand === "visibility") {
|
|
3665
|
+
if (!options.visibility) {
|
|
3666
|
+
throw new Error("visibility command requires: hidden or visible");
|
|
3667
|
+
}
|
|
3668
|
+
await runVisibilityCommand(options.visibility, page, session);
|
|
3669
|
+
await updateSession(session.id, {
|
|
3670
|
+
metadata: {
|
|
3671
|
+
env: {
|
|
3672
|
+
...existingEnv,
|
|
3673
|
+
visibility: options.visibility
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
return;
|
|
2759
3678
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
3679
|
+
if (options.topCommand === "geolocation") {
|
|
3680
|
+
if (!options.geoAction) {
|
|
3681
|
+
throw new Error("geolocation command requires: set or clear");
|
|
3682
|
+
}
|
|
3683
|
+
await runGeolocationCommand(options.geoAction, options, page, session);
|
|
3684
|
+
await updateSession(session.id, {
|
|
3685
|
+
metadata: {
|
|
3686
|
+
env: {
|
|
3687
|
+
...existingEnv,
|
|
3688
|
+
geolocation: options.geoAction === "clear" ? void 0 : {
|
|
3689
|
+
latitude: options.lat,
|
|
3690
|
+
longitude: options.lon,
|
|
3691
|
+
accuracy: options.accuracy ?? 1
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
return;
|
|
2763
3697
|
}
|
|
2764
|
-
|
|
3698
|
+
throw new Error("Unknown env command. Run bp env --help for usage.");
|
|
3699
|
+
} finally {
|
|
3700
|
+
await browser.disconnect();
|
|
2765
3701
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// src/cli/attach.ts
|
|
3705
|
+
async function applySessionEnvironment(page, currentUrl, settings) {
|
|
3706
|
+
if (!settings) {
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
const origin = originFromUrl(currentUrl);
|
|
3710
|
+
if (Array.isArray(settings.permissions)) {
|
|
3711
|
+
await applyPermissionState(page.cdpClient, origin, settings.permissions);
|
|
3712
|
+
}
|
|
3713
|
+
if (settings.geolocation) {
|
|
3714
|
+
await page.setGeolocation(settings.geolocation);
|
|
3715
|
+
}
|
|
3716
|
+
if (settings.visibility) {
|
|
3717
|
+
await applyVisibilityState(page.cdpClient, settings.visibility);
|
|
3718
|
+
}
|
|
3719
|
+
if (settings.network) {
|
|
3720
|
+
await applyNetworkOverride(page.cdpClient, settings.network);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
async function resolveSession(sessionId) {
|
|
3724
|
+
if (sessionId) {
|
|
3725
|
+
return loadSession(sessionId);
|
|
3726
|
+
}
|
|
3727
|
+
const session = await getDefaultSession();
|
|
3728
|
+
if (!session) {
|
|
3729
|
+
throw new Error('No session found. Run "bp connect" first.');
|
|
3730
|
+
}
|
|
3731
|
+
return session;
|
|
3732
|
+
}
|
|
3733
|
+
function isDaemonHealthy(session) {
|
|
3734
|
+
if (!session.daemon) return false;
|
|
3735
|
+
const daemonAge = Date.now() - new Date(session.daemon.startedAt).getTime();
|
|
3736
|
+
if (daemonAge > DAEMON_MAX_AGE_MS) {
|
|
3737
|
+
return false;
|
|
3738
|
+
}
|
|
3739
|
+
if (session.daemon.lastHeartbeat) {
|
|
3740
|
+
const heartbeatAge = Date.now() - new Date(session.daemon.lastHeartbeat).getTime();
|
|
3741
|
+
if (heartbeatAge > 9e4) {
|
|
3742
|
+
return false;
|
|
2772
3743
|
}
|
|
3744
|
+
}
|
|
3745
|
+
return isDaemonAlive(session.daemon.pid);
|
|
3746
|
+
}
|
|
3747
|
+
async function cleanupStaleDaemon(session, reason) {
|
|
3748
|
+
console.warn(`[browser-pilot] Daemon unavailable (${reason}), falling back to direct WebSocket`);
|
|
3749
|
+
const sessionFilePath = getSessionFilePath(session.id);
|
|
3750
|
+
await clearDaemonFromSession(sessionFilePath);
|
|
3751
|
+
if (session.daemon?.socketPath) {
|
|
2773
3752
|
try {
|
|
2774
|
-
|
|
3753
|
+
const fsPromises = await import("fs/promises");
|
|
3754
|
+
await fsPromises.unlink(session.daemon.socketPath).catch(() => {
|
|
3755
|
+
});
|
|
2775
3756
|
} catch {
|
|
2776
|
-
return null;
|
|
2777
3757
|
}
|
|
2778
3758
|
}
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
3759
|
+
}
|
|
3760
|
+
async function attachSession(session, options = {}) {
|
|
3761
|
+
if (session.daemon) {
|
|
3762
|
+
if (!isDaemonHealthy(session)) {
|
|
3763
|
+
const reason = !isDaemonAlive(session.daemon.pid) ? "PID not alive" : "daemon expired (>60min)";
|
|
3764
|
+
await cleanupStaleDaemon(session, reason);
|
|
3765
|
+
} else {
|
|
3766
|
+
try {
|
|
3767
|
+
const { createDaemonTransport } = await import("./transport-WHEBAZUP.mjs");
|
|
3768
|
+
const { createCDPClientFromTransport } = await import("./client-JWWZWO6L.mjs");
|
|
3769
|
+
const transport = await createDaemonTransport(session.daemon.socketPath);
|
|
3770
|
+
const cdp = createCDPClientFromTransport(transport, {
|
|
3771
|
+
debug: options.trace
|
|
3772
|
+
});
|
|
3773
|
+
const { Browser: BrowserClass } = await import("./browser-ZCR6AA4D.mjs");
|
|
3774
|
+
const { Page: PageClass } = await import("./page-IUUTJ3SW.mjs");
|
|
3775
|
+
const browser2 = BrowserClass.fromCDP(cdp, session);
|
|
3776
|
+
const page2 = session.daemon.cdpSessionId && session.targetId ? addBatchToPage(
|
|
3777
|
+
await (async () => {
|
|
3778
|
+
cdp.setSessionId(session.daemon?.cdpSessionId);
|
|
3779
|
+
const attachedPage = new PageClass(cdp, session.targetId);
|
|
3780
|
+
await attachedPage.init();
|
|
3781
|
+
return attachedPage;
|
|
3782
|
+
})()
|
|
3783
|
+
) : addBatchToPage(await browser2.page(void 0, { targetId: session.targetId }));
|
|
3784
|
+
const currentUrl2 = await page2.url();
|
|
3785
|
+
await applySessionEnvironment(page2, currentUrl2, session.metadata?.env);
|
|
3786
|
+
const refCache2 = session.metadata?.refCache;
|
|
3787
|
+
if (refCache2 && refCache2.url === currentUrl2) {
|
|
3788
|
+
page2.importRefMap(refCache2.refMap);
|
|
3789
|
+
}
|
|
3790
|
+
return { session, browser: browser2, page: page2, viaDaemon: true };
|
|
3791
|
+
} catch (err) {
|
|
3792
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3793
|
+
await cleanupStaleDaemon(session, reason);
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
let browser;
|
|
3798
|
+
try {
|
|
3799
|
+
browser = await connect({
|
|
3800
|
+
provider: session.provider,
|
|
3801
|
+
wsUrl: session.wsUrl,
|
|
3802
|
+
debug: options.trace
|
|
3803
|
+
});
|
|
3804
|
+
} catch {
|
|
3805
|
+
await deleteSession(session.id);
|
|
3806
|
+
throw new Error(
|
|
3807
|
+
`Session "${session.id}" is no longer valid (browser may have closed).
|
|
3808
|
+
Session file has been cleaned up. Run "bp connect" to create a new session.`
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
const page = addBatchToPage(await browser.page(void 0, { targetId: session.targetId }));
|
|
3812
|
+
const currentUrl = await page.url();
|
|
3813
|
+
await applySessionEnvironment(page, currentUrl, session.metadata?.env);
|
|
3814
|
+
const refCache = session.metadata?.refCache;
|
|
3815
|
+
if (refCache && refCache.url === currentUrl) {
|
|
3816
|
+
page.importRefMap(refCache.refMap);
|
|
3817
|
+
}
|
|
3818
|
+
return { session, browser, page, viaDaemon: false };
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
// src/cli/commands/eval.ts
|
|
3822
|
+
var EVAL_HELP = `
|
|
3823
|
+
bp eval - Evaluate JavaScript in the browser
|
|
3824
|
+
|
|
3825
|
+
Convenience wrapper around exec's evaluate action.
|
|
3826
|
+
No JSON escaping needed -- just pass a JS expression directly.
|
|
3827
|
+
|
|
3828
|
+
Usage:
|
|
3829
|
+
bp eval '<expression>' Evaluate inline JavaScript
|
|
3830
|
+
bp eval -f <file> Evaluate JavaScript from a file
|
|
3831
|
+
echo '<expr>' | bp eval Evaluate from stdin
|
|
3832
|
+
|
|
3833
|
+
Options:
|
|
3834
|
+
-f, --file <path> Read JavaScript from a file
|
|
3835
|
+
--wrap Wrap the expression in an async IIFE
|
|
3836
|
+
-s, --session <id> Session to use (default: most recent)
|
|
3837
|
+
-f, --format <fmt> Output format: json | pretty (default: pretty)
|
|
3838
|
+
--json Alias for -f json
|
|
3839
|
+
--trace Enable debug tracing
|
|
3840
|
+
-h, --help Show this help
|
|
3841
|
+
|
|
3842
|
+
Examples:
|
|
3843
|
+
bp eval 'document.title'
|
|
3844
|
+
bp eval 'document.querySelectorAll("a").length'
|
|
3845
|
+
bp eval -f scrape.js
|
|
3846
|
+
`.trimEnd();
|
|
3847
|
+
function parseEvalArgs(args) {
|
|
3848
|
+
const options = {};
|
|
3849
|
+
let expression;
|
|
3850
|
+
for (let i = 0; i < args.length; i++) {
|
|
3851
|
+
const arg = args[i];
|
|
3852
|
+
if (arg === "-f" || arg === "--file") {
|
|
3853
|
+
options.file = args[++i];
|
|
3854
|
+
} else if (arg === "--wrap") {
|
|
3855
|
+
options.wrap = true;
|
|
3856
|
+
} else if (!expression && !arg.startsWith("-")) {
|
|
3857
|
+
expression = arg;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
return { expression, options };
|
|
3861
|
+
}
|
|
3862
|
+
function normalizeEvalExpression(expression, wrap = false) {
|
|
3863
|
+
const trimmed = expression.trim();
|
|
3864
|
+
const needsWrap = wrap || trimmed.includes("=>") || /\bawait\b/.test(trimmed);
|
|
3865
|
+
if (!needsWrap) {
|
|
3866
|
+
return trimmed;
|
|
3867
|
+
}
|
|
3868
|
+
if (wrap || /\bawait\b/.test(trimmed)) {
|
|
3869
|
+
return `(async () => (${trimmed}))()`;
|
|
3870
|
+
}
|
|
3871
|
+
return `(() => (${trimmed}))()`;
|
|
3872
|
+
}
|
|
3873
|
+
async function evalCommand(args, globalOptions) {
|
|
3874
|
+
if (globalOptions.help) {
|
|
3875
|
+
console.log(EVAL_HELP);
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
const { expression: argExpression, options: evalOptions } = parseEvalArgs(args);
|
|
3879
|
+
let expression = argExpression;
|
|
3880
|
+
if (evalOptions.file) {
|
|
3881
|
+
const fs9 = await import("fs/promises");
|
|
3882
|
+
expression = await fs9.readFile(evalOptions.file, "utf-8");
|
|
3883
|
+
}
|
|
3884
|
+
if (!expression && !process.stdin.isTTY) {
|
|
3885
|
+
const chunks = [];
|
|
3886
|
+
for await (const chunk of process.stdin) {
|
|
3887
|
+
chunks.push(chunk);
|
|
3888
|
+
}
|
|
3889
|
+
expression = Buffer.concat(chunks).toString("utf-8").trim();
|
|
3890
|
+
}
|
|
3891
|
+
if (!expression) {
|
|
3892
|
+
throw new Error(
|
|
3893
|
+
"No expression provided.\n\nUsage:\n bp eval 'document.title'\n bp eval -f script.js\n echo 'document.title' | bp eval"
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
const session = await resolveSession(globalOptions.session);
|
|
3897
|
+
const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
|
|
3898
|
+
try {
|
|
3899
|
+
const step = {
|
|
3900
|
+
action: "evaluate",
|
|
3901
|
+
value: normalizeEvalExpression(expression, evalOptions.wrap)
|
|
3902
|
+
};
|
|
3903
|
+
const result = await page.batch([step]);
|
|
3904
|
+
const stepResult = result.steps[0];
|
|
3905
|
+
if (!stepResult.success) {
|
|
3906
|
+
throw new Error(stepResult.error ?? "Evaluation failed");
|
|
3907
|
+
}
|
|
3908
|
+
output(
|
|
3909
|
+
globalOptions.format === "json" ? { success: true, result: stepResult.result } : stepResult.result,
|
|
3910
|
+
globalOptions.format
|
|
3911
|
+
);
|
|
3912
|
+
await updateSession(session.id, { currentUrl: await page.url() });
|
|
3913
|
+
} finally {
|
|
3914
|
+
await browser.disconnect();
|
|
2787
3915
|
}
|
|
2788
|
-
return logger;
|
|
2789
3916
|
}
|
|
2790
3917
|
|
|
2791
3918
|
// src/cli/commands/exec.ts
|
|
3919
|
+
import * as nodeFs from "fs";
|
|
3920
|
+
import { basename as basename2, dirname as dirname6, join as join8, resolve as resolve4 } from "path";
|
|
2792
3921
|
var EXEC_HELP = `
|
|
2793
|
-
bp exec -
|
|
3922
|
+
bp exec - Act on a page with high-level browser steps
|
|
3923
|
+
|
|
3924
|
+
When to use:
|
|
3925
|
+
You know what you want to do in the browser and want to click, fill, wait, or assert in one batch.
|
|
3926
|
+
|
|
3927
|
+
When not to use:
|
|
3928
|
+
A human is demonstrating the workflow from scratch. Use \`bp record\`.
|
|
3929
|
+
|
|
3930
|
+
Default flow:
|
|
3931
|
+
snapshot -> exec -> snapshot or trace summary
|
|
3932
|
+
|
|
3933
|
+
Common mistake:
|
|
3934
|
+
Guessing brittle selectors instead of taking \`bp snapshot -i\` and using refs.
|
|
2794
3935
|
|
|
2795
3936
|
Usage:
|
|
2796
3937
|
bp exec '<json>' Execute action(s) from inline JSON
|
|
@@ -2804,7 +3945,7 @@ Options:
|
|
|
2804
3945
|
-s, --session <id> Session to use (default: most recent)
|
|
2805
3946
|
-f, --format <fmt> Output format: json | pretty (default: pretty)
|
|
2806
3947
|
--json Alias for -f json
|
|
2807
|
-
--
|
|
3948
|
+
--debug Enable CDP transport debugging (global option)
|
|
2808
3949
|
|
|
2809
3950
|
Recording:
|
|
2810
3951
|
--record Enable screenshot recording
|
|
@@ -2818,12 +3959,16 @@ Recording:
|
|
|
2818
3959
|
|
|
2819
3960
|
Examples:
|
|
2820
3961
|
bp exec '{"action":"goto","url":"https://example.com"}'
|
|
3962
|
+
bp exec '[{"action":"click","selector":"ref:e4"},{"action":"assertText","expect":"Saved"}]'
|
|
3963
|
+
bp exec '[{"action":"waitForWsMessage","match":"*realtime*","where":{"type":"ready"}}]'
|
|
2821
3964
|
bp exec --record '[{"action":"fill","selector":"#email","value":"me@test.com"},{"action":"submit","selector":"form"}]'
|
|
2822
3965
|
bp exec --dialog accept '{"action":"click","selector":"#delete-btn"}'
|
|
2823
3966
|
bp exec -f login-steps.json
|
|
2824
3967
|
|
|
2825
|
-
|
|
2826
|
-
|
|
3968
|
+
Likely next commands:
|
|
3969
|
+
bp snapshot -i
|
|
3970
|
+
bp diagnose "<selector>"
|
|
3971
|
+
bp trace summary -s <session> --view console
|
|
2827
3972
|
`.trimEnd();
|
|
2828
3973
|
function parseExecArgs(args) {
|
|
2829
3974
|
const options = {};
|
|
@@ -2884,24 +4029,24 @@ async function captureFinalUrl(page, steps, fallback) {
|
|
|
2884
4029
|
if (!mightNavigate) {
|
|
2885
4030
|
return currentUrl;
|
|
2886
4031
|
}
|
|
2887
|
-
await new Promise((
|
|
4032
|
+
await new Promise((resolve8) => setTimeout(resolve8, 200));
|
|
2888
4033
|
return getCurrentUrlSafe(page, currentUrl);
|
|
2889
4034
|
}
|
|
2890
4035
|
function mirrorRecordingToExport(recordingManifest, exportLogPath) {
|
|
2891
4036
|
try {
|
|
2892
|
-
const sourceDir =
|
|
2893
|
-
const exportDir =
|
|
4037
|
+
const sourceDir = dirname6(recordingManifest);
|
|
4038
|
+
const exportDir = dirname6(exportLogPath);
|
|
2894
4039
|
const manifestName = basename2(recordingManifest);
|
|
2895
|
-
const exportManifestPath =
|
|
4040
|
+
const exportManifestPath = join8(exportDir, manifestName);
|
|
2896
4041
|
nodeFs.copyFileSync(recordingManifest, exportManifestPath);
|
|
2897
|
-
const sourceScreenshotsDir =
|
|
2898
|
-
const exportScreenshotsDir =
|
|
4042
|
+
const sourceScreenshotsDir = join8(sourceDir, "screenshots");
|
|
4043
|
+
const exportScreenshotsDir = join8(exportDir, "screenshots");
|
|
2899
4044
|
if (nodeFs.existsSync(sourceScreenshotsDir)) {
|
|
2900
4045
|
nodeFs.rmSync(exportScreenshotsDir, { force: true, recursive: true });
|
|
2901
4046
|
nodeFs.mkdirSync(exportScreenshotsDir, { recursive: true });
|
|
2902
4047
|
const files = nodeFs.readdirSync(sourceScreenshotsDir);
|
|
2903
4048
|
for (const file of files) {
|
|
2904
|
-
nodeFs.copyFileSync(
|
|
4049
|
+
nodeFs.copyFileSync(join8(sourceScreenshotsDir, file), join8(exportScreenshotsDir, file));
|
|
2905
4050
|
}
|
|
2906
4051
|
}
|
|
2907
4052
|
} catch (err) {
|
|
@@ -2915,8 +4060,8 @@ async function execCommand(args, globalOptions) {
|
|
|
2915
4060
|
}
|
|
2916
4061
|
let { actionsJson, options: execOptions } = parseExecArgs(args);
|
|
2917
4062
|
if (execOptions.file) {
|
|
2918
|
-
const
|
|
2919
|
-
actionsJson = await
|
|
4063
|
+
const fs9 = await import("fs/promises");
|
|
4064
|
+
actionsJson = await fs9.readFile(execOptions.file, "utf-8");
|
|
2920
4065
|
}
|
|
2921
4066
|
if (!actionsJson && !process.stdin.isTTY) {
|
|
2922
4067
|
const chunks = [];
|
|
@@ -2965,10 +4110,10 @@ Run 'bp actions' for complete action reference.${evalTip}`
|
|
|
2965
4110
|
highlights: execOptions.noHighlights ? false : sessionRecord?.highlights ?? true
|
|
2966
4111
|
};
|
|
2967
4112
|
if (execOptions.recordDir) {
|
|
2968
|
-
recordOptions.outputDir =
|
|
4113
|
+
recordOptions.outputDir = resolve4(execOptions.recordDir);
|
|
2969
4114
|
} else {
|
|
2970
|
-
const { homedir:
|
|
2971
|
-
recordOptions.outputDir =
|
|
4115
|
+
const { homedir: homedir7 } = await import("os");
|
|
4116
|
+
recordOptions.outputDir = join8(homedir7(), ".browser-pilot", "sessions", session.id);
|
|
2972
4117
|
}
|
|
2973
4118
|
}
|
|
2974
4119
|
const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
|
|
@@ -3004,6 +4149,38 @@ Run 'bp actions' for complete action reference.${evalTip}`
|
|
|
3004
4149
|
stepResult.durationMs,
|
|
3005
4150
|
stepResult.screenshotPath ? basename2(stepResult.screenshotPath) : void 0
|
|
3006
4151
|
);
|
|
4152
|
+
if (stepResult.success && stepResult.action === "waitForWsMessage" && stepResult.result) {
|
|
4153
|
+
logger.logTrace({
|
|
4154
|
+
channel: "ws",
|
|
4155
|
+
event: "ws.frame.received",
|
|
4156
|
+
summary: "waitForWsMessage matched",
|
|
4157
|
+
data: stepResult.result
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
4160
|
+
if (stepResult.success && stepResult.action === "assertTextChanged" && stepResult.text) {
|
|
4161
|
+
logger.logTrace({
|
|
4162
|
+
channel: "dom",
|
|
4163
|
+
event: "dom.text.changed",
|
|
4164
|
+
summary: "Text changed",
|
|
4165
|
+
data: { text: stepResult.text }
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
if (stepResult.success && stepResult.action === "assertPermission" && stepResult.result) {
|
|
4169
|
+
logger.logTrace({
|
|
4170
|
+
channel: "permission",
|
|
4171
|
+
event: "permission.state",
|
|
4172
|
+
summary: "Permission assertion passed",
|
|
4173
|
+
data: stepResult.result
|
|
4174
|
+
});
|
|
4175
|
+
}
|
|
4176
|
+
if (stepResult.success && stepResult.action === "assertMediaTrackLive" && stepResult.result) {
|
|
4177
|
+
logger.logTrace({
|
|
4178
|
+
channel: "media",
|
|
4179
|
+
event: "media.track.started",
|
|
4180
|
+
summary: "Live media track detected",
|
|
4181
|
+
data: stepResult.result
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
3007
4184
|
}
|
|
3008
4185
|
if (result.recordingManifest && session.exportLog) {
|
|
3009
4186
|
mirrorRecordingToExport(result.recordingManifest, session.exportLog);
|
|
@@ -3056,8 +4233,8 @@ Run 'bp actions' for complete action reference.${evalTip}`
|
|
|
3056
4233
|
...result.recordingManifest ? { recordingManifest: result.recordingManifest } : {}
|
|
3057
4234
|
};
|
|
3058
4235
|
if (execOptions.outputFile) {
|
|
3059
|
-
const
|
|
3060
|
-
await
|
|
4236
|
+
const fs9 = await import("fs/promises");
|
|
4237
|
+
await fs9.writeFile(execOptions.outputFile, renderOutput(payload, globalOptions.format));
|
|
3061
4238
|
process.stderr.write(`Wrote output to ${execOptions.outputFile}
|
|
3062
4239
|
`);
|
|
3063
4240
|
} else {
|
|
@@ -3067,7 +4244,7 @@ Run 'bp actions' for complete action reference.${evalTip}`
|
|
|
3067
4244
|
const frameCount = result.steps.filter((s) => s.screenshotPath).length;
|
|
3068
4245
|
process.stderr.write(
|
|
3069
4246
|
`
|
|
3070
|
-
Recording: ${frameCount} screenshots saved to ${
|
|
4247
|
+
Recording: ${frameCount} screenshots saved to ${dirname6(result.recordingManifest)}
|
|
3071
4248
|
`
|
|
3072
4249
|
);
|
|
3073
4250
|
}
|
|
@@ -3242,501 +4419,161 @@ function parseListArgs(args) {
|
|
|
3242
4419
|
}
|
|
3243
4420
|
}
|
|
3244
4421
|
return options;
|
|
3245
|
-
}
|
|
3246
|
-
function formatLogEntry(entry) {
|
|
3247
|
-
const time = new Date(entry.ts).toLocaleTimeString();
|
|
3248
|
-
const status = entry.status === "failed" ? "\u2717" : entry.status === "success" ? "\u2713" : "\u25CB";
|
|
3249
|
-
if (entry.type === "command") {
|
|
3250
|
-
const dur = entry.durationMs ? `(${entry.durationMs}ms)` : "";
|
|
3251
|
-
return `${time} ${status} ${entry.cmd} ${dur}`;
|
|
3252
|
-
}
|
|
3253
|
-
if (entry.type === "error") {
|
|
3254
|
-
return `${time} \u2717 ERROR: ${entry.error}`;
|
|
3255
|
-
}
|
|
3256
|
-
return `${time} ${entry.type}: ${entry.cmd || ""}`;
|
|
3257
|
-
}
|
|
3258
|
-
async function listCommand(args, globalOptions) {
|
|
3259
|
-
const listOptions = parseListArgs(args);
|
|
3260
|
-
if (listOptions.help) {
|
|
3261
|
-
console.log(HELP);
|
|
3262
|
-
return;
|
|
3263
|
-
}
|
|
3264
|
-
if (listOptions.logPath || listOptions.logTail !== void 0 || listOptions.info) {
|
|
3265
|
-
let session;
|
|
3266
|
-
if (globalOptions.session) {
|
|
3267
|
-
session = await loadSession(globalOptions.session);
|
|
3268
|
-
} else {
|
|
3269
|
-
session = await getDefaultSession();
|
|
3270
|
-
}
|
|
3271
|
-
if (!session) {
|
|
3272
|
-
throw new Error('No session found. Run "bp connect" first or specify with -s.');
|
|
3273
|
-
}
|
|
3274
|
-
const logger = getSessionLogger(session.id);
|
|
3275
|
-
if (listOptions.logPath) {
|
|
3276
|
-
console.log(logger.getLogPath());
|
|
3277
|
-
return;
|
|
3278
|
-
}
|
|
3279
|
-
if (listOptions.logTail !== void 0) {
|
|
3280
|
-
const entries = logger.tailLog(listOptions.logTail);
|
|
3281
|
-
if (globalOptions.format === "json") {
|
|
3282
|
-
output(entries, "json");
|
|
3283
|
-
return;
|
|
3284
|
-
}
|
|
3285
|
-
if (entries.length === 0) {
|
|
3286
|
-
console.log("No log entries.");
|
|
3287
|
-
return;
|
|
3288
|
-
}
|
|
3289
|
-
console.log(`Last ${entries.length} log entries for session ${session.id}:
|
|
3290
|
-
`);
|
|
3291
|
-
for (const entry of entries) {
|
|
3292
|
-
console.log(` ${formatLogEntry(entry)}`);
|
|
3293
|
-
}
|
|
3294
|
-
return;
|
|
3295
|
-
}
|
|
3296
|
-
if (listOptions.info) {
|
|
3297
|
-
const stats = logger.getLogStats();
|
|
3298
|
-
if (globalOptions.format === "json") {
|
|
3299
|
-
output({ session, logStats: stats }, "json");
|
|
3300
|
-
return;
|
|
3301
|
-
}
|
|
3302
|
-
console.log(`Session: ${session.id}
|
|
3303
|
-
`);
|
|
3304
|
-
console.log(` Provider: ${session.provider}`);
|
|
3305
|
-
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
|
|
3306
|
-
console.log(` Last activity: ${new Date(session.lastActivity).toLocaleString()}`);
|
|
3307
|
-
console.log(` URL: ${session.currentUrl}`);
|
|
3308
|
-
if (session.exportLog) {
|
|
3309
|
-
console.log(` Export log: ${session.exportLog}`);
|
|
3310
|
-
}
|
|
3311
|
-
console.log("");
|
|
3312
|
-
console.log("Log Stats:");
|
|
3313
|
-
console.log(` Path: ${logger.getLogPath()}`);
|
|
3314
|
-
console.log(` Entries: ${stats.entries}`);
|
|
3315
|
-
console.log(` Size: ${formatBytes3(stats.size)}`);
|
|
3316
|
-
if (stats.first) {
|
|
3317
|
-
console.log(` First: ${new Date(stats.first).toLocaleString()}`);
|
|
3318
|
-
}
|
|
3319
|
-
if (stats.last) {
|
|
3320
|
-
console.log(` Last: ${new Date(stats.last).toLocaleString()}`);
|
|
3321
|
-
}
|
|
3322
|
-
return;
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
const sessions = await listSessions();
|
|
3326
|
-
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
3327
|
-
const now = Date.now();
|
|
3328
|
-
const stale = sessions.filter((s) => now - new Date(s.lastActivity).getTime() > TWO_DAYS_MS);
|
|
3329
|
-
const fresh = sessions.filter((s) => now - new Date(s.lastActivity).getTime() <= TWO_DAYS_MS);
|
|
3330
|
-
if (stale.length > 0) {
|
|
3331
|
-
for (const s of stale) {
|
|
3332
|
-
await deleteSessionFull(s.id);
|
|
3333
|
-
}
|
|
3334
|
-
}
|
|
3335
|
-
if (globalOptions.format === "json") {
|
|
3336
|
-
output(fresh, "json");
|
|
3337
|
-
return;
|
|
3338
|
-
}
|
|
3339
|
-
if (stale.length > 0) {
|
|
3340
|
-
console.log(`Cleaned ${stale.length} stale session(s) (>2 days old).
|
|
3341
|
-
`);
|
|
3342
|
-
}
|
|
3343
|
-
if (fresh.length === 0) {
|
|
3344
|
-
console.log("No active sessions.");
|
|
3345
|
-
console.log('Run "bp connect" to create a new session.');
|
|
3346
|
-
return;
|
|
3347
|
-
}
|
|
3348
|
-
console.log("Active Sessions:\n");
|
|
3349
|
-
console.log(" Tip: bp list -s <name> --log-tail View action log");
|
|
3350
|
-
console.log(" bp list -s <name> --log-path Get log file path\n");
|
|
3351
|
-
const displaySessions = fresh.slice(0, 20);
|
|
3352
|
-
for (const session of displaySessions) {
|
|
3353
|
-
const age = getAge(new Date(session.lastActivity));
|
|
3354
|
-
const daemonStatus = session.daemon ? isDaemonAlive(session.daemon.pid) ? "running" : "dead" : "none";
|
|
3355
|
-
console.log(` ${session.id}`);
|
|
3356
|
-
console.log(` Provider: ${session.provider}`);
|
|
3357
|
-
console.log(` URL: ${session.currentUrl}`);
|
|
3358
|
-
console.log(` Last activity: ${age}`);
|
|
3359
|
-
console.log(` Daemon: ${daemonStatus}`);
|
|
3360
|
-
console.log("");
|
|
3361
|
-
}
|
|
3362
|
-
if (fresh.length > 20) {
|
|
3363
|
-
console.log(` (showing 20 of ${fresh.length} sessions)
|
|
3364
|
-
`);
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
function formatBytes3(bytes) {
|
|
3368
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
3369
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3370
|
-
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
3371
|
-
}
|
|
3372
|
-
function getAge(date) {
|
|
3373
|
-
const now = Date.now();
|
|
3374
|
-
const diff = now - date.getTime();
|
|
3375
|
-
const seconds = Math.floor(diff / 1e3);
|
|
3376
|
-
if (seconds < 60) return "just now";
|
|
3377
|
-
const minutes = Math.floor(seconds / 60);
|
|
3378
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
3379
|
-
const hours = Math.floor(minutes / 60);
|
|
3380
|
-
if (hours < 24) return `${hours}h ago`;
|
|
3381
|
-
const days = Math.floor(hours / 24);
|
|
3382
|
-
return `${days}d ago`;
|
|
3383
|
-
}
|
|
3384
|
-
|
|
3385
|
-
// src/cli/commands/listen.ts
|
|
3386
|
-
var LISTEN_HELP = `
|
|
3387
|
-
bp listen - Monitor network traffic (WebSocket/HTTP)
|
|
3388
|
-
|
|
3389
|
-
Attach to a browser session and stream network events as JSONL.
|
|
3390
|
-
Status messages go to stderr; stdout is clean JSONL (pipeable to jq).
|
|
3391
|
-
|
|
3392
|
-
Usage:
|
|
3393
|
-
bp listen <mode> [options]
|
|
3394
|
-
|
|
3395
|
-
Modes:
|
|
3396
|
-
ws WebSocket traffic only
|
|
3397
|
-
http HTTP requests/responses only
|
|
3398
|
-
all Both WebSocket and HTTP
|
|
3399
|
-
|
|
3400
|
-
Options:
|
|
3401
|
-
-s, --session [id] Session to use (omit: auto-connect, -s: latest, -s <id>: specific)
|
|
3402
|
-
-m, --match <glob> Filter by URL glob pattern (e.g. "*realtime*")
|
|
3403
|
-
-o, --output <file> Write JSONL to file instead of stdout
|
|
3404
|
-
--max-payload <n> Max text payload preview length (default: 256)
|
|
3405
|
-
--timeout <ms> Auto-stop after N milliseconds
|
|
3406
|
-
-q, --quiet Suppress stderr status messages
|
|
3407
|
-
-h, --help Show this help
|
|
3408
|
-
|
|
3409
|
-
Output Format (JSONL):
|
|
3410
|
-
{"ts":"...","type":"ws:created","requestId":"1.2","url":"wss://..."}
|
|
3411
|
-
{"ts":"...","type":"ws:frame:sent","requestId":"1.2","opcode":1,"length":142,"payload":"..."}
|
|
3412
|
-
{"ts":"...","type":"ws:frame:recv","requestId":"1.2","opcode":2,"length":24000,"payload":"[binary: 18000 bytes]"}
|
|
3413
|
-
{"ts":"...","type":"ws:closed","requestId":"1.2"}
|
|
3414
|
-
{"ts":"...","type":"http:request","requestId":"3.1","method":"POST","url":"https://..."}
|
|
3415
|
-
{"ts":"...","type":"http:response","requestId":"3.1","status":200,"mimeType":"application/json"}
|
|
3416
|
-
|
|
3417
|
-
Examples:
|
|
3418
|
-
# Debug a voice agent's WebSocket protocol
|
|
3419
|
-
bp listen ws -m "*voice*" -o voice-traffic.jsonl
|
|
3420
|
-
|
|
3421
|
-
# Watch all API calls during a session
|
|
3422
|
-
bp listen http -m "*/api/*" --max-payload 1024
|
|
3423
|
-
|
|
3424
|
-
# Capture everything for 60 seconds
|
|
3425
|
-
bp listen all -o full-trace.jsonl --timeout 60000
|
|
3426
|
-
|
|
3427
|
-
# Pipe to jq for live filtering
|
|
3428
|
-
bp listen ws | jq 'select(.type == "ws:frame:recv")'
|
|
3429
|
-
`;
|
|
3430
|
-
function parseListenArgs(args) {
|
|
3431
|
-
const options = {};
|
|
3432
|
-
for (let i = 0; i < args.length; i++) {
|
|
3433
|
-
const arg = args[i];
|
|
3434
|
-
if (arg === "-m" || arg === "--match") {
|
|
3435
|
-
options.match = args[++i];
|
|
3436
|
-
} else if (arg === "-o" || arg === "--output") {
|
|
3437
|
-
options.output = args[++i];
|
|
3438
|
-
} else if (arg === "--max-payload") {
|
|
3439
|
-
options.maxPayload = Number.parseInt(args[++i] ?? "", 10);
|
|
3440
|
-
} else if (arg === "--timeout") {
|
|
3441
|
-
options.timeout = Number.parseInt(args[++i] ?? "", 10);
|
|
3442
|
-
} else if (arg === "-q" || arg === "--quiet") {
|
|
3443
|
-
options.quiet = true;
|
|
3444
|
-
} else if (arg === "-h" || arg === "--help") {
|
|
3445
|
-
options.help = true;
|
|
3446
|
-
} else if (arg === "-s" || arg === "--session") {
|
|
3447
|
-
const nextArg = args[i + 1];
|
|
3448
|
-
if (!nextArg || nextArg.startsWith("-")) {
|
|
3449
|
-
options.useLatestSession = true;
|
|
3450
|
-
}
|
|
3451
|
-
} else if (!arg.startsWith("-") && !options.mode) {
|
|
3452
|
-
if (arg === "ws" || arg === "http" || arg === "all") {
|
|
3453
|
-
options.mode = arg;
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
}
|
|
3457
|
-
return options;
|
|
3458
|
-
}
|
|
3459
|
-
function globToRegex(pattern) {
|
|
3460
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3461
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
3462
|
-
return new RegExp(`^${withWildcards}$`);
|
|
3463
|
-
}
|
|
3464
|
-
var TrafficMonitor = class {
|
|
3465
|
-
cdp;
|
|
3466
|
-
opts;
|
|
3467
|
-
matchRegex;
|
|
3468
|
-
wsUrls = /* @__PURE__ */ new Map();
|
|
3469
|
-
httpUrls = /* @__PURE__ */ new Map();
|
|
3470
|
-
handlers = [];
|
|
3471
|
-
lineCount = 0;
|
|
3472
|
-
constructor(cdp, opts) {
|
|
3473
|
-
this.cdp = cdp;
|
|
3474
|
-
this.opts = opts;
|
|
3475
|
-
this.matchRegex = opts.match ? globToRegex(opts.match) : null;
|
|
3476
|
-
}
|
|
3477
|
-
emit(record) {
|
|
3478
|
-
this.opts.write(JSON.stringify(record));
|
|
3479
|
-
this.lineCount++;
|
|
3480
|
-
}
|
|
3481
|
-
matchesUrl(url) {
|
|
3482
|
-
if (!this.matchRegex) return true;
|
|
3483
|
-
return this.matchRegex.test(url);
|
|
3484
|
-
}
|
|
3485
|
-
formatPayload(payloadData, opcode) {
|
|
3486
|
-
const data = payloadData ?? "";
|
|
3487
|
-
if (opcode === 2) {
|
|
3488
|
-
const byteLength = Math.floor(data.length * 3 / 4);
|
|
3489
|
-
return { payload: `[binary: ${byteLength} bytes]`, length: data.length };
|
|
3490
|
-
}
|
|
3491
|
-
const length = data.length;
|
|
3492
|
-
if (length > this.opts.maxPayload) {
|
|
3493
|
-
return {
|
|
3494
|
-
payload: `${data.slice(0, this.opts.maxPayload)}... [truncated, ${length} total]`,
|
|
3495
|
-
length
|
|
3496
|
-
};
|
|
3497
|
-
}
|
|
3498
|
-
return { payload: data, length };
|
|
3499
|
-
}
|
|
3500
|
-
subscribe(event, handler) {
|
|
3501
|
-
this.cdp.on(event, handler);
|
|
3502
|
-
this.handlers.push({ event, handler });
|
|
3503
|
-
}
|
|
3504
|
-
start() {
|
|
3505
|
-
const mode = this.opts.mode;
|
|
3506
|
-
if (mode === "ws" || mode === "all") {
|
|
3507
|
-
this.subscribe("Network.webSocketCreated", (params) => {
|
|
3508
|
-
const url = params["url"];
|
|
3509
|
-
const requestId = params["requestId"];
|
|
3510
|
-
if (!this.matchesUrl(url)) return;
|
|
3511
|
-
this.wsUrls.set(requestId, url);
|
|
3512
|
-
this.emit({
|
|
3513
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3514
|
-
type: "ws:created",
|
|
3515
|
-
requestId,
|
|
3516
|
-
url
|
|
3517
|
-
});
|
|
3518
|
-
});
|
|
3519
|
-
this.subscribe("Network.webSocketFrameSent", (params) => {
|
|
3520
|
-
const requestId = params["requestId"];
|
|
3521
|
-
if (!this.wsUrls.has(requestId)) return;
|
|
3522
|
-
const response = params["response"];
|
|
3523
|
-
const opcode = response?.opcode ?? 1;
|
|
3524
|
-
const { payload, length } = this.formatPayload(response?.payloadData, opcode);
|
|
3525
|
-
this.emit({
|
|
3526
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3527
|
-
type: "ws:frame:sent",
|
|
3528
|
-
requestId,
|
|
3529
|
-
opcode,
|
|
3530
|
-
length,
|
|
3531
|
-
payload
|
|
3532
|
-
});
|
|
3533
|
-
});
|
|
3534
|
-
this.subscribe("Network.webSocketFrameReceived", (params) => {
|
|
3535
|
-
const requestId = params["requestId"];
|
|
3536
|
-
if (!this.wsUrls.has(requestId)) return;
|
|
3537
|
-
const response = params["response"];
|
|
3538
|
-
const opcode = response?.opcode ?? 1;
|
|
3539
|
-
const { payload, length } = this.formatPayload(response?.payloadData, opcode);
|
|
3540
|
-
this.emit({
|
|
3541
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3542
|
-
type: "ws:frame:recv",
|
|
3543
|
-
requestId,
|
|
3544
|
-
opcode,
|
|
3545
|
-
length,
|
|
3546
|
-
payload
|
|
3547
|
-
});
|
|
3548
|
-
});
|
|
3549
|
-
this.subscribe("Network.webSocketClosed", (params) => {
|
|
3550
|
-
const requestId = params["requestId"];
|
|
3551
|
-
if (!this.wsUrls.has(requestId)) return;
|
|
3552
|
-
this.wsUrls.delete(requestId);
|
|
3553
|
-
this.emit({
|
|
3554
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3555
|
-
type: "ws:closed",
|
|
3556
|
-
requestId
|
|
3557
|
-
});
|
|
3558
|
-
});
|
|
3559
|
-
}
|
|
3560
|
-
if (mode === "http" || mode === "all") {
|
|
3561
|
-
this.subscribe("Network.requestWillBeSent", (params) => {
|
|
3562
|
-
const request = params["request"];
|
|
3563
|
-
const url = request?.url ?? "";
|
|
3564
|
-
const requestId = params["requestId"];
|
|
3565
|
-
if (!this.matchesUrl(url)) return;
|
|
3566
|
-
this.httpUrls.set(requestId, url);
|
|
3567
|
-
this.emit({
|
|
3568
|
-
ts: params["wallTime"] ? new Date(params["wallTime"] * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
3569
|
-
type: "http:request",
|
|
3570
|
-
requestId,
|
|
3571
|
-
method: request?.method ?? "GET",
|
|
3572
|
-
url
|
|
3573
|
-
});
|
|
3574
|
-
});
|
|
3575
|
-
this.subscribe("Network.responseReceived", (params) => {
|
|
3576
|
-
const requestId = params["requestId"];
|
|
3577
|
-
if (!this.httpUrls.has(requestId)) return;
|
|
3578
|
-
const response = params["response"];
|
|
3579
|
-
this.emit({
|
|
3580
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3581
|
-
type: "http:response",
|
|
3582
|
-
requestId,
|
|
3583
|
-
status: response?.status ?? 0,
|
|
3584
|
-
mimeType: response?.mimeType ?? ""
|
|
3585
|
-
});
|
|
3586
|
-
});
|
|
3587
|
-
this.subscribe("Network.loadingFailed", (params) => {
|
|
3588
|
-
const requestId = params["requestId"];
|
|
3589
|
-
if (!this.httpUrls.has(requestId)) return;
|
|
3590
|
-
this.emit({
|
|
3591
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3592
|
-
type: "http:failed",
|
|
3593
|
-
requestId,
|
|
3594
|
-
errorText: params["errorText"] ?? ""
|
|
3595
|
-
});
|
|
3596
|
-
});
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
stop() {
|
|
3600
|
-
for (const { event, handler } of this.handlers) {
|
|
3601
|
-
this.cdp.off(event, handler);
|
|
3602
|
-
}
|
|
3603
|
-
this.handlers = [];
|
|
3604
|
-
}
|
|
3605
|
-
};
|
|
3606
|
-
async function resolveConnection2(sessionId, useLatestSession, trace) {
|
|
3607
|
-
if (sessionId) {
|
|
3608
|
-
const session2 = await loadSession(sessionId);
|
|
3609
|
-
const browser2 = await connect({
|
|
3610
|
-
provider: session2.provider,
|
|
3611
|
-
wsUrl: session2.wsUrl,
|
|
3612
|
-
debug: trace
|
|
3613
|
-
});
|
|
3614
|
-
return { browser: browser2, session: session2 };
|
|
3615
|
-
}
|
|
3616
|
-
if (useLatestSession) {
|
|
3617
|
-
const session2 = await getDefaultSession();
|
|
3618
|
-
if (!session2) {
|
|
3619
|
-
throw new Error('No sessions found. Run "bp connect" first or omit -s to auto-connect.');
|
|
3620
|
-
}
|
|
3621
|
-
const browser2 = await connect({
|
|
3622
|
-
provider: session2.provider,
|
|
3623
|
-
wsUrl: session2.wsUrl,
|
|
3624
|
-
debug: trace
|
|
3625
|
-
});
|
|
3626
|
-
return { browser: browser2, session: session2 };
|
|
4422
|
+
}
|
|
4423
|
+
function formatLogEntry(entry) {
|
|
4424
|
+
const time = new Date(entry.ts).toLocaleTimeString();
|
|
4425
|
+
const status = entry.status === "failed" ? "\u2717" : entry.status === "success" ? "\u2713" : "\u25CB";
|
|
4426
|
+
if (entry.type === "command") {
|
|
4427
|
+
const dur = entry.durationMs ? `(${entry.durationMs}ms)` : "";
|
|
4428
|
+
return `${time} ${status} ${entry.cmd} ${dur}`;
|
|
3627
4429
|
}
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
wsUrl = await getBrowserWebSocketUrl("localhost:9222");
|
|
3631
|
-
} catch {
|
|
3632
|
-
throw new Error(
|
|
3633
|
-
"Could not auto-discover browser.\nEither:\n 1. Start Chrome with: --remote-debugging-port=9222\n 2. Use an existing session: bp listen -s <session-id>\n 3. Use latest session: bp listen -s"
|
|
3634
|
-
);
|
|
4430
|
+
if (entry.type === "error") {
|
|
4431
|
+
return `${time} \u2717 ERROR: ${entry.error}`;
|
|
3635
4432
|
}
|
|
3636
|
-
|
|
3637
|
-
const page = await browser.page();
|
|
3638
|
-
const currentUrl = await page.url();
|
|
3639
|
-
const newSessionId = generateSessionId();
|
|
3640
|
-
const session = {
|
|
3641
|
-
id: newSessionId,
|
|
3642
|
-
provider: "generic",
|
|
3643
|
-
wsUrl: browser.wsUrl,
|
|
3644
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3645
|
-
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3646
|
-
currentUrl
|
|
3647
|
-
};
|
|
3648
|
-
await saveSession(session);
|
|
3649
|
-
return { browser, session };
|
|
4433
|
+
return `${time} ${entry.type}: ${entry.cmd || ""}`;
|
|
3650
4434
|
}
|
|
3651
|
-
async function
|
|
3652
|
-
const
|
|
3653
|
-
if (
|
|
3654
|
-
console.log(
|
|
4435
|
+
async function listCommand(args, globalOptions) {
|
|
4436
|
+
const listOptions = parseListArgs(args);
|
|
4437
|
+
if (listOptions.help) {
|
|
4438
|
+
console.log(HELP);
|
|
3655
4439
|
return;
|
|
3656
4440
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
4441
|
+
if (listOptions.logPath || listOptions.logTail !== void 0 || listOptions.info) {
|
|
4442
|
+
let session;
|
|
4443
|
+
if (globalOptions.session) {
|
|
4444
|
+
session = await loadSession(globalOptions.session);
|
|
4445
|
+
} else {
|
|
4446
|
+
session = await getDefaultSession();
|
|
4447
|
+
}
|
|
4448
|
+
if (!session) {
|
|
4449
|
+
throw new Error('No session found. Run "bp connect" first or specify with -s.');
|
|
4450
|
+
}
|
|
4451
|
+
const logger = getSessionLogger(session.id);
|
|
4452
|
+
if (listOptions.logPath) {
|
|
4453
|
+
console.log(logger.getLogPath());
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
if (listOptions.logTail !== void 0) {
|
|
4457
|
+
const entries = logger.tailLog(listOptions.logTail);
|
|
4458
|
+
if (globalOptions.format === "json") {
|
|
4459
|
+
output(entries, "json");
|
|
4460
|
+
return;
|
|
4461
|
+
}
|
|
4462
|
+
if (entries.length === 0) {
|
|
4463
|
+
console.log("No log entries.");
|
|
4464
|
+
return;
|
|
4465
|
+
}
|
|
4466
|
+
console.log(`Last ${entries.length} log entries for session ${session.id}:
|
|
3659
4467
|
`);
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
`
|
|
3672
|
-
close: () => fileStream.end()
|
|
3673
|
-
};
|
|
3674
|
-
log(`Writing to ${options.output}`);
|
|
3675
|
-
} else {
|
|
3676
|
-
outputStream = {
|
|
3677
|
-
write: (line) => {
|
|
3678
|
-
try {
|
|
3679
|
-
process.stdout.write(`${line}
|
|
4468
|
+
for (const entry of entries) {
|
|
4469
|
+
console.log(` ${formatLogEntry(entry)}`);
|
|
4470
|
+
}
|
|
4471
|
+
return;
|
|
4472
|
+
}
|
|
4473
|
+
if (listOptions.info) {
|
|
4474
|
+
const stats = logger.getLogStats();
|
|
4475
|
+
if (globalOptions.format === "json") {
|
|
4476
|
+
output({ session, logStats: stats }, "json");
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
console.log(`Session: ${session.id}
|
|
3680
4480
|
`);
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
4481
|
+
console.log(` Provider: ${session.provider}`);
|
|
4482
|
+
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
|
|
4483
|
+
console.log(` Last activity: ${new Date(session.lastActivity).toLocaleString()}`);
|
|
4484
|
+
console.log(` URL: ${session.currentUrl}`);
|
|
4485
|
+
if (session.exportLog) {
|
|
4486
|
+
console.log(` Export log: ${session.exportLog}`);
|
|
3684
4487
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
4488
|
+
console.log("");
|
|
4489
|
+
console.log("Log Stats:");
|
|
4490
|
+
console.log(` Path: ${logger.getLogPath()}`);
|
|
4491
|
+
console.log(` Entries: ${stats.entries}`);
|
|
4492
|
+
console.log(` Size: ${formatBytes3(stats.size)}`);
|
|
4493
|
+
if (stats.first) {
|
|
4494
|
+
console.log(` First: ${new Date(stats.first).toLocaleString()}`);
|
|
3689
4495
|
}
|
|
3690
|
-
|
|
4496
|
+
if (stats.last) {
|
|
4497
|
+
console.log(` Last: ${new Date(stats.last).toLocaleString()}`);
|
|
4498
|
+
}
|
|
4499
|
+
return;
|
|
4500
|
+
}
|
|
3691
4501
|
}
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
write: (line) => outputStream.write(line)
|
|
3701
|
-
});
|
|
3702
|
-
monitor.start();
|
|
3703
|
-
const matchLabel = options.match ? ` matching "${options.match}"` : "";
|
|
3704
|
-
log(`Listening for ${options.mode} traffic${matchLabel} (session: ${session.id})`);
|
|
3705
|
-
log("Press Ctrl+C to stop.");
|
|
3706
|
-
let cleaned = false;
|
|
3707
|
-
const cleanup = () => {
|
|
3708
|
-
if (cleaned) return;
|
|
3709
|
-
cleaned = true;
|
|
3710
|
-
monitor.stop();
|
|
3711
|
-
log(`
|
|
3712
|
-
Stopped. ${monitor.lineCount} events captured.`);
|
|
3713
|
-
outputStream.close?.();
|
|
3714
|
-
browser.disconnect().catch(() => {
|
|
3715
|
-
});
|
|
3716
|
-
process.exit(0);
|
|
3717
|
-
};
|
|
3718
|
-
process.on("SIGINT", cleanup);
|
|
3719
|
-
process.on("SIGTERM", cleanup);
|
|
3720
|
-
if (options.timeout && options.timeout > 0) {
|
|
3721
|
-
setTimeout(() => {
|
|
3722
|
-
log(`
|
|
3723
|
-
Timeout reached (${options.timeout}ms).`);
|
|
3724
|
-
cleanup();
|
|
3725
|
-
}, options.timeout);
|
|
4502
|
+
const sessions = await listSessions();
|
|
4503
|
+
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
4504
|
+
const now = Date.now();
|
|
4505
|
+
const stale = sessions.filter((s) => now - new Date(s.lastActivity).getTime() > TWO_DAYS_MS);
|
|
4506
|
+
const fresh = sessions.filter((s) => now - new Date(s.lastActivity).getTime() <= TWO_DAYS_MS);
|
|
4507
|
+
if (stale.length > 0) {
|
|
4508
|
+
for (const s of stale) {
|
|
4509
|
+
await deleteSessionFull(s.id);
|
|
3726
4510
|
}
|
|
3727
|
-
await new Promise(() => {
|
|
3728
|
-
});
|
|
3729
|
-
} catch (error) {
|
|
3730
|
-
outputStream.close?.();
|
|
3731
|
-
await browser.disconnect();
|
|
3732
|
-
throw error;
|
|
3733
4511
|
}
|
|
4512
|
+
if (globalOptions.format === "json") {
|
|
4513
|
+
output(fresh, "json");
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
if (stale.length > 0) {
|
|
4517
|
+
console.log(`Cleaned ${stale.length} stale session(s) (>2 days old).
|
|
4518
|
+
`);
|
|
4519
|
+
}
|
|
4520
|
+
if (fresh.length === 0) {
|
|
4521
|
+
console.log("No active sessions.");
|
|
4522
|
+
console.log('Run "bp connect" to create a new session.');
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
console.log("Active Sessions:\n");
|
|
4526
|
+
console.log(" Tip: bp list -s <name> --log-tail View action log");
|
|
4527
|
+
console.log(" bp list -s <name> --log-path Get log file path\n");
|
|
4528
|
+
const displaySessions = fresh.slice(0, 20);
|
|
4529
|
+
for (const session of displaySessions) {
|
|
4530
|
+
const age = getAge(new Date(session.lastActivity));
|
|
4531
|
+
const daemonStatus = session.daemon ? isDaemonAlive(session.daemon.pid) ? "running" : "dead" : "none";
|
|
4532
|
+
console.log(` ${session.id}`);
|
|
4533
|
+
console.log(` Provider: ${session.provider}`);
|
|
4534
|
+
console.log(` URL: ${session.currentUrl}`);
|
|
4535
|
+
console.log(` Last activity: ${age}`);
|
|
4536
|
+
console.log(` Daemon: ${daemonStatus}`);
|
|
4537
|
+
console.log("");
|
|
4538
|
+
}
|
|
4539
|
+
if (fresh.length > 20) {
|
|
4540
|
+
console.log(` (showing 20 of ${fresh.length} sessions)
|
|
4541
|
+
`);
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
function formatBytes3(bytes) {
|
|
4545
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
4546
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
4547
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
4548
|
+
}
|
|
4549
|
+
function getAge(date) {
|
|
4550
|
+
const now = Date.now();
|
|
4551
|
+
const diff = now - date.getTime();
|
|
4552
|
+
const seconds = Math.floor(diff / 1e3);
|
|
4553
|
+
if (seconds < 60) return "just now";
|
|
4554
|
+
const minutes = Math.floor(seconds / 60);
|
|
4555
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
4556
|
+
const hours = Math.floor(minutes / 60);
|
|
4557
|
+
if (hours < 24) return `${hours}h ago`;
|
|
4558
|
+
const days = Math.floor(hours / 24);
|
|
4559
|
+
return `${days}d ago`;
|
|
3734
4560
|
}
|
|
3735
4561
|
|
|
3736
4562
|
// src/cli/commands/page.ts
|
|
3737
4563
|
var PAGE_HELP = `
|
|
3738
4564
|
bp page - Show a compact overview of the current page
|
|
3739
4565
|
|
|
4566
|
+
When to use:
|
|
4567
|
+
You want a quick summary of URL, title, headings, forms, and interactive controls.
|
|
4568
|
+
|
|
4569
|
+
When not to use:
|
|
4570
|
+
You need the full accessibility tree or reusable refs for precise automation. Use \`bp snapshot\`.
|
|
4571
|
+
|
|
4572
|
+
Likely next commands:
|
|
4573
|
+
bp snapshot -i
|
|
4574
|
+
bp forms
|
|
4575
|
+
bp exec '[{"action":"click","selector":"ref:e4"}]'
|
|
4576
|
+
|
|
3740
4577
|
Usage:
|
|
3741
4578
|
bp page [options]
|
|
3742
4579
|
|
|
@@ -3938,8 +4775,9 @@ async function quickstartCommand() {
|
|
|
3938
4775
|
|
|
3939
4776
|
// src/cli/commands/record.ts
|
|
3940
4777
|
import * as nodeFs2 from "fs";
|
|
3941
|
-
import {
|
|
3942
|
-
import {
|
|
4778
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4779
|
+
import { homedir as homedir6 } from "os";
|
|
4780
|
+
import { dirname as dirname7, join as join9, resolve as resolve5 } from "path";
|
|
3943
4781
|
|
|
3944
4782
|
// src/recording/aggregator.ts
|
|
3945
4783
|
var INPUT_DEBOUNCE_MS = 300;
|
|
@@ -4686,6 +5524,15 @@ var RECORDER_SCRIPT = `(function() {
|
|
|
4686
5524
|
})();`;
|
|
4687
5525
|
|
|
4688
5526
|
// src/recording/recorder.ts
|
|
5527
|
+
function readString(value) {
|
|
5528
|
+
return typeof value === "string" ? value : void 0;
|
|
5529
|
+
}
|
|
5530
|
+
function readStringOr(value, fallback = "") {
|
|
5531
|
+
return readString(value) ?? fallback;
|
|
5532
|
+
}
|
|
5533
|
+
function formatConsoleArg(entry) {
|
|
5534
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
5535
|
+
}
|
|
4689
5536
|
var Recorder = class {
|
|
4690
5537
|
cdp;
|
|
4691
5538
|
options;
|
|
@@ -4705,6 +5552,8 @@ var Recorder = class {
|
|
|
4705
5552
|
pendingBodies = [];
|
|
4706
5553
|
wsUrls = /* @__PURE__ */ new Map();
|
|
4707
5554
|
httpUrls = /* @__PURE__ */ new Map();
|
|
5555
|
+
traceEvents = [];
|
|
5556
|
+
traceHandlers = [];
|
|
4708
5557
|
constructor(cdp, options) {
|
|
4709
5558
|
this.cdp = cdp;
|
|
4710
5559
|
this.options = options ?? {};
|
|
@@ -4726,6 +5575,7 @@ var Recorder = class {
|
|
|
4726
5575
|
throw new Error("Recording already in progress");
|
|
4727
5576
|
}
|
|
4728
5577
|
this.events = [];
|
|
5578
|
+
this.traceEvents = [];
|
|
4729
5579
|
this.startTime = Date.now();
|
|
4730
5580
|
this.recording = true;
|
|
4731
5581
|
await this.cdp.send("Runtime.enable");
|
|
@@ -4740,23 +5590,75 @@ var Recorder = class {
|
|
|
4740
5590
|
this.startUrl = "";
|
|
4741
5591
|
}
|
|
4742
5592
|
await this.cdp.send("Runtime.addBinding", { name: RECORDER_BINDING_NAME });
|
|
5593
|
+
await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
4743
5594
|
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
4744
5595
|
source: RECORDER_SCRIPT
|
|
4745
5596
|
});
|
|
5597
|
+
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
5598
|
+
source: TRACE_SCRIPT
|
|
5599
|
+
});
|
|
4746
5600
|
await this.cdp.send("Runtime.evaluate", {
|
|
4747
5601
|
expression: RECORDER_SCRIPT,
|
|
4748
5602
|
awaitPromise: false
|
|
4749
5603
|
});
|
|
5604
|
+
await this.cdp.send("Runtime.evaluate", {
|
|
5605
|
+
expression: TRACE_SCRIPT,
|
|
5606
|
+
awaitPromise: false
|
|
5607
|
+
});
|
|
4750
5608
|
this.bindingHandler = (params) => {
|
|
5609
|
+
const payload = readString(params["payload"]);
|
|
5610
|
+
if (!payload) {
|
|
5611
|
+
return;
|
|
5612
|
+
}
|
|
4751
5613
|
if (params["name"] === RECORDER_BINDING_NAME) {
|
|
4752
|
-
this.handleBindingCall(
|
|
5614
|
+
this.handleBindingCall(payload);
|
|
5615
|
+
} else if (params["name"] === TRACE_BINDING_NAME) {
|
|
5616
|
+
this.handleTraceBindingCall(payload);
|
|
4753
5617
|
}
|
|
4754
5618
|
};
|
|
4755
5619
|
this.cdp.on("Runtime.bindingCalled", this.bindingHandler);
|
|
5620
|
+
this.subscribeTrace("Runtime.consoleAPICalled", (params) => {
|
|
5621
|
+
const type = readStringOr(params["type"], "log");
|
|
5622
|
+
if (type !== "log" && type !== "warn" && type !== "error") {
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
5626
|
+
const text = args.map(formatConsoleArg).filter(Boolean).join(" ");
|
|
5627
|
+
this.traceEvents.push(
|
|
5628
|
+
normalizeTraceEvent({
|
|
5629
|
+
traceId: createTraceId("console"),
|
|
5630
|
+
sessionId: "",
|
|
5631
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5632
|
+
elapsedMs: this.elapsed(),
|
|
5633
|
+
channel: "console",
|
|
5634
|
+
event: `console.${type}`,
|
|
5635
|
+
severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
|
|
5636
|
+
summary: text || `console.${type}`,
|
|
5637
|
+
data: { args },
|
|
5638
|
+
url: this.startUrl
|
|
5639
|
+
})
|
|
5640
|
+
);
|
|
5641
|
+
});
|
|
5642
|
+
this.subscribeTrace("Runtime.exceptionThrown", (params) => {
|
|
5643
|
+
const details = params["exceptionDetails"] ?? {};
|
|
5644
|
+
this.traceEvents.push(
|
|
5645
|
+
normalizeTraceEvent({
|
|
5646
|
+
traceId: createTraceId("runtime"),
|
|
5647
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5648
|
+
elapsedMs: this.elapsed(),
|
|
5649
|
+
channel: "runtime",
|
|
5650
|
+
event: "runtime.exception",
|
|
5651
|
+
severity: "error",
|
|
5652
|
+
summary: readString(details["text"]) ?? "Runtime exception",
|
|
5653
|
+
data: details,
|
|
5654
|
+
url: this.startUrl
|
|
5655
|
+
})
|
|
5656
|
+
);
|
|
5657
|
+
});
|
|
4756
5658
|
if (this.options.listen) {
|
|
4757
5659
|
const listenOpts = typeof this.options.listen === "boolean" ? { mode: "all" } : this.options.listen;
|
|
4758
5660
|
this.listenOpts = listenOpts;
|
|
4759
|
-
this.matchRegex = listenOpts.match ?
|
|
5661
|
+
this.matchRegex = listenOpts.match ? globToRegex(listenOpts.match) : null;
|
|
4760
5662
|
await this.cdp.send("Network.enable");
|
|
4761
5663
|
this.setupNetworkListeners(listenOpts);
|
|
4762
5664
|
}
|
|
@@ -4780,6 +5682,10 @@ var Recorder = class {
|
|
|
4780
5682
|
this.cdp.off(event, handler);
|
|
4781
5683
|
}
|
|
4782
5684
|
this.networkHandlers = [];
|
|
5685
|
+
for (const { event, handler } of this.traceHandlers) {
|
|
5686
|
+
this.cdp.off(event, handler);
|
|
5687
|
+
}
|
|
5688
|
+
this.traceHandlers = [];
|
|
4783
5689
|
if (this.listenOpts) {
|
|
4784
5690
|
await this.cdp.send("Network.disable");
|
|
4785
5691
|
}
|
|
@@ -4790,7 +5696,8 @@ var Recorder = class {
|
|
|
4790
5696
|
recordedAt: new Date(this.startTime).toISOString(),
|
|
4791
5697
|
startUrl: this.startUrl,
|
|
4792
5698
|
duration,
|
|
4793
|
-
steps
|
|
5699
|
+
steps,
|
|
5700
|
+
traceEvents: this.traceEvents
|
|
4794
5701
|
};
|
|
4795
5702
|
if (this.listenOpts) {
|
|
4796
5703
|
const mode = this.listenOpts.mode ?? "all";
|
|
@@ -4831,11 +5738,35 @@ var Recorder = class {
|
|
|
4831
5738
|
} catch {
|
|
4832
5739
|
}
|
|
4833
5740
|
}
|
|
5741
|
+
handleTraceBindingCall(payload) {
|
|
5742
|
+
if (!this.recording) return;
|
|
5743
|
+
try {
|
|
5744
|
+
const data = JSON.parse(payload);
|
|
5745
|
+
this.traceEvents.push(
|
|
5746
|
+
normalizeTraceEvent({
|
|
5747
|
+
traceId: createTraceId("trace"),
|
|
5748
|
+
ts: data.ts ? new Date(data.ts).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5749
|
+
elapsedMs: this.elapsed(),
|
|
5750
|
+
channel: this.channelForTraceEvent(data.event),
|
|
5751
|
+
event: data.event,
|
|
5752
|
+
severity: data.severity,
|
|
5753
|
+
summary: data.summary ?? data.event,
|
|
5754
|
+
data: data.data ?? {},
|
|
5755
|
+
url: typeof data.data?.["url"] === "string" ? data.data["url"] : this.startUrl
|
|
5756
|
+
})
|
|
5757
|
+
);
|
|
5758
|
+
} catch {
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
4834
5761
|
/** Subscribe to a CDP event, tracking for cleanup. */
|
|
4835
5762
|
subscribeNetwork(event, handler) {
|
|
4836
5763
|
this.cdp.on(event, handler);
|
|
4837
5764
|
this.networkHandlers.push({ event, handler });
|
|
4838
5765
|
}
|
|
5766
|
+
subscribeTrace(event, handler) {
|
|
5767
|
+
this.cdp.on(event, handler);
|
|
5768
|
+
this.traceHandlers.push({ event, handler });
|
|
5769
|
+
}
|
|
4839
5770
|
/** Check if a URL matches the configured filter. */
|
|
4840
5771
|
matchesUrl(url) {
|
|
4841
5772
|
if (!this.matchRegex) return true;
|
|
@@ -4879,6 +5810,20 @@ var Recorder = class {
|
|
|
4879
5810
|
type: "created",
|
|
4880
5811
|
url
|
|
4881
5812
|
});
|
|
5813
|
+
this.traceEvents.push(
|
|
5814
|
+
normalizeTraceEvent({
|
|
5815
|
+
traceId: createTraceId("ws"),
|
|
5816
|
+
ts: new Date(now).toISOString(),
|
|
5817
|
+
elapsedMs: this.elapsed(),
|
|
5818
|
+
channel: "ws",
|
|
5819
|
+
event: "ws.connection.created",
|
|
5820
|
+
summary: `WebSocket opened ${url}`,
|
|
5821
|
+
data: { url },
|
|
5822
|
+
connectionId: requestId,
|
|
5823
|
+
requestId,
|
|
5824
|
+
url
|
|
5825
|
+
})
|
|
5826
|
+
);
|
|
4882
5827
|
});
|
|
4883
5828
|
this.subscribeNetwork("Network.webSocketFrameSent", (params) => {
|
|
4884
5829
|
const requestId = params["requestId"];
|
|
@@ -4896,6 +5841,20 @@ var Recorder = class {
|
|
|
4896
5841
|
payload,
|
|
4897
5842
|
length
|
|
4898
5843
|
});
|
|
5844
|
+
this.traceEvents.push(
|
|
5845
|
+
normalizeTraceEvent({
|
|
5846
|
+
traceId: createTraceId("ws"),
|
|
5847
|
+
ts: new Date(now).toISOString(),
|
|
5848
|
+
elapsedMs: this.elapsed(),
|
|
5849
|
+
channel: "ws",
|
|
5850
|
+
event: "ws.frame.sent",
|
|
5851
|
+
summary: `WebSocket frame sent ${requestId}`,
|
|
5852
|
+
data: { opcode, payload, length },
|
|
5853
|
+
connectionId: requestId,
|
|
5854
|
+
requestId,
|
|
5855
|
+
url: this.wsUrls.get(requestId)
|
|
5856
|
+
})
|
|
5857
|
+
);
|
|
4899
5858
|
});
|
|
4900
5859
|
this.subscribeNetwork("Network.webSocketFrameReceived", (params) => {
|
|
4901
5860
|
const requestId = params["requestId"];
|
|
@@ -4913,10 +5872,25 @@ var Recorder = class {
|
|
|
4913
5872
|
payload,
|
|
4914
5873
|
length
|
|
4915
5874
|
});
|
|
5875
|
+
this.traceEvents.push(
|
|
5876
|
+
normalizeTraceEvent({
|
|
5877
|
+
traceId: createTraceId("ws"),
|
|
5878
|
+
ts: new Date(now).toISOString(),
|
|
5879
|
+
elapsedMs: this.elapsed(),
|
|
5880
|
+
channel: "ws",
|
|
5881
|
+
event: "ws.frame.received",
|
|
5882
|
+
summary: `WebSocket frame received ${requestId}`,
|
|
5883
|
+
data: { opcode, payload, length },
|
|
5884
|
+
connectionId: requestId,
|
|
5885
|
+
requestId,
|
|
5886
|
+
url: this.wsUrls.get(requestId)
|
|
5887
|
+
})
|
|
5888
|
+
);
|
|
4916
5889
|
});
|
|
4917
5890
|
this.subscribeNetwork("Network.webSocketClosed", (params) => {
|
|
4918
5891
|
const requestId = params["requestId"];
|
|
4919
5892
|
if (!this.wsUrls.has(requestId)) return;
|
|
5893
|
+
const url = this.wsUrls.get(requestId);
|
|
4920
5894
|
this.wsUrls.delete(requestId);
|
|
4921
5895
|
const now = Date.now();
|
|
4922
5896
|
this.wsEvents.push({
|
|
@@ -4925,6 +5899,21 @@ var Recorder = class {
|
|
|
4925
5899
|
elapsedMs: this.elapsed(),
|
|
4926
5900
|
type: "closed"
|
|
4927
5901
|
});
|
|
5902
|
+
this.traceEvents.push(
|
|
5903
|
+
normalizeTraceEvent({
|
|
5904
|
+
traceId: createTraceId("ws"),
|
|
5905
|
+
ts: new Date(now).toISOString(),
|
|
5906
|
+
elapsedMs: this.elapsed(),
|
|
5907
|
+
channel: "ws",
|
|
5908
|
+
event: "ws.connection.closed",
|
|
5909
|
+
severity: "warn",
|
|
5910
|
+
summary: `WebSocket closed ${requestId}`,
|
|
5911
|
+
data: { url: url ?? null },
|
|
5912
|
+
connectionId: requestId,
|
|
5913
|
+
requestId,
|
|
5914
|
+
url
|
|
5915
|
+
})
|
|
5916
|
+
);
|
|
4928
5917
|
});
|
|
4929
5918
|
}
|
|
4930
5919
|
if (mode === "http" || mode === "all") {
|
|
@@ -4944,6 +5933,23 @@ var Recorder = class {
|
|
|
4944
5933
|
headers: request?.headers,
|
|
4945
5934
|
body: request?.postData
|
|
4946
5935
|
});
|
|
5936
|
+
this.traceEvents.push(
|
|
5937
|
+
normalizeTraceEvent({
|
|
5938
|
+
traceId: createTraceId("http"),
|
|
5939
|
+
ts: new Date(now).toISOString(),
|
|
5940
|
+
elapsedMs: this.elapsed(),
|
|
5941
|
+
channel: "http",
|
|
5942
|
+
event: "http.request.sent",
|
|
5943
|
+
summary: `${request?.method ?? "GET"} ${url}`,
|
|
5944
|
+
data: {
|
|
5945
|
+
method: request?.method ?? "GET",
|
|
5946
|
+
headers: request?.headers ?? {},
|
|
5947
|
+
body: request?.postData ?? null
|
|
5948
|
+
},
|
|
5949
|
+
requestId,
|
|
5950
|
+
url
|
|
5951
|
+
})
|
|
5952
|
+
);
|
|
4947
5953
|
});
|
|
4948
5954
|
this.subscribeNetwork("Network.responseReceived", (params) => {
|
|
4949
5955
|
const requestId = params["requestId"];
|
|
@@ -4958,6 +5964,23 @@ var Recorder = class {
|
|
|
4958
5964
|
headers: response?.headers,
|
|
4959
5965
|
mimeType: response?.mimeType
|
|
4960
5966
|
});
|
|
5967
|
+
this.traceEvents.push(
|
|
5968
|
+
normalizeTraceEvent({
|
|
5969
|
+
traceId: createTraceId("http"),
|
|
5970
|
+
ts: new Date(now).toISOString(),
|
|
5971
|
+
elapsedMs: this.elapsed(),
|
|
5972
|
+
channel: "http",
|
|
5973
|
+
event: "http.response.received",
|
|
5974
|
+
summary: `${response?.status ?? 0} ${this.httpUrls.get(requestId) ?? ""}`,
|
|
5975
|
+
data: {
|
|
5976
|
+
status: response?.status ?? 0,
|
|
5977
|
+
headers: response?.headers ?? {},
|
|
5978
|
+
mimeType: response?.mimeType ?? null
|
|
5979
|
+
},
|
|
5980
|
+
requestId,
|
|
5981
|
+
url: this.httpUrls.get(requestId)
|
|
5982
|
+
})
|
|
5983
|
+
);
|
|
4961
5984
|
if (this.listenOpts?.captureResponseBodies) {
|
|
4962
5985
|
const bodyPromise = this.cdp.send("Network.getResponseBody", {
|
|
4963
5986
|
requestId
|
|
@@ -4972,8 +5995,37 @@ var Recorder = class {
|
|
|
4972
5995
|
this.pendingBodies.push(bodyPromise);
|
|
4973
5996
|
}
|
|
4974
5997
|
});
|
|
5998
|
+
this.subscribeNetwork("Network.loadingFailed", (params) => {
|
|
5999
|
+
const requestId = params["requestId"];
|
|
6000
|
+
this.traceEvents.push(
|
|
6001
|
+
normalizeTraceEvent({
|
|
6002
|
+
traceId: createTraceId("http"),
|
|
6003
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6004
|
+
elapsedMs: this.elapsed(),
|
|
6005
|
+
channel: "http",
|
|
6006
|
+
event: "http.response.failed",
|
|
6007
|
+
severity: "error",
|
|
6008
|
+
summary: `HTTP request failed ${requestId}`,
|
|
6009
|
+
data: {
|
|
6010
|
+
errorText: params["errorText"] ?? null,
|
|
6011
|
+
blockedReason: params["blockedReason"] ?? null,
|
|
6012
|
+
canceled: params["canceled"] ?? false
|
|
6013
|
+
},
|
|
6014
|
+
requestId,
|
|
6015
|
+
url: this.httpUrls.get(requestId)
|
|
6016
|
+
})
|
|
6017
|
+
);
|
|
6018
|
+
});
|
|
4975
6019
|
}
|
|
4976
6020
|
}
|
|
6021
|
+
channelForTraceEvent(eventName) {
|
|
6022
|
+
if (eventName.startsWith("permission.")) return "permission";
|
|
6023
|
+
if (eventName.startsWith("media.")) return "media";
|
|
6024
|
+
if (eventName.startsWith("voice.")) return "voice";
|
|
6025
|
+
if (eventName.startsWith("dom.")) return "dom";
|
|
6026
|
+
if (eventName.startsWith("runtime.")) return "runtime";
|
|
6027
|
+
return "session";
|
|
6028
|
+
}
|
|
4977
6029
|
/** Build a merged timeline from action events and network events. */
|
|
4978
6030
|
buildTimeline() {
|
|
4979
6031
|
const entries = [];
|
|
@@ -5021,7 +6073,7 @@ var Recorder = class {
|
|
|
5021
6073
|
return entries;
|
|
5022
6074
|
}
|
|
5023
6075
|
};
|
|
5024
|
-
function
|
|
6076
|
+
function globToRegex(pattern) {
|
|
5025
6077
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
5026
6078
|
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
5027
6079
|
return new RegExp(`^${withWildcards}$`);
|
|
@@ -5029,48 +6081,57 @@ function globToRegex2(pattern) {
|
|
|
5029
6081
|
|
|
5030
6082
|
// src/cli/commands/record.ts
|
|
5031
6083
|
var RECORD_HELP = `
|
|
5032
|
-
bp record -
|
|
6084
|
+
bp record - Capture a human demo into one canonical artifact
|
|
6085
|
+
|
|
6086
|
+
When to use:
|
|
6087
|
+
A human is demonstrating the workflow and you want replayable automation later.
|
|
6088
|
+
|
|
6089
|
+
When not to use:
|
|
6090
|
+
You already have steps and just want to run or validate them. Use \`bp exec\` or \`bp run\`.
|
|
6091
|
+
|
|
6092
|
+
Default flow:
|
|
6093
|
+
capture -> summary -> inspect or trace -> derive -> run
|
|
6094
|
+
|
|
6095
|
+
Common mistake:
|
|
6096
|
+
Opening \`recording.json\` first. Start with \`bp record summary\`.
|
|
5033
6097
|
|
|
5034
6098
|
Usage:
|
|
5035
6099
|
bp record [options]
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
--
|
|
5044
|
-
--
|
|
5045
|
-
--
|
|
5046
|
-
-
|
|
5047
|
-
|
|
5048
|
-
|
|
6100
|
+
bp record <inspect|summary|derive|export> [artifact] [options]
|
|
6101
|
+
|
|
6102
|
+
Capture options:
|
|
6103
|
+
-s, --session [id] Session to use (omit: auto-connect, -s: latest, -s <id>: specific)
|
|
6104
|
+
-f, --file <path> Artifact output path (default: recording.json)
|
|
6105
|
+
--timeout <ms> Auto-stop after timeout
|
|
6106
|
+
--profile <name> automation | realtime | voice | auth (default: automation)
|
|
6107
|
+
--listen [mode] ws | http | all (default: all)
|
|
6108
|
+
--bodies Capture HTTP response bodies
|
|
6109
|
+
-m, --match <glob> Filter HTTP/WS URLs
|
|
6110
|
+
--max-payload <n> Max WebSocket payload preview length (default: 256)
|
|
6111
|
+
|
|
6112
|
+
Artifact subcommands:
|
|
6113
|
+
inspect [artifact] Show artifact metadata and next commands
|
|
6114
|
+
summary [artifact] Show workflow summary plus trace views
|
|
6115
|
+
derive <artifact> -o <output> Write replayable steps for bp run
|
|
6116
|
+
export <artifact> -o <output> Write canonical triage bundle
|
|
5049
6117
|
|
|
5050
6118
|
Examples:
|
|
5051
|
-
bp record
|
|
5052
|
-
bp record
|
|
5053
|
-
bp record
|
|
5054
|
-
bp record
|
|
5055
|
-
bp record
|
|
5056
|
-
bp record
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
and merged into a unified timeline in the output file.
|
|
5065
|
-
Sensitive fields are automatically redacted as [REDACTED] based on the
|
|
5066
|
-
field settings (password, hidden, one-time-code, card autofill hints).
|
|
5067
|
-
Recording also enables session-level auto-recording for subsequent
|
|
5068
|
-
bp exec calls (equivalent to bp connect --record).
|
|
5069
|
-
|
|
5070
|
-
Press Ctrl+C to stop recording and save.
|
|
5071
|
-
`;
|
|
6119
|
+
bp record -s demo --profile automation
|
|
6120
|
+
bp record --profile voice --bodies
|
|
6121
|
+
bp record summary recording.json
|
|
6122
|
+
bp record inspect recording.json
|
|
6123
|
+
bp record derive recording.json -o workflow.json
|
|
6124
|
+
bp record export recording.json -o bundle.json
|
|
6125
|
+
|
|
6126
|
+
Likely next commands:
|
|
6127
|
+
bp record summary recording.json
|
|
6128
|
+
bp trace summary recording.json --view ws
|
|
6129
|
+
bp record derive recording.json -o workflow.json
|
|
6130
|
+
`.trim();
|
|
6131
|
+
var DEFAULT_ARTIFACT = "recording.json";
|
|
5072
6132
|
function parseRecordArgs(args) {
|
|
5073
6133
|
const options = {};
|
|
6134
|
+
let nextIsArtifact = false;
|
|
5074
6135
|
for (let i = 0; i < args.length; i++) {
|
|
5075
6136
|
const arg = args[i];
|
|
5076
6137
|
if (arg === "-f" || arg === "--file") {
|
|
@@ -5086,7 +6147,7 @@ function parseRecordArgs(args) {
|
|
|
5086
6147
|
}
|
|
5087
6148
|
} else if (arg === "--listen") {
|
|
5088
6149
|
const nextArg = args[i + 1];
|
|
5089
|
-
if (nextArg
|
|
6150
|
+
if (nextArg === "ws" || nextArg === "http" || nextArg === "all") {
|
|
5090
6151
|
options.listen = nextArg;
|
|
5091
6152
|
i++;
|
|
5092
6153
|
} else {
|
|
@@ -5098,176 +6159,265 @@ function parseRecordArgs(args) {
|
|
|
5098
6159
|
options.match = args[++i];
|
|
5099
6160
|
} else if (arg === "--max-payload") {
|
|
5100
6161
|
options.maxPayload = Number.parseInt(args[++i] ?? "", 10);
|
|
6162
|
+
} else if (arg === "--profile") {
|
|
6163
|
+
const profile = args[++i];
|
|
6164
|
+
if (profile === "automation" || profile === "realtime" || profile === "voice" || profile === "auth") {
|
|
6165
|
+
options.profile = profile;
|
|
6166
|
+
}
|
|
6167
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
6168
|
+
options.output = args[++i];
|
|
6169
|
+
} else if (!arg.startsWith("-") && !options.subcommand && !nextIsArtifact) {
|
|
6170
|
+
if (isSubcommand(arg)) {
|
|
6171
|
+
options.subcommand = arg;
|
|
6172
|
+
nextIsArtifact = arg !== "capture";
|
|
6173
|
+
} else if (!options.artifactPath) {
|
|
6174
|
+
options.artifactPath = arg;
|
|
6175
|
+
}
|
|
6176
|
+
} else if (!arg.startsWith("-") && nextIsArtifact && !options.artifactPath) {
|
|
6177
|
+
options.artifactPath = arg;
|
|
6178
|
+
nextIsArtifact = false;
|
|
5101
6179
|
}
|
|
5102
6180
|
}
|
|
5103
6181
|
return options;
|
|
5104
6182
|
}
|
|
5105
|
-
|
|
6183
|
+
function isSubcommand(value) {
|
|
6184
|
+
return value === "capture" || value === "inspect" || value === "summary" || value === "derive" || value === "export";
|
|
6185
|
+
}
|
|
6186
|
+
async function resolveConnection3(sessionId, useLatestSession, debug) {
|
|
5106
6187
|
if (sessionId) {
|
|
5107
6188
|
const session2 = await loadSession(sessionId);
|
|
5108
6189
|
const browser2 = await connect({
|
|
5109
6190
|
provider: session2.provider,
|
|
5110
6191
|
wsUrl: session2.wsUrl,
|
|
5111
|
-
debug
|
|
6192
|
+
debug
|
|
5112
6193
|
});
|
|
5113
6194
|
return { browser: browser2, session: session2, isNewSession: false };
|
|
5114
6195
|
}
|
|
5115
6196
|
if (useLatestSession) {
|
|
5116
6197
|
const session2 = await getDefaultSession();
|
|
5117
6198
|
if (!session2) {
|
|
5118
|
-
throw new Error(
|
|
5119
|
-
'No sessions found. Run "bp connect" first or use "bp record" to auto-connect.'
|
|
5120
|
-
);
|
|
6199
|
+
throw new Error('No sessions found. Run "bp connect" first or omit -s to auto-connect.');
|
|
5121
6200
|
}
|
|
5122
6201
|
const browser2 = await connect({
|
|
5123
6202
|
provider: session2.provider,
|
|
5124
6203
|
wsUrl: session2.wsUrl,
|
|
5125
|
-
debug
|
|
6204
|
+
debug
|
|
5126
6205
|
});
|
|
5127
6206
|
return { browser: browser2, session: session2, isNewSession: false };
|
|
5128
6207
|
}
|
|
5129
6208
|
let wsUrl;
|
|
5130
6209
|
try {
|
|
5131
|
-
wsUrl = await
|
|
5132
|
-
} catch {
|
|
6210
|
+
wsUrl = (await resolveCLIEndpoint()).wsUrl;
|
|
6211
|
+
} catch (error) {
|
|
5133
6212
|
throw new Error(
|
|
5134
|
-
|
|
6213
|
+
formatBrowserDiscoveryError(error, {
|
|
6214
|
+
explicitHint: " - Create a session first: bp connect --browser-url <ws-url>",
|
|
6215
|
+
reuseSessionHint: "bp record -s <session-id>",
|
|
6216
|
+
latestSessionHint: "bp record -s"
|
|
6217
|
+
})
|
|
5135
6218
|
);
|
|
5136
6219
|
}
|
|
5137
6220
|
const browser = await connect({
|
|
5138
6221
|
provider: "generic",
|
|
5139
6222
|
wsUrl,
|
|
5140
|
-
debug
|
|
6223
|
+
debug
|
|
5141
6224
|
});
|
|
5142
6225
|
const page = await browser.page();
|
|
5143
6226
|
const currentUrl = await page.url();
|
|
5144
|
-
const newSessionId = generateSessionId();
|
|
5145
6227
|
const session = {
|
|
5146
|
-
id:
|
|
6228
|
+
id: generateSessionId(),
|
|
5147
6229
|
provider: "generic",
|
|
5148
6230
|
wsUrl: browser.wsUrl,
|
|
5149
6231
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5150
6232
|
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5151
6233
|
currentUrl
|
|
5152
6234
|
};
|
|
5153
|
-
await saveSession(session);
|
|
5154
|
-
return { browser, session, isNewSession: true };
|
|
6235
|
+
await saveSession(session);
|
|
6236
|
+
return { browser, session, isNewSession: true };
|
|
6237
|
+
}
|
|
6238
|
+
function artifactSessionDir(sessionId) {
|
|
6239
|
+
return join9(homedir6(), ".browser-pilot", "sessions", sessionId);
|
|
6240
|
+
}
|
|
6241
|
+
function resolveArtifactPath(explicit, session) {
|
|
6242
|
+
if (explicit) {
|
|
6243
|
+
return resolve5(explicit);
|
|
6244
|
+
}
|
|
6245
|
+
if (session) {
|
|
6246
|
+
return join9(artifactSessionDir(session.id), DEFAULT_ARTIFACT);
|
|
6247
|
+
}
|
|
6248
|
+
return resolve5(DEFAULT_ARTIFACT);
|
|
6249
|
+
}
|
|
6250
|
+
function normalizeProfile(profile) {
|
|
6251
|
+
return profile ?? "automation";
|
|
6252
|
+
}
|
|
6253
|
+
function tipsForArtifact(path) {
|
|
6254
|
+
return {
|
|
6255
|
+
tip: {
|
|
6256
|
+
reason: "summary_first",
|
|
6257
|
+
command: `bp record summary ${path}`
|
|
6258
|
+
},
|
|
6259
|
+
alternateTips: [
|
|
6260
|
+
{
|
|
6261
|
+
reason: "derive_replayable_steps",
|
|
6262
|
+
command: `bp record derive ${path} -o workflow.json`
|
|
6263
|
+
},
|
|
6264
|
+
{
|
|
6265
|
+
reason: "inspect_trace_views",
|
|
6266
|
+
command: `bp trace summary ${path} --view ws`
|
|
6267
|
+
}
|
|
6268
|
+
]
|
|
6269
|
+
};
|
|
5155
6270
|
}
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
6271
|
+
function buildSummary(artifact, source) {
|
|
6272
|
+
return {
|
|
6273
|
+
source,
|
|
6274
|
+
version: artifact.version,
|
|
6275
|
+
session: artifact.session,
|
|
6276
|
+
counts: {
|
|
6277
|
+
steps: artifact.recipe.steps.length,
|
|
6278
|
+
actions: artifact.actions.length,
|
|
6279
|
+
screenshots: artifact.screenshots.length,
|
|
6280
|
+
traceEvents: artifact.trace.events.length,
|
|
6281
|
+
assertions: artifact.assertions.length
|
|
6282
|
+
},
|
|
6283
|
+
trace: artifact.trace.summaries,
|
|
6284
|
+
tips: tipsForArtifact(source)
|
|
6285
|
+
};
|
|
6286
|
+
}
|
|
6287
|
+
function deriveAssertions(artifact) {
|
|
6288
|
+
const assertions = [];
|
|
6289
|
+
if (artifact.session.endUrl) {
|
|
6290
|
+
assertions.push({ action: "assertUrl", expect: artifact.session.endUrl });
|
|
6291
|
+
}
|
|
6292
|
+
for (const action of artifact.actions) {
|
|
6293
|
+
if (action.action === "fill" && action.selector && typeof action.value === "string") {
|
|
6294
|
+
assertions.push({
|
|
6295
|
+
action: "assertValue",
|
|
6296
|
+
selector: action.selector,
|
|
6297
|
+
expect: action.value
|
|
6298
|
+
});
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
return assertions;
|
|
6302
|
+
}
|
|
6303
|
+
async function loadArtifact(pathOrFallback) {
|
|
6304
|
+
if (!existsSync6(pathOrFallback)) {
|
|
6305
|
+
throw new Error(`Artifact not found: ${pathOrFallback}`);
|
|
5161
6306
|
}
|
|
5162
|
-
const
|
|
6307
|
+
const raw = JSON.parse(nodeFs2.readFileSync(pathOrFallback, "utf-8"));
|
|
6308
|
+
return { path: pathOrFallback, artifact: canonicalizeRecordingArtifact(raw) };
|
|
6309
|
+
}
|
|
6310
|
+
function artifactToFrames(artifact) {
|
|
6311
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
6312
|
+
return artifact.actions.map((action, index) => {
|
|
6313
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
6314
|
+
return {
|
|
6315
|
+
seq: index + 1,
|
|
6316
|
+
timestamp: Date.parse(action.ts),
|
|
6317
|
+
action: action.action,
|
|
6318
|
+
selector: action.selector,
|
|
6319
|
+
selectorUsed: action.selectorUsed,
|
|
6320
|
+
value: action.value,
|
|
6321
|
+
url: action.url,
|
|
6322
|
+
coordinates: action.coordinates,
|
|
6323
|
+
boundingBox: action.boundingBox,
|
|
6324
|
+
success: action.success,
|
|
6325
|
+
durationMs: action.durationMs,
|
|
6326
|
+
error: action.error,
|
|
6327
|
+
screenshot: screenshot?.file ?? "",
|
|
6328
|
+
pageUrl: action.pageUrl,
|
|
6329
|
+
pageTitle: action.pageTitle,
|
|
6330
|
+
stepIndex: action.stepIndex,
|
|
6331
|
+
actionId: action.id
|
|
6332
|
+
};
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
6335
|
+
async function runRecordCapture(args, globalOptions) {
|
|
6336
|
+
const profile = normalizeProfile(args.profile);
|
|
5163
6337
|
const { browser, session, isNewSession } = await resolveConnection3(
|
|
5164
6338
|
globalOptions.session,
|
|
5165
|
-
|
|
6339
|
+
args.useLatestSession ?? false,
|
|
5166
6340
|
globalOptions.trace ?? false
|
|
5167
6341
|
);
|
|
5168
6342
|
if (isNewSession) {
|
|
5169
6343
|
console.log(`Created new session: ${session.id}`);
|
|
5170
6344
|
}
|
|
5171
6345
|
if (!session.metadata?.record) {
|
|
5172
|
-
const
|
|
5173
|
-
await updateSession(session.id, { metadata: { record:
|
|
6346
|
+
const recordSettings2 = {};
|
|
6347
|
+
await updateSession(session.id, { metadata: { record: recordSettings2 } });
|
|
5174
6348
|
}
|
|
5175
|
-
const page = await browser.page();
|
|
6349
|
+
const page = await browser.page(void 0, { targetId: session.targetId });
|
|
5176
6350
|
const cdp = page.cdpClient;
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
match: options.match,
|
|
5182
|
-
captureResponseBodies: options.bodies,
|
|
5183
|
-
maxPayload: options.maxPayload
|
|
5184
|
-
};
|
|
5185
|
-
listenConfig = listenOpts;
|
|
5186
|
-
}
|
|
5187
|
-
const sessionDir = join8(homedir5(), ".browser-pilot", "sessions", session.id);
|
|
5188
|
-
const screenshotDir = join8(sessionDir, "screenshots");
|
|
5189
|
-
const manifestPath = join8(sessionDir, "recording.json");
|
|
6351
|
+
const sessionDir = artifactSessionDir(session.id);
|
|
6352
|
+
const screenshotDir = join9(sessionDir, "screenshots");
|
|
6353
|
+
const canonicalPath = join9(sessionDir, DEFAULT_ARTIFACT);
|
|
6354
|
+
const outputPath = resolve5(args.file ?? DEFAULT_ARTIFACT);
|
|
5190
6355
|
nodeFs2.mkdirSync(screenshotDir, { recursive: true });
|
|
5191
|
-
const
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
6356
|
+
const existingArtifact = existsSync6(canonicalPath) ? canonicalizeRecordingArtifact(
|
|
6357
|
+
JSON.parse(nodeFs2.readFileSync(canonicalPath, "utf-8"))
|
|
6358
|
+
) : null;
|
|
6359
|
+
const recordingFrames = existingArtifact ? artifactToFrames(existingArtifact) : [];
|
|
6360
|
+
let listenConfig = {
|
|
6361
|
+
mode: typeof args.listen === "string" ? args.listen : "all",
|
|
6362
|
+
match: args.match,
|
|
6363
|
+
captureResponseBodies: Boolean(args.bodies),
|
|
6364
|
+
maxPayload: args.maxPayload
|
|
6365
|
+
};
|
|
6366
|
+
if (args.listen === false) {
|
|
6367
|
+
listenConfig = void 0;
|
|
6368
|
+
}
|
|
6369
|
+
if (!args.listen && profile === "voice") {
|
|
6370
|
+
listenConfig = {
|
|
6371
|
+
mode: "all",
|
|
6372
|
+
match: args.match,
|
|
6373
|
+
captureResponseBodies: Boolean(args.bodies),
|
|
6374
|
+
maxPayload: args.maxPayload
|
|
6375
|
+
};
|
|
5202
6376
|
}
|
|
5203
|
-
const
|
|
5204
|
-
const
|
|
6377
|
+
const recordSettings = session.metadata?.record;
|
|
6378
|
+
const recordFormat = recordSettings?.format ?? "webp";
|
|
6379
|
+
const recordQuality = recordSettings?.quality ?? 40;
|
|
5205
6380
|
let screenshotCount = 0;
|
|
5206
|
-
function eventKindLabel(kind) {
|
|
5207
|
-
switch (kind) {
|
|
5208
|
-
case "click":
|
|
5209
|
-
case "dblclick":
|
|
5210
|
-
return "click";
|
|
5211
|
-
case "input":
|
|
5212
|
-
case "change":
|
|
5213
|
-
return "fill";
|
|
5214
|
-
case "submit":
|
|
5215
|
-
return "submit";
|
|
5216
|
-
case "keydown":
|
|
5217
|
-
return "press";
|
|
5218
|
-
case "navigation":
|
|
5219
|
-
return "goto";
|
|
5220
|
-
default:
|
|
5221
|
-
return kind;
|
|
5222
|
-
}
|
|
5223
|
-
}
|
|
5224
6381
|
async function captureScreenshotForEvent(event) {
|
|
5225
6382
|
try {
|
|
5226
6383
|
const ts = Date.now();
|
|
5227
6384
|
const seq = String(recordingFrames.length + 1).padStart(4, "0");
|
|
5228
6385
|
const label = eventKindLabel(event.kind);
|
|
5229
6386
|
const filename = `${seq}-${ts}-${label}.${recordFormat}`;
|
|
5230
|
-
const filepath =
|
|
6387
|
+
const filepath = join9(screenshotDir, filename);
|
|
5231
6388
|
const result = await cdp.send("Page.captureScreenshot", {
|
|
5232
6389
|
format: recordFormat === "png" ? "png" : recordFormat === "jpeg" ? "jpeg" : "webp",
|
|
5233
6390
|
quality: recordFormat === "png" ? void 0 : recordQuality
|
|
5234
6391
|
});
|
|
5235
|
-
|
|
5236
|
-
nodeFs2.writeFileSync(filepath, buffer);
|
|
6392
|
+
nodeFs2.writeFileSync(filepath, Buffer.from(result.data, "base64"));
|
|
5237
6393
|
let pageUrl;
|
|
5238
6394
|
let pageTitle;
|
|
5239
6395
|
try {
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
returnByValue: true
|
|
5243
|
-
});
|
|
5244
|
-
pageUrl = urlResult.result.value;
|
|
5245
|
-
const titleResult = await cdp.send("Runtime.evaluate", {
|
|
5246
|
-
expression: "document.title",
|
|
5247
|
-
returnByValue: true
|
|
5248
|
-
});
|
|
5249
|
-
pageTitle = titleResult.result.value;
|
|
6396
|
+
pageUrl = await page.url();
|
|
6397
|
+
pageTitle = await page.title();
|
|
5250
6398
|
} catch {
|
|
5251
6399
|
}
|
|
5252
6400
|
const targetMetadata = event.element ? {
|
|
5253
|
-
tagName: event.element
|
|
5254
|
-
inputType: event.element
|
|
6401
|
+
tagName: event.element["tag"],
|
|
6402
|
+
inputType: event.element["type"] ?? void 0
|
|
5255
6403
|
} : void 0;
|
|
5256
|
-
|
|
6404
|
+
recordingFrames.push({
|
|
5257
6405
|
seq: recordingFrames.length + 1,
|
|
5258
6406
|
timestamp: ts,
|
|
5259
6407
|
action: label,
|
|
5260
6408
|
selector: event.selectors?.[0]?.selector,
|
|
6409
|
+
selectorUsed: event.selectors?.[0]?.selector,
|
|
5261
6410
|
value: redactValueForRecording(event.value, targetMetadata),
|
|
5262
6411
|
coordinates: event.client,
|
|
5263
6412
|
success: true,
|
|
5264
6413
|
durationMs: 0,
|
|
5265
6414
|
screenshot: `screenshots/${filename}`,
|
|
5266
6415
|
pageUrl,
|
|
5267
|
-
pageTitle
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
6416
|
+
pageTitle,
|
|
6417
|
+
stepIndex: recordingFrames.length,
|
|
6418
|
+
actionId: `action-${recordingFrames.length + 1}`
|
|
6419
|
+
});
|
|
6420
|
+
screenshotCount += 1;
|
|
5271
6421
|
} catch {
|
|
5272
6422
|
}
|
|
5273
6423
|
}
|
|
@@ -5277,108 +6427,194 @@ async function recordCommand(args, globalOptions) {
|
|
|
5277
6427
|
};
|
|
5278
6428
|
const recorder = new Recorder(cdp, recorderOptions);
|
|
5279
6429
|
let stopping = false;
|
|
5280
|
-
async
|
|
5281
|
-
if (stopping)
|
|
6430
|
+
const stopAndSave = async () => {
|
|
6431
|
+
if (stopping) {
|
|
6432
|
+
return;
|
|
6433
|
+
}
|
|
5282
6434
|
stopping = true;
|
|
5283
6435
|
try {
|
|
5284
6436
|
const recording = await recorder.stop();
|
|
5285
|
-
const
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
let viewport = { width: 1280, height: 720 };
|
|
5289
|
-
try {
|
|
5290
|
-
const urlResult = await cdp.send("Runtime.evaluate", {
|
|
5291
|
-
expression: "location.href",
|
|
5292
|
-
returnByValue: true
|
|
5293
|
-
});
|
|
5294
|
-
currentUrl = urlResult.result.value;
|
|
5295
|
-
} catch {
|
|
5296
|
-
}
|
|
5297
|
-
try {
|
|
5298
|
-
const metrics = await cdp.send("Page.getLayoutMetrics");
|
|
5299
|
-
viewport = {
|
|
5300
|
-
width: metrics.cssVisualViewport.clientWidth,
|
|
5301
|
-
height: metrics.cssVisualViewport.clientHeight
|
|
5302
|
-
};
|
|
5303
|
-
} catch {
|
|
5304
|
-
}
|
|
5305
|
-
const manifest = {
|
|
5306
|
-
version: 1,
|
|
5307
|
-
recordedAt: manifestRecordedAt,
|
|
6437
|
+
const currentUrl = await page.url().catch(() => recording.startUrl);
|
|
6438
|
+
const manifest = createRecordingManifest({
|
|
6439
|
+
recordedAt: existingArtifact?.recordedAt ?? recording.recordedAt,
|
|
5308
6440
|
sessionId: session.id,
|
|
5309
|
-
startUrl:
|
|
6441
|
+
startUrl: existingArtifact?.session.startUrl ?? recording.startUrl,
|
|
5310
6442
|
endUrl: currentUrl,
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
6443
|
+
targetId: page.targetId,
|
|
6444
|
+
profile,
|
|
6445
|
+
steps: recording.steps,
|
|
6446
|
+
frames: recordingFrames,
|
|
6447
|
+
traceEvents: recording.traceEvents ?? [],
|
|
6448
|
+
assertions: deriveAssertions(
|
|
6449
|
+
createRecordingManifest({
|
|
6450
|
+
recordedAt: recording.recordedAt,
|
|
6451
|
+
sessionId: session.id,
|
|
6452
|
+
startUrl: recording.startUrl,
|
|
6453
|
+
endUrl: currentUrl,
|
|
6454
|
+
targetId: page.targetId,
|
|
6455
|
+
profile,
|
|
6456
|
+
steps: recording.steps,
|
|
6457
|
+
frames: recordingFrames,
|
|
6458
|
+
traceEvents: recording.traceEvents ?? []
|
|
6459
|
+
})
|
|
6460
|
+
),
|
|
6461
|
+
notes: profile === "voice" ? ["Voice profile capture"] : [],
|
|
6462
|
+
recordingManifest: DEFAULT_ARTIFACT,
|
|
6463
|
+
screenshotDir: "screenshots/"
|
|
6464
|
+
});
|
|
6465
|
+
nodeFs2.writeFileSync(canonicalPath, JSON.stringify(manifest, null, 2));
|
|
6466
|
+
if (outputPath !== canonicalPath) {
|
|
6467
|
+
nodeFs2.mkdirSync(dirname7(outputPath), { recursive: true });
|
|
6468
|
+
nodeFs2.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
|
|
5331
6469
|
}
|
|
6470
|
+
await updateSession(session.id, { currentUrl });
|
|
6471
|
+
await browser.disconnect();
|
|
6472
|
+
const summary = buildSummary(manifest, outputPath);
|
|
5332
6473
|
if (globalOptions.format === "json") {
|
|
5333
|
-
output(
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
steps: recording.steps.length,
|
|
5338
|
-
screenshots: screenshotCount,
|
|
5339
|
-
duration: recording.duration,
|
|
5340
|
-
networkRequests: recording.network?.requests.length ?? 0,
|
|
5341
|
-
wsFrames: recording.websockets?.frames.length ?? 0,
|
|
5342
|
-
timelineEntries: recording.timeline?.length ?? 0
|
|
5343
|
-
},
|
|
5344
|
-
"json"
|
|
6474
|
+
output({ success: true, ...summary }, "json");
|
|
6475
|
+
} else {
|
|
6476
|
+
console.log(
|
|
6477
|
+
`Saved ${manifest.recipe.steps.length} steps, ${screenshotCount} screenshots, ${manifest.trace.events.length} trace events to ${outputPath}`
|
|
5345
6478
|
);
|
|
6479
|
+
console.log(`Use: bp record summary ${outputPath}`);
|
|
5346
6480
|
}
|
|
5347
|
-
process.exit(0);
|
|
5348
6481
|
} catch (error) {
|
|
5349
|
-
console.error(
|
|
6482
|
+
console.error(
|
|
6483
|
+
`Error saving recording: ${error instanceof Error ? error.message : String(error)}`
|
|
6484
|
+
);
|
|
5350
6485
|
process.exit(1);
|
|
5351
6486
|
}
|
|
5352
|
-
}
|
|
6487
|
+
};
|
|
5353
6488
|
const handleSignal = () => {
|
|
5354
|
-
stopAndSave()
|
|
5355
|
-
console.error("Error during shutdown:", err);
|
|
5356
|
-
process.exit(1);
|
|
5357
|
-
});
|
|
6489
|
+
void stopAndSave();
|
|
5358
6490
|
};
|
|
5359
6491
|
process.on("SIGINT", handleSignal);
|
|
5360
6492
|
process.on("SIGTERM", handleSignal);
|
|
5361
|
-
if (
|
|
5362
|
-
setTimeout(() =>
|
|
5363
|
-
void stopAndSave();
|
|
5364
|
-
}, options.timeout);
|
|
6493
|
+
if (args.timeout && args.timeout > 0) {
|
|
6494
|
+
setTimeout(() => void stopAndSave(), args.timeout);
|
|
5365
6495
|
}
|
|
5366
6496
|
await recorder.start();
|
|
5367
|
-
console.log(`Recording... Press Ctrl+C to stop
|
|
5368
|
-
if (options.listen) {
|
|
5369
|
-
const listenMode = typeof options.listen === "string" ? options.listen : "all";
|
|
5370
|
-
const matchLabel = options.match ? ` matching "${options.match}"` : "";
|
|
5371
|
-
console.log(`Network capture: ${listenMode} traffic${matchLabel}`);
|
|
5372
|
-
}
|
|
6497
|
+
console.log(`Recording... Press Ctrl+C to stop. Artifact: ${outputPath}`);
|
|
5373
6498
|
console.log(`Session: ${session.id}`);
|
|
6499
|
+
console.log(`Profile: ${profile}`);
|
|
5374
6500
|
console.log(`URL: ${await page.url()}`);
|
|
5375
6501
|
}
|
|
6502
|
+
async function runRecordInspect(pathHint, globalOptions) {
|
|
6503
|
+
const session = globalOptions.session ? await loadSession(globalOptions.session) : await getDefaultSession();
|
|
6504
|
+
const artifactPath = resolveArtifactPath(pathHint, session ?? void 0);
|
|
6505
|
+
const { path, artifact } = await loadArtifact(artifactPath);
|
|
6506
|
+
output(buildSummary(artifact, path), globalOptions.format ?? "pretty");
|
|
6507
|
+
}
|
|
6508
|
+
async function runRecordSummary(pathHint, globalOptions) {
|
|
6509
|
+
const session = globalOptions.session ? await loadSession(globalOptions.session) : await getDefaultSession();
|
|
6510
|
+
const artifactPath = resolveArtifactPath(pathHint, session ?? void 0);
|
|
6511
|
+
const { path, artifact } = await loadArtifact(artifactPath);
|
|
6512
|
+
const summary = buildSummary(artifact, path);
|
|
6513
|
+
summary.trace = buildTraceSummaries(artifact.trace.events);
|
|
6514
|
+
output(summary, globalOptions.format ?? "pretty");
|
|
6515
|
+
}
|
|
6516
|
+
async function runRecordDerive(pathHint, outputPath, globalOptions) {
|
|
6517
|
+
if (!outputPath) {
|
|
6518
|
+
throw new Error("record derive requires -o <workflow.json>");
|
|
6519
|
+
}
|
|
6520
|
+
const session = globalOptions.session ? await loadSession(globalOptions.session) : await getDefaultSession();
|
|
6521
|
+
const artifactPath = resolveArtifactPath(pathHint, session ?? void 0);
|
|
6522
|
+
const { artifact } = await loadArtifact(artifactPath);
|
|
6523
|
+
const steps = artifact.recipe.steps;
|
|
6524
|
+
nodeFs2.mkdirSync(dirname7(resolve5(outputPath)), { recursive: true });
|
|
6525
|
+
nodeFs2.writeFileSync(outputPath, JSON.stringify(steps, null, 2));
|
|
6526
|
+
output(
|
|
6527
|
+
{
|
|
6528
|
+
success: true,
|
|
6529
|
+
output: outputPath,
|
|
6530
|
+
steps: steps.length,
|
|
6531
|
+
suggestedAssertions: deriveAssertions(artifact)
|
|
6532
|
+
},
|
|
6533
|
+
globalOptions.format ?? "pretty"
|
|
6534
|
+
);
|
|
6535
|
+
}
|
|
6536
|
+
async function runRecordExport(pathHint, outputPath, globalOptions) {
|
|
6537
|
+
if (!outputPath) {
|
|
6538
|
+
throw new Error("record export requires -o <bundle.json>");
|
|
6539
|
+
}
|
|
6540
|
+
const session = globalOptions.session ? await loadSession(globalOptions.session) : await getDefaultSession();
|
|
6541
|
+
const artifactPath = resolveArtifactPath(pathHint, session ?? void 0);
|
|
6542
|
+
const { artifact, path } = await loadArtifact(artifactPath);
|
|
6543
|
+
const bundle = {
|
|
6544
|
+
source: path,
|
|
6545
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6546
|
+
artifact,
|
|
6547
|
+
summary: buildSummary(artifact, path)
|
|
6548
|
+
};
|
|
6549
|
+
nodeFs2.mkdirSync(dirname7(resolve5(outputPath)), { recursive: true });
|
|
6550
|
+
nodeFs2.writeFileSync(outputPath, JSON.stringify(bundle, null, 2));
|
|
6551
|
+
output({ success: true, output: outputPath }, globalOptions.format ?? "pretty");
|
|
6552
|
+
}
|
|
6553
|
+
function eventKindLabel(kind) {
|
|
6554
|
+
switch (kind) {
|
|
6555
|
+
case "click":
|
|
6556
|
+
case "dblclick":
|
|
6557
|
+
return "click";
|
|
6558
|
+
case "input":
|
|
6559
|
+
case "change":
|
|
6560
|
+
return "fill";
|
|
6561
|
+
case "submit":
|
|
6562
|
+
return "submit";
|
|
6563
|
+
case "keydown":
|
|
6564
|
+
return "press";
|
|
6565
|
+
case "navigation":
|
|
6566
|
+
return "goto";
|
|
6567
|
+
default:
|
|
6568
|
+
return kind;
|
|
6569
|
+
}
|
|
6570
|
+
}
|
|
6571
|
+
async function recordCommand(args, globalOptions) {
|
|
6572
|
+
const options = parseRecordArgs(args);
|
|
6573
|
+
const command = options.subcommand ?? "capture";
|
|
6574
|
+
if (options.help || globalOptions.help) {
|
|
6575
|
+
console.log(RECORD_HELP);
|
|
6576
|
+
return;
|
|
6577
|
+
}
|
|
6578
|
+
if (command === "capture") {
|
|
6579
|
+
await runRecordCapture(options, globalOptions);
|
|
6580
|
+
return;
|
|
6581
|
+
}
|
|
6582
|
+
const pathHint = options.artifactPath ?? DEFAULT_ARTIFACT;
|
|
6583
|
+
switch (command) {
|
|
6584
|
+
case "inspect":
|
|
6585
|
+
await runRecordInspect(pathHint, globalOptions);
|
|
6586
|
+
break;
|
|
6587
|
+
case "summary":
|
|
6588
|
+
await runRecordSummary(pathHint, globalOptions);
|
|
6589
|
+
break;
|
|
6590
|
+
case "derive":
|
|
6591
|
+
await runRecordDerive(pathHint, options.output, globalOptions);
|
|
6592
|
+
break;
|
|
6593
|
+
case "export":
|
|
6594
|
+
await runRecordExport(pathHint, options.output, globalOptions);
|
|
6595
|
+
break;
|
|
6596
|
+
default:
|
|
6597
|
+
throw new Error(`Unknown record subcommand: ${command}`);
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
5376
6600
|
|
|
5377
6601
|
// src/cli/commands/run.ts
|
|
5378
6602
|
import { readFile } from "fs/promises";
|
|
5379
|
-
import { resolve as
|
|
6603
|
+
import { resolve as resolve6 } from "path";
|
|
5380
6604
|
var RUN_HELP = `
|
|
5381
|
-
bp run - Execute a workflow file
|
|
6605
|
+
bp run - Execute a saved workflow file
|
|
6606
|
+
|
|
6607
|
+
When to use:
|
|
6608
|
+
You already have reusable steps from \`bp record derive\` or a hand-authored workflow.
|
|
6609
|
+
|
|
6610
|
+
When not to use:
|
|
6611
|
+
You are exploring a page inline or debugging one interaction. Use \`bp exec\`.
|
|
6612
|
+
|
|
6613
|
+
Default flow:
|
|
6614
|
+
derive or author workflow -> run -> inspect failures -> harden with assertions
|
|
6615
|
+
|
|
6616
|
+
Common mistake:
|
|
6617
|
+
Treating \`run\` as discovery. It is for repeatable execution, not first-pass exploration.
|
|
5382
6618
|
|
|
5383
6619
|
Usage:
|
|
5384
6620
|
bp run <workflow.json> [options]
|
|
@@ -5392,7 +6628,7 @@ Options:
|
|
|
5392
6628
|
--timeout <ms> Default timeout for all steps (ms)
|
|
5393
6629
|
-s, --session <id> Session to use (default: most recent)
|
|
5394
6630
|
--json Output results as JSON
|
|
5395
|
-
--
|
|
6631
|
+
--debug Enable CDP transport debugging (global option)
|
|
5396
6632
|
-h, --help Show this help
|
|
5397
6633
|
|
|
5398
6634
|
Examples:
|
|
@@ -5400,7 +6636,9 @@ Examples:
|
|
|
5400
6636
|
bp run checkout.json --on-fail continue --json
|
|
5401
6637
|
bp run smoke-test.json --timeout 10000
|
|
5402
6638
|
|
|
5403
|
-
|
|
6639
|
+
Likely next commands:
|
|
6640
|
+
bp trace summary -s <session> --view console
|
|
6641
|
+
bp exec --record -f workflow.json
|
|
5404
6642
|
`.trimEnd();
|
|
5405
6643
|
function parseRunArgs(args) {
|
|
5406
6644
|
const options = {};
|
|
@@ -5438,7 +6676,7 @@ async function runCommand(args, globalOptions) {
|
|
|
5438
6676
|
"No workflow file provided. Usage: bp run <workflow.json>\n\nRun 'bp run --help' for details."
|
|
5439
6677
|
);
|
|
5440
6678
|
}
|
|
5441
|
-
const absPath =
|
|
6679
|
+
const absPath = resolve6(workflowPath);
|
|
5442
6680
|
let raw;
|
|
5443
6681
|
try {
|
|
5444
6682
|
raw = await readFile(absPath, "utf-8");
|
|
@@ -5590,7 +6828,7 @@ async function screenshotCommand(args, globalOptions) {
|
|
|
5590
6828
|
}
|
|
5591
6829
|
|
|
5592
6830
|
// src/cli/commands/snapshot.ts
|
|
5593
|
-
import * as
|
|
6831
|
+
import * as fs7 from "fs";
|
|
5594
6832
|
|
|
5595
6833
|
// src/browser/overlay.ts
|
|
5596
6834
|
var OVERLAY_SCRIPT = `(function() {
|
|
@@ -5841,7 +7079,19 @@ function formatDiffPretty(diff) {
|
|
|
5841
7079
|
|
|
5842
7080
|
// src/cli/commands/snapshot.ts
|
|
5843
7081
|
var SNAPSHOT_HELP = `
|
|
5844
|
-
bp snapshot -
|
|
7082
|
+
bp snapshot - Inspect the page and collect refs
|
|
7083
|
+
|
|
7084
|
+
When to use:
|
|
7085
|
+
You need to understand the current page or choose reliable targets for \`bp exec\`.
|
|
7086
|
+
|
|
7087
|
+
When not to use:
|
|
7088
|
+
You need long-running behavior, network, or voice causality. Use \`bp trace\`.
|
|
7089
|
+
|
|
7090
|
+
Default flow:
|
|
7091
|
+
snapshot -i -> use ref:eN selectors in exec -> take a fresh snapshot after navigation
|
|
7092
|
+
|
|
7093
|
+
Common mistake:
|
|
7094
|
+
Reusing refs after the page navigated or materially changed.
|
|
5845
7095
|
|
|
5846
7096
|
Usage:
|
|
5847
7097
|
bp snapshot [options]
|
|
@@ -5857,16 +7107,21 @@ Options:
|
|
|
5857
7107
|
-s, --session <id> Session to use (default: most recent)
|
|
5858
7108
|
-f, --format <fmt> Output format: json | pretty (default: pretty)
|
|
5859
7109
|
--json Alias for -f json
|
|
5860
|
-
--
|
|
7110
|
+
--debug Enable CDP transport debugging (global option)
|
|
5861
7111
|
-h, --help Show this help
|
|
5862
7112
|
|
|
5863
7113
|
Examples:
|
|
5864
7114
|
bp snapshot # Full accessibility tree as readable text
|
|
5865
|
-
bp snapshot -i # Interactive elements only
|
|
7115
|
+
bp snapshot -i # Interactive elements only; best default for automation
|
|
5866
7116
|
bp snapshot --role radio,checkbox # Focus on specific control roles
|
|
5867
7117
|
bp snapshot --json > page.json # Save full snapshot to file
|
|
5868
7118
|
bp snapshot --diff before.json # Show what changed since before.json
|
|
5869
7119
|
bp snapshot --inspect # Visual ref labels on the page
|
|
7120
|
+
|
|
7121
|
+
Likely next commands:
|
|
7122
|
+
bp exec '[{"action":"click","selector":"ref:e4"}]'
|
|
7123
|
+
bp page
|
|
7124
|
+
bp diagnose "ref:e4"
|
|
5870
7125
|
`.trimEnd();
|
|
5871
7126
|
function parseSnapshotArgs(args) {
|
|
5872
7127
|
const options = {
|
|
@@ -5902,7 +7157,7 @@ function writeInfo(message, asStderr = false) {
|
|
|
5902
7157
|
`);
|
|
5903
7158
|
}
|
|
5904
7159
|
function sleep2(ms) {
|
|
5905
|
-
return new Promise((
|
|
7160
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
5906
7161
|
}
|
|
5907
7162
|
async function snapshotCommand(args, globalOptions) {
|
|
5908
7163
|
const options = parseSnapshotArgs(args);
|
|
@@ -5939,10 +7194,10 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
5939
7194
|
}
|
|
5940
7195
|
});
|
|
5941
7196
|
if (options.diffFile) {
|
|
5942
|
-
if (!
|
|
7197
|
+
if (!fs7.existsSync(options.diffFile)) {
|
|
5943
7198
|
throw new Error(`Diff file not found: ${options.diffFile}`);
|
|
5944
7199
|
}
|
|
5945
|
-
const beforeContent =
|
|
7200
|
+
const beforeContent = fs7.readFileSync(options.diffFile, "utf-8");
|
|
5946
7201
|
const parsedBefore = JSON.parse(beforeContent);
|
|
5947
7202
|
if (!isRecord(parsedBefore) || !Array.isArray(parsedBefore["accessibilityTree"])) {
|
|
5948
7203
|
throw new Error("Diff file is not a valid PageSnapshot JSON payload");
|
|
@@ -5950,7 +7205,7 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
5950
7205
|
const beforeSnapshot = parsedBefore;
|
|
5951
7206
|
const diff = diffSnapshots(beforeSnapshot, snapshot);
|
|
5952
7207
|
if (options.outputFile) {
|
|
5953
|
-
|
|
7208
|
+
fs7.writeFileSync(options.outputFile, renderOutput(diff, globalOptions.format));
|
|
5954
7209
|
writeInfo(`Wrote output to ${options.outputFile}`, true);
|
|
5955
7210
|
} else if (globalOptions.format === "json") {
|
|
5956
7211
|
output(diff, "json");
|
|
@@ -5990,7 +7245,7 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
5990
7245
|
}
|
|
5991
7246
|
}
|
|
5992
7247
|
if (options.outputFile) {
|
|
5993
|
-
|
|
7248
|
+
fs7.writeFileSync(options.outputFile, renderOutput(payload, globalOptions.format));
|
|
5994
7249
|
writeInfo(`Wrote output to ${options.outputFile}`, true);
|
|
5995
7250
|
} else {
|
|
5996
7251
|
output(payload, globalOptions.format);
|
|
@@ -6064,6 +7319,15 @@ async function targetsCommand(_args, globalOptions) {
|
|
|
6064
7319
|
var TEXT_HELP = `
|
|
6065
7320
|
bp text - Extract text content from the current page
|
|
6066
7321
|
|
|
7322
|
+
When to use:
|
|
7323
|
+
You need readable content for summarization, comparison, or assertions outside the action batch.
|
|
7324
|
+
|
|
7325
|
+
When not to use:
|
|
7326
|
+
You are choosing clickable or fillable targets. Use \`bp snapshot -i\` or \`bp page\`.
|
|
7327
|
+
|
|
7328
|
+
Common mistake:
|
|
7329
|
+
Expecting \`bp text\` to tell you what to click next. It is content-focused, not action-focused.
|
|
7330
|
+
|
|
6067
7331
|
Usage:
|
|
6068
7332
|
bp text [options]
|
|
6069
7333
|
|
|
@@ -6072,13 +7336,17 @@ Options:
|
|
|
6072
7336
|
-s, --session <id> Session to use (default: most recent)
|
|
6073
7337
|
-f, --format <fmt> Output format: json | pretty (default: pretty)
|
|
6074
7338
|
--json Alias for -f json
|
|
6075
|
-
--
|
|
7339
|
+
--debug Enable CDP transport debugging (global option)
|
|
6076
7340
|
-h, --help Show this help
|
|
6077
7341
|
|
|
6078
7342
|
Examples:
|
|
6079
7343
|
bp text # Extract all text from the page
|
|
6080
7344
|
bp text --selector '#main' # Extract text from #main element only
|
|
6081
7345
|
bp text --json # Output as JSON with URL and selector info
|
|
7346
|
+
|
|
7347
|
+
Likely next commands:
|
|
7348
|
+
bp snapshot -i
|
|
7349
|
+
bp exec '[{"action":"assertText","expect":"..."}]'
|
|
6082
7350
|
`.trimEnd();
|
|
6083
7351
|
function parseTextArgs(args) {
|
|
6084
7352
|
const options = {};
|
|
@@ -6127,56 +7395,404 @@ async function textCommand(args, globalOptions) {
|
|
|
6127
7395
|
}
|
|
6128
7396
|
}
|
|
6129
7397
|
|
|
7398
|
+
// src/cli/commands/trace.ts
|
|
7399
|
+
import * as fs8 from "fs";
|
|
7400
|
+
import { dirname as dirname8, resolve as resolve7 } from "path";
|
|
7401
|
+
var TRACE_HELP = `
|
|
7402
|
+
bp trace - Inspect, summarize, and watch behavior over time
|
|
7403
|
+
|
|
7404
|
+
When to use:
|
|
7405
|
+
The question spans time, causality, network, console, permissions, media, or voice state.
|
|
7406
|
+
|
|
7407
|
+
When not to use:
|
|
7408
|
+
You already know the target element and just need to click, fill, or assert DOM state once. Use \`bp exec\`.
|
|
7409
|
+
|
|
7410
|
+
Default flow:
|
|
7411
|
+
start or tail live capture -> summary by view -> watch if you need a durable assertion -> export evidence
|
|
7412
|
+
|
|
7413
|
+
Common mistake:
|
|
7414
|
+
Using \`trace\` as the primary action surface. It analyzes behavior; it does not replace \`exec\` or \`run\`.
|
|
7415
|
+
|
|
7416
|
+
Usage:
|
|
7417
|
+
bp trace <start|tail|summary|watch|export|merge> [artifact|trace.jsonl] [options]
|
|
7418
|
+
|
|
7419
|
+
Commands:
|
|
7420
|
+
start Capture live trace events into the session trace store
|
|
7421
|
+
tail Stream live canonical events (listen compatibility surface)
|
|
7422
|
+
summary Summarize a session trace or saved artifact
|
|
7423
|
+
watch Run a durable trace assertion over a live capture window
|
|
7424
|
+
export Export events + summary to JSON
|
|
7425
|
+
merge Merge multiple artifacts or trace files
|
|
7426
|
+
|
|
7427
|
+
Options:
|
|
7428
|
+
-s, --session [id] Session to use (omit: default session, -s: latest, -s <id>: specific)
|
|
7429
|
+
--view <name> ws | voice | console | permissions | media | ui | session
|
|
7430
|
+
-m, --match <glob> Filter HTTP/WS URLs for live capture
|
|
7431
|
+
-o, --output <path> Output file for start/export/merge
|
|
7432
|
+
--timeout <ms> Stop live capture or watch after timeout
|
|
7433
|
+
--max-payload <n> Max WebSocket payload preview length (default: 256)
|
|
7434
|
+
--assert <name> Watch assertion (profile:reconnect, no-console-errors)
|
|
7435
|
+
-q, --quiet Suppress status lines for live capture
|
|
7436
|
+
-h, --help Show help
|
|
7437
|
+
|
|
7438
|
+
Examples:
|
|
7439
|
+
bp trace start -s dev --timeout 30000
|
|
7440
|
+
bp trace tail ws -m "*realtime*"
|
|
7441
|
+
bp trace summary -s dev --view session
|
|
7442
|
+
bp trace summary recording.json --view ws
|
|
7443
|
+
bp trace watch -s dev --view ws --assert profile:reconnect --timeout 15000
|
|
7444
|
+
bp trace watch -s dev --view console --assert no-console-errors --timeout 5000
|
|
7445
|
+
bp trace export -s dev -o trace-bundle.json
|
|
7446
|
+
bp trace merge trace-a.jsonl trace-b.jsonl -o merged-trace.json
|
|
7447
|
+
|
|
7448
|
+
Likely next commands:
|
|
7449
|
+
bp trace summary -s dev --view ws
|
|
7450
|
+
bp trace summary -s dev --view voice
|
|
7451
|
+
bp trace export -s dev -o trace-bundle.json
|
|
7452
|
+
`.trim();
|
|
7453
|
+
function parseTraceArgs(args) {
|
|
7454
|
+
const options = { sources: [], view: "session" };
|
|
7455
|
+
for (let i = 0; i < args.length; i++) {
|
|
7456
|
+
const arg = args[i];
|
|
7457
|
+
if (!options.subcommand && (arg === "start" || arg === "tail" || arg === "summary" || arg === "watch" || arg === "export" || arg === "merge")) {
|
|
7458
|
+
options.subcommand = arg;
|
|
7459
|
+
continue;
|
|
7460
|
+
}
|
|
7461
|
+
if (!options.subcommand && (arg === "ws" || arg === "http" || arg === "all")) {
|
|
7462
|
+
options.subcommand = "tail";
|
|
7463
|
+
options.mode = arg;
|
|
7464
|
+
continue;
|
|
7465
|
+
}
|
|
7466
|
+
if (arg === "-o" || arg === "--output") {
|
|
7467
|
+
options.output = args[++i];
|
|
7468
|
+
} else if (arg === "--timeout") {
|
|
7469
|
+
options.timeout = Number.parseInt(args[++i] ?? "", 10);
|
|
7470
|
+
} else if (arg === "--view") {
|
|
7471
|
+
const view = args[++i];
|
|
7472
|
+
if (view === "ws" || view === "voice" || view === "console" || view === "permissions" || view === "media" || view === "ui" || view === "session") {
|
|
7473
|
+
options.view = view;
|
|
7474
|
+
}
|
|
7475
|
+
} else if (arg === "--assert") {
|
|
7476
|
+
options.assert = args[++i];
|
|
7477
|
+
} else if (arg === "-m" || arg === "--match") {
|
|
7478
|
+
options.match = args[++i];
|
|
7479
|
+
} else if (arg === "--max-payload") {
|
|
7480
|
+
options.maxPayload = Number.parseInt(args[++i] ?? "", 10);
|
|
7481
|
+
} else if (arg === "-q" || arg === "--quiet") {
|
|
7482
|
+
options.quiet = true;
|
|
7483
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
7484
|
+
options.help = true;
|
|
7485
|
+
} else if (arg === "-s" || arg === "--session") {
|
|
7486
|
+
const nextArg = args[i + 1];
|
|
7487
|
+
if (!nextArg || nextArg.startsWith("-")) {
|
|
7488
|
+
options.useLatestSession = true;
|
|
7489
|
+
}
|
|
7490
|
+
} else if (arg === "ws" || arg === "http" || arg === "all") {
|
|
7491
|
+
options.mode = arg;
|
|
7492
|
+
} else if (!arg.startsWith("-")) {
|
|
7493
|
+
if (!options.source) {
|
|
7494
|
+
options.source = arg;
|
|
7495
|
+
}
|
|
7496
|
+
options.sources.push(arg);
|
|
7497
|
+
}
|
|
7498
|
+
}
|
|
7499
|
+
options.subcommand ??= "summary";
|
|
7500
|
+
return options;
|
|
7501
|
+
}
|
|
7502
|
+
async function resolveTraceSource(sourceHint, sessionId) {
|
|
7503
|
+
if (sourceHint && fs8.existsSync(sourceHint)) {
|
|
7504
|
+
if (sourceHint.endsWith(".jsonl")) {
|
|
7505
|
+
return { path: resolve7(sourceHint), events: readTraceEvents(sourceHint) };
|
|
7506
|
+
}
|
|
7507
|
+
const raw = JSON.parse(fs8.readFileSync(sourceHint, "utf-8"));
|
|
7508
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
7509
|
+
return { path: resolve7(sourceHint), events: artifact.trace.events };
|
|
7510
|
+
}
|
|
7511
|
+
let session = null;
|
|
7512
|
+
if (sessionId) {
|
|
7513
|
+
session = await loadSession(sessionId);
|
|
7514
|
+
} else {
|
|
7515
|
+
session = await getDefaultSession();
|
|
7516
|
+
}
|
|
7517
|
+
if (!session) {
|
|
7518
|
+
throw new Error('No session found. Run "bp connect" first or pass an artifact path.');
|
|
7519
|
+
}
|
|
7520
|
+
const tracePath = getSessionTracePath(session.id);
|
|
7521
|
+
if (fs8.existsSync(tracePath)) {
|
|
7522
|
+
return { path: tracePath, events: readTraceEvents(tracePath) };
|
|
7523
|
+
}
|
|
7524
|
+
const artifactPath = resolve7(getSessionTracePath(session.id), "..", "recording.json");
|
|
7525
|
+
if (fs8.existsSync(artifactPath)) {
|
|
7526
|
+
const raw = JSON.parse(fs8.readFileSync(artifactPath, "utf-8"));
|
|
7527
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
7528
|
+
return { path: artifactPath, events: artifact.trace.events };
|
|
7529
|
+
}
|
|
7530
|
+
return { path: tracePath, events: [] };
|
|
7531
|
+
}
|
|
7532
|
+
async function runLiveTrace(sessionId, options, debug, mode) {
|
|
7533
|
+
const session = await resolveSession(sessionId);
|
|
7534
|
+
const { browser, page } = await attachSession(session, { trace: debug });
|
|
7535
|
+
const tracePath = getSessionTracePath(session.id);
|
|
7536
|
+
fs8.mkdirSync(dirname8(tracePath), { recursive: true });
|
|
7537
|
+
const sinkPath = options.output ? resolve7(options.output) : tracePath;
|
|
7538
|
+
fs8.mkdirSync(dirname8(sinkPath), { recursive: true });
|
|
7539
|
+
const liveEvents = [];
|
|
7540
|
+
const collector = new LiveTraceCollector(page.cdpClient, {
|
|
7541
|
+
sessionId: session.id,
|
|
7542
|
+
targetId: page.targetId,
|
|
7543
|
+
mode: options.mode ?? "all",
|
|
7544
|
+
match: options.match,
|
|
7545
|
+
maxPayload: options.maxPayload ?? 256,
|
|
7546
|
+
onEvent: (event) => {
|
|
7547
|
+
liveEvents.push(event);
|
|
7548
|
+
if (mode !== "tail") {
|
|
7549
|
+
fs8.appendFileSync(tracePath, `${JSON.stringify(event)}
|
|
7550
|
+
`, "utf-8");
|
|
7551
|
+
}
|
|
7552
|
+
if (options.output) {
|
|
7553
|
+
fs8.appendFileSync(sinkPath, `${JSON.stringify(event)}
|
|
7554
|
+
`, "utf-8");
|
|
7555
|
+
}
|
|
7556
|
+
if (mode === "tail") {
|
|
7557
|
+
process.stdout.write(`${JSON.stringify(event)}
|
|
7558
|
+
`);
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
});
|
|
7562
|
+
const stopPromise = new Promise((resolveStop) => {
|
|
7563
|
+
const stop = () => resolveStop();
|
|
7564
|
+
process.once("SIGINT", stop);
|
|
7565
|
+
process.once("SIGTERM", stop);
|
|
7566
|
+
if (options.timeout && options.timeout > 0) {
|
|
7567
|
+
setTimeout(stop, options.timeout);
|
|
7568
|
+
}
|
|
7569
|
+
});
|
|
7570
|
+
if (!options.quiet && mode !== "tail") {
|
|
7571
|
+
process.stderr.write(
|
|
7572
|
+
`Tracing session ${session.id} -> ${mode === "start" ? tracePath : "stdout"}
|
|
7573
|
+
`
|
|
7574
|
+
);
|
|
7575
|
+
}
|
|
7576
|
+
try {
|
|
7577
|
+
await collector.start();
|
|
7578
|
+
await stopPromise;
|
|
7579
|
+
return await collector.stop();
|
|
7580
|
+
} finally {
|
|
7581
|
+
await browser.disconnect();
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
function evaluateWatchAssertion(events, assertion, view) {
|
|
7585
|
+
if (assertion === "no-console-errors") {
|
|
7586
|
+
const summary = buildTraceSummary(events, "console");
|
|
7587
|
+
return {
|
|
7588
|
+
ok: summary.errors === 0,
|
|
7589
|
+
reason: summary.errors === 0 ? "No console errors detected" : `${summary.errors} console/runtime errors detected`
|
|
7590
|
+
};
|
|
7591
|
+
}
|
|
7592
|
+
if (assertion === "profile:reconnect" && view === "ws") {
|
|
7593
|
+
const wsEvents = events.filter((event) => event.event.startsWith("ws."));
|
|
7594
|
+
const created = wsEvents.filter((event) => event.event === "ws.connection.created");
|
|
7595
|
+
const closed = wsEvents.filter((event) => event.event === "ws.connection.closed");
|
|
7596
|
+
const reopened = created.length > 1 || closed.length === 0;
|
|
7597
|
+
return {
|
|
7598
|
+
ok: created.length > 0 && reopened,
|
|
7599
|
+
reason: created.length === 0 ? "No WebSocket connections were observed" : reopened ? "WebSocket stayed up or reconnected" : "WebSocket closed without a reconnect"
|
|
7600
|
+
};
|
|
7601
|
+
}
|
|
7602
|
+
return {
|
|
7603
|
+
ok: false,
|
|
7604
|
+
reason: `Unsupported assertion: ${assertion}`
|
|
7605
|
+
};
|
|
7606
|
+
}
|
|
7607
|
+
async function traceStart(options, globalOptions) {
|
|
7608
|
+
const events = await runLiveTrace(
|
|
7609
|
+
globalOptions.session,
|
|
7610
|
+
options,
|
|
7611
|
+
globalOptions.trace ?? false,
|
|
7612
|
+
"start"
|
|
7613
|
+
);
|
|
7614
|
+
output(
|
|
7615
|
+
{
|
|
7616
|
+
success: true,
|
|
7617
|
+
events: events.length,
|
|
7618
|
+
output: getSessionTracePath((await resolveSession(globalOptions.session)).id)
|
|
7619
|
+
},
|
|
7620
|
+
"pretty"
|
|
7621
|
+
);
|
|
7622
|
+
}
|
|
7623
|
+
async function traceTail(options, globalOptions) {
|
|
7624
|
+
await runLiveTrace(globalOptions.session, options, globalOptions.trace ?? false, "tail");
|
|
7625
|
+
}
|
|
7626
|
+
async function traceSummary(options, globalOptions) {
|
|
7627
|
+
const source = await resolveTraceSource(options.source, globalOptions.session);
|
|
7628
|
+
const filtered = options.mode === "ws" ? source.events.filter((event) => event.channel === "ws") : options.mode === "http" ? source.events.filter((event) => event.channel === "http") : source.events;
|
|
7629
|
+
const summary = buildTraceSummary(filtered, options.view ?? "session");
|
|
7630
|
+
output(
|
|
7631
|
+
{
|
|
7632
|
+
source: source.path,
|
|
7633
|
+
view: options.view ?? "session",
|
|
7634
|
+
summary
|
|
7635
|
+
},
|
|
7636
|
+
globalOptions.format ?? "pretty"
|
|
7637
|
+
);
|
|
7638
|
+
}
|
|
7639
|
+
async function traceWatch(options, globalOptions) {
|
|
7640
|
+
const view = options.view ?? "session";
|
|
7641
|
+
const events = options.source ? (await resolveTraceSource(options.source, globalOptions.session)).events : await runLiveTrace(globalOptions.session, options, globalOptions.trace ?? false, "watch");
|
|
7642
|
+
const assertion = options.assert ?? (view === "ws" ? "profile:reconnect" : "no-console-errors");
|
|
7643
|
+
const result = evaluateWatchAssertion(events, assertion, view);
|
|
7644
|
+
if (!result.ok) {
|
|
7645
|
+
throw new Error(result.reason);
|
|
7646
|
+
}
|
|
7647
|
+
output(
|
|
7648
|
+
{
|
|
7649
|
+
success: true,
|
|
7650
|
+
assertion,
|
|
7651
|
+
reason: result.reason,
|
|
7652
|
+
summary: buildTraceSummary(events, view)
|
|
7653
|
+
},
|
|
7654
|
+
globalOptions.format ?? "pretty"
|
|
7655
|
+
);
|
|
7656
|
+
}
|
|
7657
|
+
async function traceExport(options, globalOptions) {
|
|
7658
|
+
if (!options.output) {
|
|
7659
|
+
throw new Error("trace export requires -o <output.json>");
|
|
7660
|
+
}
|
|
7661
|
+
const source = await resolveTraceSource(options.source, globalOptions.session);
|
|
7662
|
+
const payload = {
|
|
7663
|
+
source: source.path,
|
|
7664
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7665
|
+
summary: buildTraceSummary(source.events, options.view ?? "session"),
|
|
7666
|
+
views: buildTraceSummaries(source.events),
|
|
7667
|
+
events: source.events
|
|
7668
|
+
};
|
|
7669
|
+
fs8.mkdirSync(dirname8(resolve7(options.output)), { recursive: true });
|
|
7670
|
+
fs8.writeFileSync(resolve7(options.output), JSON.stringify(payload, null, 2));
|
|
7671
|
+
output({ success: true, output: resolve7(options.output) }, globalOptions.format ?? "pretty");
|
|
7672
|
+
}
|
|
7673
|
+
async function traceMerge(options, globalOptions) {
|
|
7674
|
+
if (options.sources.length === 0) {
|
|
7675
|
+
throw new Error("trace merge requires at least one artifact or trace file");
|
|
7676
|
+
}
|
|
7677
|
+
if (!options.output) {
|
|
7678
|
+
throw new Error("trace merge requires -o <output.json>");
|
|
7679
|
+
}
|
|
7680
|
+
const merged = [];
|
|
7681
|
+
for (const sourcePath of options.sources) {
|
|
7682
|
+
const source = await resolveTraceSource(sourcePath, globalOptions.session);
|
|
7683
|
+
merged.push(...source.events);
|
|
7684
|
+
}
|
|
7685
|
+
merged.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
7686
|
+
const payload = {
|
|
7687
|
+
mergedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7688
|
+
summary: buildTraceSummary(merged, options.view ?? "session"),
|
|
7689
|
+
views: buildTraceSummaries(merged),
|
|
7690
|
+
events: merged
|
|
7691
|
+
};
|
|
7692
|
+
fs8.mkdirSync(dirname8(resolve7(options.output)), { recursive: true });
|
|
7693
|
+
fs8.writeFileSync(resolve7(options.output), JSON.stringify(payload, null, 2));
|
|
7694
|
+
output(
|
|
7695
|
+
{ success: true, output: resolve7(options.output), events: merged.length },
|
|
7696
|
+
globalOptions.format ?? "pretty"
|
|
7697
|
+
);
|
|
7698
|
+
}
|
|
7699
|
+
async function traceCommand(args, globalOptions) {
|
|
7700
|
+
const options = parseTraceArgs(args);
|
|
7701
|
+
if (options.help || globalOptions.help) {
|
|
7702
|
+
console.log(TRACE_HELP);
|
|
7703
|
+
return;
|
|
7704
|
+
}
|
|
7705
|
+
switch (options.subcommand) {
|
|
7706
|
+
case "start":
|
|
7707
|
+
await traceStart(options, globalOptions);
|
|
7708
|
+
break;
|
|
7709
|
+
case "tail":
|
|
7710
|
+
await traceTail(options, globalOptions);
|
|
7711
|
+
break;
|
|
7712
|
+
case "summary":
|
|
7713
|
+
await traceSummary(options, globalOptions);
|
|
7714
|
+
break;
|
|
7715
|
+
case "watch":
|
|
7716
|
+
await traceWatch(options, globalOptions);
|
|
7717
|
+
break;
|
|
7718
|
+
case "export":
|
|
7719
|
+
await traceExport(options, globalOptions);
|
|
7720
|
+
break;
|
|
7721
|
+
case "merge":
|
|
7722
|
+
await traceMerge(options, globalOptions);
|
|
7723
|
+
break;
|
|
7724
|
+
default:
|
|
7725
|
+
console.log(TRACE_HELP);
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7728
|
+
|
|
6130
7729
|
// src/cli/index.ts
|
|
6131
7730
|
var HELP2 = `
|
|
6132
|
-
bp -
|
|
7731
|
+
bp - automation-first browser CLI for agents
|
|
7732
|
+
|
|
7733
|
+
Route the job first:
|
|
7734
|
+
Inspect page state snapshot, page, forms, text, targets, diagnose
|
|
7735
|
+
Act in the browser exec, run
|
|
7736
|
+
Capture a human demo record
|
|
7737
|
+
Analyze behavior over time trace (listen is a compatibility alias)
|
|
7738
|
+
Exercise voice/media audio
|
|
7739
|
+
Change browser conditions env
|
|
6133
7740
|
|
|
6134
7741
|
Usage:
|
|
6135
7742
|
bp <command> [options]
|
|
6136
7743
|
|
|
6137
7744
|
Commands:
|
|
6138
|
-
quickstart Getting started guide
|
|
6139
|
-
connect Create browser session
|
|
6140
|
-
exec Execute actions
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
7745
|
+
quickstart Getting started guide
|
|
7746
|
+
connect Create a browser session
|
|
7747
|
+
exec Execute high-level actions
|
|
7748
|
+
snapshot Inspect current page with refs
|
|
7749
|
+
record Record a human workflow and derive replayable output
|
|
7750
|
+
trace Inspect and analyze behavior over time (listen alias for live stream)
|
|
7751
|
+
audio Set up/validate/inject/capture voice pipelines
|
|
7752
|
+
env Session and browser-environment controls
|
|
7753
|
+
run Run a workflow file
|
|
7754
|
+
page Compact page overview
|
|
7755
|
+
forms List form controls
|
|
6144
7756
|
targets List available browser tabs
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
audio Audio I/O for voice agent testing
|
|
6148
|
-
listen Monitor network traffic (WebSocket/HTTP)
|
|
6149
|
-
snapshot Get page with element refs
|
|
6150
|
-
diagnose Debug element selection issues
|
|
6151
|
-
text Extract text content
|
|
6152
|
-
screenshot Take screenshot
|
|
6153
|
-
daemon Manage daemon processes (status, stop, logs)
|
|
7757
|
+
daemon Manage session daemon
|
|
7758
|
+
list List sessions
|
|
6154
7759
|
close Close session
|
|
6155
|
-
|
|
6156
|
-
clean Clean up old sessions
|
|
7760
|
+
clean Clean old sessions and artifacts
|
|
6157
7761
|
actions Complete action reference
|
|
6158
7762
|
|
|
7763
|
+
Golden paths:
|
|
7764
|
+
1. Automate a page
|
|
7765
|
+
bp connect --provider generic --name dev
|
|
7766
|
+
bp snapshot -i -s dev
|
|
7767
|
+
bp exec -s dev '[{"action":"click","selector":"ref:e4"}]'
|
|
7768
|
+
|
|
7769
|
+
2. Capture a manual workflow and derive automation
|
|
7770
|
+
bp record -s demo --profile automation
|
|
7771
|
+
bp record summary demo/recording.json
|
|
7772
|
+
bp record derive demo/recording.json -o workflow.json
|
|
7773
|
+
bp run workflow.json
|
|
7774
|
+
|
|
7775
|
+
3. Debug a realtime or voice session
|
|
7776
|
+
bp trace start -s dev
|
|
7777
|
+
bp trace summary -s dev --view ws
|
|
7778
|
+
bp audio check -s dev
|
|
7779
|
+
bp trace summary -s dev --view voice
|
|
7780
|
+
|
|
7781
|
+
4. Exercise failure modes
|
|
7782
|
+
bp env network offline -s dev --duration 10000
|
|
7783
|
+
bp trace watch -s dev --view ws --assert profile:reconnect --timeout 15000
|
|
7784
|
+
|
|
6159
7785
|
Options:
|
|
6160
7786
|
-s, --session <id> Session ID
|
|
6161
7787
|
-f, --format <fmt> json | pretty (default: pretty)
|
|
6162
7788
|
--json Alias for -f json
|
|
6163
|
-
--
|
|
7789
|
+
--debug Enable debug logs for CDP transport
|
|
7790
|
+
--trace Legacy alias for --debug
|
|
6164
7791
|
--dialog <mode> Handle dialogs: accept | dismiss
|
|
6165
7792
|
-h, --help Show help
|
|
6166
7793
|
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
bp exec '{"action":"goto","url":"https://example.com"}'
|
|
6170
|
-
bp exec --record '[{"action":"click","selector":"#checkout"}]'
|
|
6171
|
-
bp snapshot -i
|
|
6172
|
-
bp exec '{"action":"click","selector":"ref:e3"}'
|
|
6173
|
-
bp clean --max-size 500MB
|
|
6174
|
-
bp eval 'document.title'
|
|
6175
|
-
bp audio roundtrip -i prompt.wav --transcribe --silence-timeout 5000
|
|
6176
|
-
|
|
6177
|
-
Run 'bp quickstart' for CLI workflow guide.
|
|
6178
|
-
Run 'bp actions' for complete action reference.
|
|
6179
|
-
Run 'bp audio --help' for voice agent testing guide.
|
|
7794
|
+
Notes:
|
|
7795
|
+
Start with "record summary" or "trace summary" before opening raw artifacts.
|
|
6180
7796
|
`;
|
|
6181
7797
|
function parseGlobalOptions(args) {
|
|
6182
7798
|
const options = {
|
|
@@ -6198,7 +7814,8 @@ function parseGlobalOptions(args) {
|
|
|
6198
7814
|
options.format = "json";
|
|
6199
7815
|
} else if (arg === "--pretty") {
|
|
6200
7816
|
options.format = "pretty";
|
|
6201
|
-
} else if (arg === "--trace") {
|
|
7817
|
+
} else if (arg === "--debug" || arg === "--trace") {
|
|
7818
|
+
options.debug = true;
|
|
6202
7819
|
options.trace = true;
|
|
6203
7820
|
} else if (arg === "-h" || arg === "--help") {
|
|
6204
7821
|
options.help = true;
|
|
@@ -6297,9 +7914,6 @@ async function main() {
|
|
|
6297
7914
|
case "screenshot":
|
|
6298
7915
|
await screenshotCommand(remaining, options);
|
|
6299
7916
|
break;
|
|
6300
|
-
case "daemon":
|
|
6301
|
-
await daemonCommand(remaining, options);
|
|
6302
|
-
break;
|
|
6303
7917
|
case "close":
|
|
6304
7918
|
await closeCommand(remaining, options);
|
|
6305
7919
|
break;
|
|
@@ -6318,11 +7932,20 @@ async function main() {
|
|
|
6318
7932
|
case "record":
|
|
6319
7933
|
await recordCommand(remaining, options);
|
|
6320
7934
|
break;
|
|
7935
|
+
case "trace":
|
|
7936
|
+
await traceCommand(remaining, options);
|
|
7937
|
+
break;
|
|
6321
7938
|
case "audio":
|
|
6322
7939
|
await audioCommand(remaining, options);
|
|
6323
7940
|
break;
|
|
7941
|
+
case "env":
|
|
7942
|
+
await envCommand(remaining, options);
|
|
7943
|
+
break;
|
|
6324
7944
|
case "listen":
|
|
6325
|
-
await
|
|
7945
|
+
await traceCommand(["tail", ...remaining], options);
|
|
7946
|
+
break;
|
|
7947
|
+
case "daemon":
|
|
7948
|
+
await daemonCommand(remaining, options);
|
|
6326
7949
|
break;
|
|
6327
7950
|
case "help":
|
|
6328
7951
|
case "--help":
|