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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/cli.js +94 -24
  3. 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("0.1.0").option("--human", "force human-readable output (default when TTY)").addHelpText("after", `
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("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nRef is a number from the snapshot output.\n\nExamples:\n bp click 3\n bp click 3 --limit 10").action(action(async (ref, opts) => {
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
- const objectId = await resolveTarget(transport, sessionId, ref, state.activeTargetId);
759
- try {
760
- const { result } = await transport.send("Runtime.callFunctionOn", {
761
- objectId,
762
- functionDeclaration: GET_CLICK_COORDS,
763
- returnByValue: true
764
- }, sessionId);
765
- const { x, y } = JSON.parse(result.value);
766
- await dispatchClick(transport, sessionId, x, y);
767
- } finally {
768
- await transport.send("Runtime.releaseObject", { objectId }, sessionId).catch(() => {
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 code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
825
- const keyCode = char.toUpperCase().charCodeAt(0);
826
- await t.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sid);
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
- await dispatchKey(transport, sessionId, "Meta+a");
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 code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
869
- const keyCode = char.toUpperCase().charCodeAt(0);
870
- await transport.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sessionId);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-pilot-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI tool to control your browser via Chrome DevTools Protocol",
5
5
  "type": "module",
6
6
  "bin": {