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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/cli.js +84 -20
  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,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("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
551
- bp click 0 --xy 400,300 # click at coordinates (canvas/maps)
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 <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", `
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 # click element [3]
761
- bp click 0 --xy 400,300 # click at coordinates (ref ignored)
762
- bp click 0 --xy 400,300 --double # double-click at coordinates
763
- bp click 0 --xy 400,300 --right # right-click at coordinates`).action(action(async (ref, opts) => {
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 code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
859
- const keyCode = char.toUpperCase().charCodeAt(0);
860
- await t.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sid);
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
- await dispatchKey(transport, sessionId, "Meta+a");
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 code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
903
- const keyCode = char.toUpperCase().charCodeAt(0);
904
- await transport.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sessionId);
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
- emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-pilot-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI tool to control your browser via Chrome DevTools Protocol",
5
5
  "type": "module",
6
6
  "bin": {