browser-pilot-cli 0.1.3 → 0.1.5
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 +12 -1
- package/dist/cli.js +94 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,8 +96,10 @@ The daemon maintains a single CDP WebSocket connection. A pulsing blue glow arou
|
|
|
96
96
|
|---------|---------|-------------|
|
|
97
97
|
| `bp open <url>` | snapshot | Navigate to URL |
|
|
98
98
|
| `bp snapshot` | snapshot | Get interactive elements |
|
|
99
|
-
| `bp click <ref>` | snapshot | Click element by ref number |
|
|
99
|
+
| `bp click <ref>` | snapshot | Click element by ref number (`--double`, `--right`) |
|
|
100
|
+
| `bp click 0 --xy x,y` | snapshot | Click at viewport coordinates (canvas, maps) |
|
|
100
101
|
| `bp type <ref> <text>` | snapshot | Type into element (`--clear`, `--submit`) |
|
|
102
|
+
| `bp keyboard <text>` | snapshot | Type via keyboard events (`--click`, `--clear`) |
|
|
101
103
|
| `bp press <key>` | snapshot | Press key (Enter, Escape, Control+a, Meta+c) |
|
|
102
104
|
| `bp eval [js]` | value | Run JavaScript (escape hatch for anything) |
|
|
103
105
|
|
|
@@ -211,6 +213,15 @@ bp upload ~/Downloads/photo.jpg # auto-finds file input, triggers upload
|
|
|
211
213
|
bp type 3 "new content" --clear # replace content in a rich text editor
|
|
212
214
|
```
|
|
213
215
|
|
|
216
|
+
For canvas-based editors (Google Docs, Google Sheets, Figma), use `bp keyboard` which sends real keyboard events:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
bp keyboard "Hello Docs!" --click ".kix-appview-editor" # Google Docs
|
|
220
|
+
bp press Meta+b # toggle bold
|
|
221
|
+
bp keyboard "bold text"
|
|
222
|
+
bp click 0 --xy 400,300 # click canvas area
|
|
223
|
+
```
|
|
224
|
+
|
|
214
225
|
Shadow DOM elements are traversed automatically — no special commands needed. Elements inside open shadow roots (even deeply nested) appear in snapshots and can be clicked/typed normally.
|
|
215
226
|
|
|
216
227
|
For `<select>` dropdowns (shown as `combobox`), use `bp eval`:
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { writeFileSync as writeFileSync3, readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
6
6
|
import { resolve as resolvePath } from "path";
|
|
7
|
+
import { createRequire } from "module";
|
|
7
8
|
|
|
8
9
|
// src/session.ts
|
|
9
10
|
import { spawn } from "child_process";
|
|
@@ -542,12 +543,15 @@ async function resolveTarget(transport, sessionId, target, targetId) {
|
|
|
542
543
|
}
|
|
543
544
|
|
|
544
545
|
// src/cli.ts
|
|
546
|
+
var require2 = createRequire(import.meta.url);
|
|
547
|
+
var PKG_VERSION = require2("../package.json").version;
|
|
545
548
|
var program = new Command();
|
|
546
|
-
program.name("bp").description("Control your browser from the command line").version(
|
|
549
|
+
program.name("bp").description("Control your browser from the command line").version(PKG_VERSION).option("--human", "force human-readable output (default when TTY)").addHelpText("after", `
|
|
547
550
|
Workflow:
|
|
548
551
|
bp connect # one-time setup (click Allow in Chrome)
|
|
549
552
|
bp open <url> # navigate \u2014 returns snapshot with [ref] numbers
|
|
550
553
|
bp click <ref> # interact \u2014 returns updated snapshot
|
|
554
|
+
bp click 0 --xy 400,300 # click at coordinates (canvas/maps)
|
|
551
555
|
bp type <ref> <text> # input text \u2014 returns updated snapshot
|
|
552
556
|
bp keyboard <text> # type via keyboard events (Google Docs etc.)
|
|
553
557
|
bp press <key> # press key \u2014 returns updated snapshot
|
|
@@ -658,7 +662,30 @@ var KEY_DEFS = {
|
|
|
658
662
|
home: { key: "Home", code: "Home", keyCode: 36 },
|
|
659
663
|
end: { key: "End", code: "End", keyCode: 35 },
|
|
660
664
|
pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
661
|
-
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
665
|
+
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 },
|
|
666
|
+
// Digits
|
|
667
|
+
"0": { key: "0", code: "Digit0", keyCode: 48 },
|
|
668
|
+
"1": { key: "1", code: "Digit1", keyCode: 49 },
|
|
669
|
+
"2": { key: "2", code: "Digit2", keyCode: 50 },
|
|
670
|
+
"3": { key: "3", code: "Digit3", keyCode: 51 },
|
|
671
|
+
"4": { key: "4", code: "Digit4", keyCode: 52 },
|
|
672
|
+
"5": { key: "5", code: "Digit5", keyCode: 53 },
|
|
673
|
+
"6": { key: "6", code: "Digit6", keyCode: 54 },
|
|
674
|
+
"7": { key: "7", code: "Digit7", keyCode: 55 },
|
|
675
|
+
"8": { key: "8", code: "Digit8", keyCode: 56 },
|
|
676
|
+
"9": { key: "9", code: "Digit9", keyCode: 57 },
|
|
677
|
+
// Punctuation
|
|
678
|
+
"-": { key: "-", code: "Minus", keyCode: 189 },
|
|
679
|
+
"=": { key: "=", code: "Equal", keyCode: 187 },
|
|
680
|
+
"[": { key: "[", code: "BracketLeft", keyCode: 219 },
|
|
681
|
+
"]": { key: "]", code: "BracketRight", keyCode: 221 },
|
|
682
|
+
"\\": { key: "\\", code: "Backslash", keyCode: 220 },
|
|
683
|
+
";": { key: ";", code: "Semicolon", keyCode: 186 },
|
|
684
|
+
"'": { key: "'", code: "Quote", keyCode: 222 },
|
|
685
|
+
",": { key: ",", code: "Comma", keyCode: 188 },
|
|
686
|
+
".": { key: ".", code: "Period", keyCode: 190 },
|
|
687
|
+
"/": { key: "/", code: "Slash", keyCode: 191 },
|
|
688
|
+
"`": { key: "`", code: "Backquote", keyCode: 192 }
|
|
662
689
|
};
|
|
663
690
|
var MOD_DEFS = {
|
|
664
691
|
control: { key: "Control", code: "ControlLeft", keyCode: 17, mask: 2 },
|
|
@@ -752,21 +779,55 @@ program.command("snapshot").description("Get interactive elements on the page").
|
|
|
752
779
|
emitSnapshot(await takeSnapshot(transport, sessionId, state.activeTargetId, limit));
|
|
753
780
|
});
|
|
754
781
|
}));
|
|
755
|
-
program.command("click <ref>").description("Click element by ref number and return page snapshot").option("
|
|
782
|
+
program.command("click <ref>").description("Click element by ref number and return page snapshot").option("--xy <coords>", "click at x,y coordinates instead of ref (e.g. --xy 400,300)").option("--double", "double-click").option("--right", "right-click (context menu)").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", `
|
|
783
|
+
Ref is a number from the snapshot output, or use --xy for coordinate clicks.
|
|
784
|
+
|
|
785
|
+
Examples:
|
|
786
|
+
bp click 3 # click element [3]
|
|
787
|
+
bp click 0 --xy 400,300 # click at coordinates (ref ignored)
|
|
788
|
+
bp click 0 --xy 400,300 --double # double-click at coordinates
|
|
789
|
+
bp click 0 --xy 400,300 --right # right-click at coordinates`).action(action(async (ref, opts) => {
|
|
790
|
+
if (opts.double && opts.right) throw new Error("--double and --right are mutually exclusive");
|
|
756
791
|
const limit = parseLimit(opts.limit);
|
|
757
792
|
await withPilot(async ({ transport, sessionId, state }) => {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
793
|
+
if (opts.xy) {
|
|
794
|
+
const [xStr, yStr] = opts.xy.split(",");
|
|
795
|
+
const x = parseFloat(xStr), y = parseFloat(yStr);
|
|
796
|
+
if (isNaN(x) || isNaN(y)) throw new Error("--xy must be x,y (e.g. --xy 400,300)");
|
|
797
|
+
if (opts.right) {
|
|
798
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" }, sessionId);
|
|
799
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "right", clickCount: 1 }, sessionId);
|
|
800
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "right", clickCount: 1 }, sessionId);
|
|
801
|
+
} else {
|
|
802
|
+
const clickCount = opts.double ? 2 : 1;
|
|
803
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" }, sessionId);
|
|
804
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount }, sessionId);
|
|
805
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount }, sessionId);
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
const objectId = await resolveTarget(transport, sessionId, ref, state.activeTargetId);
|
|
809
|
+
try {
|
|
810
|
+
const { result } = await transport.send("Runtime.callFunctionOn", {
|
|
811
|
+
objectId,
|
|
812
|
+
functionDeclaration: GET_CLICK_COORDS,
|
|
813
|
+
returnByValue: true
|
|
814
|
+
}, sessionId);
|
|
815
|
+
const { x, y } = JSON.parse(result.value);
|
|
816
|
+
if (opts.double) {
|
|
817
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" }, sessionId);
|
|
818
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 2 }, sessionId);
|
|
819
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 2 }, sessionId);
|
|
820
|
+
} else if (opts.right) {
|
|
821
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" }, sessionId);
|
|
822
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "right", clickCount: 1 }, sessionId);
|
|
823
|
+
await transport.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "right", clickCount: 1 }, sessionId);
|
|
824
|
+
} else {
|
|
825
|
+
await dispatchClick(transport, sessionId, x, y);
|
|
826
|
+
}
|
|
827
|
+
} finally {
|
|
828
|
+
await transport.send("Runtime.releaseObject", { objectId }, sessionId).catch(() => {
|
|
829
|
+
});
|
|
830
|
+
}
|
|
770
831
|
}
|
|
771
832
|
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
772
833
|
});
|
|
@@ -814,6 +875,13 @@ program.command("type <ref> <text>").description("Type text into element and ret
|
|
|
814
875
|
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
815
876
|
});
|
|
816
877
|
}));
|
|
878
|
+
function charToCode(char) {
|
|
879
|
+
const kd = KEY_DEFS[char];
|
|
880
|
+
if (kd) return kd;
|
|
881
|
+
if (/^[a-zA-Z]$/.test(char)) return { key: char, code: `Key${char.toUpperCase()}`, keyCode: char.toUpperCase().charCodeAt(0) };
|
|
882
|
+
if (char === " ") return { key: " ", code: "Space", keyCode: 32 };
|
|
883
|
+
return { key: char, code: "", keyCode: char.charCodeAt(0) };
|
|
884
|
+
}
|
|
817
885
|
async function typeViaKeyboard(t, sid, text) {
|
|
818
886
|
for (const char of text) {
|
|
819
887
|
if (char === "\n") {
|
|
@@ -821,10 +889,9 @@ async function typeViaKeyboard(t, sid, text) {
|
|
|
821
889
|
} else if (char === " ") {
|
|
822
890
|
await dispatchKey(t, sid, "Tab");
|
|
823
891
|
} else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
await t.send("Input.dispatchKeyEvent", { type: "
|
|
827
|
-
await t.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sid);
|
|
892
|
+
const { key, code, keyCode } = charToCode(char);
|
|
893
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyDown", key, code, windowsVirtualKeyCode: keyCode, text: char }, sid);
|
|
894
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: keyCode }, sid);
|
|
828
895
|
} else {
|
|
829
896
|
await t.send("Input.insertText", { text: char }, sid);
|
|
830
897
|
}
|
|
@@ -856,19 +923,22 @@ Examples:
|
|
|
856
923
|
await new Promise((r) => setTimeout(r, 300));
|
|
857
924
|
}
|
|
858
925
|
if (opts.clear) {
|
|
859
|
-
|
|
926
|
+
const selectAllMod = process.platform === "darwin" ? "Meta" : "Control";
|
|
927
|
+
await dispatchKey(transport, sessionId, `${selectAllMod}+a`);
|
|
860
928
|
await dispatchKey(transport, sessionId, "Delete");
|
|
861
929
|
}
|
|
862
930
|
if (opts.delay) {
|
|
863
931
|
const delay = parseInt(opts.delay, 10);
|
|
932
|
+
if (isNaN(delay) || delay < 0) throw new Error("--delay must be a non-negative number");
|
|
864
933
|
for (const char of text) {
|
|
865
934
|
if (char === "\n") {
|
|
866
935
|
await dispatchKey(transport, sessionId, "Enter");
|
|
936
|
+
} else if (char === " ") {
|
|
937
|
+
await dispatchKey(transport, sessionId, "Tab");
|
|
867
938
|
} else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
await transport.send("Input.dispatchKeyEvent", { type: "
|
|
871
|
-
await transport.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sessionId);
|
|
939
|
+
const { key, code, keyCode } = charToCode(char);
|
|
940
|
+
await transport.send("Input.dispatchKeyEvent", { type: "keyDown", key, code, windowsVirtualKeyCode: keyCode, text: char }, sessionId);
|
|
941
|
+
await transport.send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: keyCode }, sessionId);
|
|
872
942
|
} else {
|
|
873
943
|
await transport.send("Input.insertText", { text: char }, sessionId);
|
|
874
944
|
}
|