browser-pilot-cli 0.1.4 → 0.1.6
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 +84 -20
- 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,13 +543,16 @@ 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
|
|
551
|
-
bp click
|
|
554
|
+
bp click --xy 400,300 # click at coordinates (canvas/maps)
|
|
555
|
+
bp locate ".selector" # get element coordinates for click --xy
|
|
552
556
|
bp type <ref> <text> # input text \u2014 returns updated snapshot
|
|
553
557
|
bp keyboard <text> # type via keyboard events (Google Docs etc.)
|
|
554
558
|
bp press <key> # press key \u2014 returns updated snapshot
|
|
@@ -659,7 +663,30 @@ var KEY_DEFS = {
|
|
|
659
663
|
home: { key: "Home", code: "Home", keyCode: 36 },
|
|
660
664
|
end: { key: "End", code: "End", keyCode: 35 },
|
|
661
665
|
pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
662
|
-
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
666
|
+
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 },
|
|
667
|
+
// Digits
|
|
668
|
+
"0": { key: "0", code: "Digit0", keyCode: 48 },
|
|
669
|
+
"1": { key: "1", code: "Digit1", keyCode: 49 },
|
|
670
|
+
"2": { key: "2", code: "Digit2", keyCode: 50 },
|
|
671
|
+
"3": { key: "3", code: "Digit3", keyCode: 51 },
|
|
672
|
+
"4": { key: "4", code: "Digit4", keyCode: 52 },
|
|
673
|
+
"5": { key: "5", code: "Digit5", keyCode: 53 },
|
|
674
|
+
"6": { key: "6", code: "Digit6", keyCode: 54 },
|
|
675
|
+
"7": { key: "7", code: "Digit7", keyCode: 55 },
|
|
676
|
+
"8": { key: "8", code: "Digit8", keyCode: 56 },
|
|
677
|
+
"9": { key: "9", code: "Digit9", keyCode: 57 },
|
|
678
|
+
// Punctuation
|
|
679
|
+
"-": { key: "-", code: "Minus", keyCode: 189 },
|
|
680
|
+
"=": { key: "=", code: "Equal", keyCode: 187 },
|
|
681
|
+
"[": { key: "[", code: "BracketLeft", keyCode: 219 },
|
|
682
|
+
"]": { key: "]", code: "BracketRight", keyCode: 221 },
|
|
683
|
+
"\\": { key: "\\", code: "Backslash", keyCode: 220 },
|
|
684
|
+
";": { key: ";", code: "Semicolon", keyCode: 186 },
|
|
685
|
+
"'": { key: "'", code: "Quote", keyCode: 222 },
|
|
686
|
+
",": { key: ",", code: "Comma", keyCode: 188 },
|
|
687
|
+
".": { key: ".", code: "Period", keyCode: 190 },
|
|
688
|
+
"/": { key: "/", code: "Slash", keyCode: 191 },
|
|
689
|
+
"`": { key: "`", code: "Backquote", keyCode: 192 }
|
|
663
690
|
};
|
|
664
691
|
var MOD_DEFS = {
|
|
665
692
|
control: { key: "Control", code: "ControlLeft", keyCode: 17, mask: 2 },
|
|
@@ -753,14 +780,15 @@ program.command("snapshot").description("Get interactive elements on the page").
|
|
|
753
780
|
emitSnapshot(await takeSnapshot(transport, sessionId, state.activeTargetId, limit));
|
|
754
781
|
});
|
|
755
782
|
}));
|
|
756
|
-
program.command("click
|
|
757
|
-
Ref is a number from the snapshot output, or use --xy for coordinate clicks.
|
|
758
|
-
|
|
783
|
+
program.command("click [ref]").description("Click element by ref number or at x,y coordinates").option("--xy <coords>", "click at x,y viewport coordinates (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", `
|
|
759
784
|
Examples:
|
|
760
|
-
bp click 3
|
|
761
|
-
bp click
|
|
762
|
-
bp click
|
|
763
|
-
bp click
|
|
785
|
+
bp click 3 # click element [3] from snapshot
|
|
786
|
+
bp click --xy 400,300 # click at viewport coordinates
|
|
787
|
+
bp click --xy 400,300 --double # double-click at coordinates
|
|
788
|
+
bp click --xy 400,300 --right # right-click (context menu)
|
|
789
|
+
bp click 3 --right # right-click element [3]`).action(action(async (ref, opts) => {
|
|
790
|
+
if (opts.double && opts.right) throw new Error("--double and --right are mutually exclusive");
|
|
791
|
+
if (!ref && !opts.xy) throw new Error("Provide a ref number or --xy coordinates");
|
|
764
792
|
const limit = parseLimit(opts.limit);
|
|
765
793
|
await withPilot(async ({ transport, sessionId, state }) => {
|
|
766
794
|
if (opts.xy) {
|
|
@@ -805,6 +833,28 @@ Examples:
|
|
|
805
833
|
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
806
834
|
});
|
|
807
835
|
}));
|
|
836
|
+
program.command("locate <selector>").description("Get element coordinates by CSS selector (for use with click --xy)").addHelpText("after", `
|
|
837
|
+
Returns center coordinates and bounding box of an element.
|
|
838
|
+
Use with click --xy for canvas apps, charts, or elements not in snapshot.
|
|
839
|
+
|
|
840
|
+
Examples:
|
|
841
|
+
bp locate ".kix-appview-editor" # Google Docs editor area
|
|
842
|
+
bp locate "canvas" # canvas element
|
|
843
|
+
bp locate "#map" # map container`).action(action(async (selector) => {
|
|
844
|
+
await withPilot(async ({ transport, sessionId }) => {
|
|
845
|
+
const { result } = await transport.send("Runtime.evaluate", {
|
|
846
|
+
expression: `JSON.stringify((function(){var el=document.querySelector(${JSON.stringify(selector)});if(!el)return null;el.scrollIntoView({block:'center',inline:'center'});var r=el.getBoundingClientRect();return{x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2),top:Math.round(r.top),left:Math.round(r.left),width:Math.round(r.width),height:Math.round(r.height)}})())`,
|
|
847
|
+
returnByValue: true
|
|
848
|
+
}, sessionId);
|
|
849
|
+
const coords = result.value ? JSON.parse(result.value) : null;
|
|
850
|
+
if (!coords) throw new Error(`Element not found: ${selector}`);
|
|
851
|
+
if (useJson()) {
|
|
852
|
+
console.log(JSON.stringify({ ok: true, ...coords }));
|
|
853
|
+
} else {
|
|
854
|
+
console.log(`center: ${coords.x},${coords.y} size: ${coords.width}x${coords.height} (top:${coords.top} left:${coords.left})`);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}));
|
|
808
858
|
program.command("type <ref> <text>").description("Type text into element and return page snapshot").option("-c, --clear", "clear field before typing").option("-s, --submit", "press Enter after typing").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", '\nExamples:\n bp type 2 "hello world"\n bp type 5 "query" --submit\n bp type 3 "new value" --clear').action(action(async (ref, text, opts) => {
|
|
809
859
|
const limit = parseLimit(opts.limit);
|
|
810
860
|
await withPilot(async ({ transport, sessionId, state }) => {
|
|
@@ -848,6 +898,13 @@ program.command("type <ref> <text>").description("Type text into element and ret
|
|
|
848
898
|
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
849
899
|
});
|
|
850
900
|
}));
|
|
901
|
+
function charToCode(char) {
|
|
902
|
+
const kd = KEY_DEFS[char];
|
|
903
|
+
if (kd) return kd;
|
|
904
|
+
if (/^[a-zA-Z]$/.test(char)) return { key: char, code: `Key${char.toUpperCase()}`, keyCode: char.toUpperCase().charCodeAt(0) };
|
|
905
|
+
if (char === " ") return { key: " ", code: "Space", keyCode: 32 };
|
|
906
|
+
return { key: char, code: "", keyCode: char.charCodeAt(0) };
|
|
907
|
+
}
|
|
851
908
|
async function typeViaKeyboard(t, sid, text) {
|
|
852
909
|
for (const char of text) {
|
|
853
910
|
if (char === "\n") {
|
|
@@ -855,10 +912,9 @@ async function typeViaKeyboard(t, sid, text) {
|
|
|
855
912
|
} else if (char === " ") {
|
|
856
913
|
await dispatchKey(t, sid, "Tab");
|
|
857
914
|
} else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
await t.send("Input.dispatchKeyEvent", { type: "
|
|
861
|
-
await t.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sid);
|
|
915
|
+
const { key, code, keyCode } = charToCode(char);
|
|
916
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyDown", key, code, windowsVirtualKeyCode: keyCode, text: char }, sid);
|
|
917
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: keyCode }, sid);
|
|
862
918
|
} else {
|
|
863
919
|
await t.send("Input.insertText", { text: char }, sid);
|
|
864
920
|
}
|
|
@@ -890,19 +946,22 @@ Examples:
|
|
|
890
946
|
await new Promise((r) => setTimeout(r, 300));
|
|
891
947
|
}
|
|
892
948
|
if (opts.clear) {
|
|
893
|
-
|
|
949
|
+
const selectAllMod = process.platform === "darwin" ? "Meta" : "Control";
|
|
950
|
+
await dispatchKey(transport, sessionId, `${selectAllMod}+a`);
|
|
894
951
|
await dispatchKey(transport, sessionId, "Delete");
|
|
895
952
|
}
|
|
896
953
|
if (opts.delay) {
|
|
897
954
|
const delay = parseInt(opts.delay, 10);
|
|
955
|
+
if (isNaN(delay) || delay < 0) throw new Error("--delay must be a non-negative number");
|
|
898
956
|
for (const char of text) {
|
|
899
957
|
if (char === "\n") {
|
|
900
958
|
await dispatchKey(transport, sessionId, "Enter");
|
|
959
|
+
} else if (char === " ") {
|
|
960
|
+
await dispatchKey(transport, sessionId, "Tab");
|
|
901
961
|
} else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
await transport.send("Input.dispatchKeyEvent", { type: "
|
|
905
|
-
await transport.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sessionId);
|
|
962
|
+
const { key, code, keyCode } = charToCode(char);
|
|
963
|
+
await transport.send("Input.dispatchKeyEvent", { type: "keyDown", key, code, windowsVirtualKeyCode: keyCode, text: char }, sessionId);
|
|
964
|
+
await transport.send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: keyCode }, sessionId);
|
|
906
965
|
} else {
|
|
907
966
|
await transport.send("Input.insertText", { text: char }, sessionId);
|
|
908
967
|
}
|
|
@@ -912,7 +971,12 @@ Examples:
|
|
|
912
971
|
await typeViaKeyboard(transport, sessionId, text);
|
|
913
972
|
}
|
|
914
973
|
if (opts.submit) await dispatchKey(transport, sessionId, "Enter");
|
|
915
|
-
|
|
974
|
+
const snapResult = await snap(transport, sessionId, state.activeTargetId, limit);
|
|
975
|
+
if (useJson()) {
|
|
976
|
+
console.log(JSON.stringify({ ok: true, typed: text, ...snapResult.data }));
|
|
977
|
+
} else {
|
|
978
|
+
console.log(snapResult.text);
|
|
979
|
+
}
|
|
916
980
|
});
|
|
917
981
|
}));
|
|
918
982
|
program.command("press <key>").description("Press key combo (e.g. Enter, Escape, Control+a) and return snapshot").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nKeys: Enter, Escape, Tab, Space, Backspace, Delete,\n ArrowUp, ArrowDown, ArrowLeft, ArrowRight,\n Home, End, PageUp, PageDown\nModifiers: Control (Ctrl), Shift, Alt, Meta (Cmd)\n\nExamples:\n bp press Enter\n bp press Escape\n bp press Control+a\n bp press Meta+c").action(action(async (key, opts) => {
|