browser-ctl 0.2.6__tar.gz → 0.2.8__tar.gz

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 (23) hide show
  1. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/PKG-INFO +1 -1
  2. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/SKILL.md +70 -3
  3. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/cli.py +29 -0
  4. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/background.js +9 -2
  5. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/manifest.json +1 -1
  6. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/PKG-INFO +1 -1
  7. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/pyproject.toml +1 -1
  8. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/LICENSE +0 -0
  9. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/README.md +0 -0
  10. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/__init__.py +0 -0
  11. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/__main__.py +0 -0
  12. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/client.py +0 -0
  13. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-128.png +0 -0
  14. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-16.png +0 -0
  15. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-32.png +0 -0
  16. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-48.png +0 -0
  17. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/server.py +0 -0
  18. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/SOURCES.txt +0 -0
  19. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/dependency_links.txt +0 -0
  20. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/entry_points.txt +0 -0
  21. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/requires.txt +0 -0
  22. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/top_level.txt +0 -0
  23. {browser_ctl-0.2.6 → browser_ctl-0.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browser-ctl
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Control your browser from the command line via a Chrome extension + WebSocket bridge
5
5
  Author-email: geb <853934146@qq.com>
6
6
  License-Expression: MIT
@@ -80,8 +80,8 @@ bctl eval <code> Execute JS in page context (MAIN world)
80
80
 
81
81
  ### Tabs
82
82
  ```
83
- bctl tabs List all tabs (id, url, title, active)
84
- bctl tab <id> Switch to tab
83
+ bctl tabs List all tabs (id, url, title, active, windowId)
84
+ bctl tab <id> Switch to tab (also focuses the containing window)
85
85
  bctl new-tab [url] Open new tab
86
86
  bctl close-tab [id] Close tab (default: active)
87
87
  ```
@@ -166,6 +166,69 @@ bctl go "https://v.qq.com/x/cover/<cid>.html"
166
166
  - After navigation: `bctl wait 2-3` or `bctl wait "<selector>" 10`
167
167
  - After hover for overlay: `bctl wait 1`
168
168
  - AI generation: **poll** with `bctl wait 5 && bctl count "selector"` in a loop
169
+ - Pure sleep (`bctl wait N`) runs locally in Python — no extension round-trip, so it
170
+ never times out even on heavy pages.
171
+
172
+ ### Heavy SPA Pages (YouTube, Gmail, etc.)
173
+ Heavy SPA pages can cause the extension service worker to become unresponsive during
174
+ page load. To avoid timeouts:
175
+ - **Don't chain `bctl wait` with navigation via `&&`** — if the page is loading, the
176
+ wait command may timeout because the extension is busy. Instead, run them separately:
177
+ ```bash
178
+ bctl go "https://www.youtube.com"
179
+ bctl wait 3
180
+ bctl status
181
+ ```
182
+ - **Use `bctl go` instead of `bctl new-tab`** when you just need to navigate — it's
183
+ more reliable because it reuses the current tab instead of creating a new one.
184
+ - **If `new-tab` times out but the tab was created**, use `bctl tabs` to find it and
185
+ `bctl tab <id>` to switch to it.
186
+
187
+ ### Multi-Window Awareness
188
+ `bctl tabs` returns tabs from ALL windows with `windowId` and `focusedWindowId`.
189
+ `bctl tab <id>` automatically focuses the containing window before activating the tab,
190
+ so cross-window tab switching works reliably.
191
+
192
+ When working with multiple windows:
193
+ - Check `windowId` in `bctl tabs` output to understand which window each tab belongs to
194
+ - `bctl tab <id>` handles cross-window switching automatically
195
+ - `bctl status` and `bctl snapshot` always operate on the active tab of the **focused** window
196
+
197
+ ### SPA Form Interactions (React, Vue, Angular, etc.)
198
+ Modern SPA frameworks manage form state internally. **Never use `bctl eval` to set
199
+ form values or click buttons** — it bypasses the framework's event system:
200
+
201
+ ```bash
202
+ # BAD — bypasses React state, the dropdown/filter won't actually update:
203
+ bctl eval "document.querySelector('input').value = 'hello'; ..."
204
+
205
+ # GOOD — triggers real keyboard events that React/Vue can observe:
206
+ bctl type "input" "hello"
207
+
208
+ # BAD — JS .click() doesn't fire full pointer+mouse sequence, SPA may ignore it:
209
+ bctl eval "document.querySelector('button').click()"
210
+
211
+ # GOOD — dispatches real mousedown/mouseup/click events:
212
+ bctl click "button" -t "Submit"
213
+ ```
214
+
215
+ **`bctl type`** — sets value and fires focus/input/change events; works for most
216
+ React/Vue inputs including search filters and form fields.
217
+
218
+ **`bctl input-text`** — types character-by-character with real keydown/keypress/keyup
219
+ events; use for rich text editors, autocomplete fields, or when `type` doesn't trigger
220
+ the expected behavior. Add `--delay 50` if the app debounces input.
221
+
222
+ **Complex dropdowns/pickers** (tag selectors, date pickers, combo boxes):
223
+ 1. `bctl click` to open the dropdown
224
+ 2. `bctl wait 1` for the dropdown to render
225
+ 3. `bctl type` into the filter/search input (NOT `bctl eval` with `value =`)
226
+ 4. `bctl wait 1` for results to filter
227
+ 5. `bctl click` on the target option (use `-t` for text matching)
228
+ 6. **Verify** the selection: `bctl snapshot` or `bctl text` to confirm state changed
229
+
230
+ Always verify after complex interactions — if the state didn't change, retry with
231
+ `bctl input-text --delay 50` instead of `bctl type`.
169
232
 
170
233
  ### Data Extraction
171
234
  Prefer `bctl select` over `bctl eval` — it's more reliable, works on all sites,
@@ -178,11 +241,15 @@ and returns text/href/id/class/aria-label automatically.
178
241
  3. **Use `download` for authenticated resources** — never `curl` from sites behind login.
179
242
  4. **Use `hover` before clicking overlay buttons** — many UIs hide actions until hover.
180
243
  5. **Check `tabs` after tab-opening actions** — popups may switch the active tab.
181
- 6. **Chain commands** with `&&`: `bctl go "https://example.com" && bctl wait 2 && bctl status`
244
+ 6. **Don't chain `&&` with `bctl wait` after navigation** run them as separate commands.
245
+ 7. **Prefer `bctl go` over `bctl new-tab`** for simple navigation — fewer failure modes.
246
+ 8. **Never use `eval` to set input values or click buttons on SPA sites** — use `type`/`input-text`/`click`.
247
+ 9. **Verify after complex UI interactions** — `snapshot` or `text` to confirm state changed.
182
248
 
183
249
  ## Known Limitations
184
250
 
185
251
  - `eval` blocked by Trusted Types on some sites (Gemini, YouTube) — use `attr`/`select` instead
252
+ - `eval` with `input.value = ...` or `.click()` bypasses SPA framework state — use `type`/`click` instead
186
253
  - `screenshot` captures visible viewport only — scroll for full-page capture
187
254
  - Without `-i`, `click` always hits the FIRST match — use `count` to check first
188
255
 
@@ -678,7 +678,25 @@ def args_to_action_params(cmd: str, args) -> tuple[str, dict]:
678
678
  # ---------------------------------------------------------------------------
679
679
 
680
680
 
681
+ def _ensure_utf8_stdio():
682
+ """Reconfigure stdout/stderr to UTF-8 on Windows.
683
+
684
+ Windows console defaults to the system code page (e.g. CP936 for Chinese
685
+ locales), which causes garbled output when printing non-ASCII characters
686
+ via ``json.dumps(ensure_ascii=False)``. Calling ``reconfigure`` fixes
687
+ this for all subsequent ``print()`` calls.
688
+ """
689
+ if sys.platform == "win32":
690
+ for stream in (sys.stdout, sys.stderr):
691
+ if hasattr(stream, "reconfigure"):
692
+ try:
693
+ stream.reconfigure(encoding="utf-8")
694
+ except Exception:
695
+ pass
696
+
697
+
681
698
  def main():
699
+ _ensure_utf8_stdio()
682
700
  parser = build_parser()
683
701
  args = parser.parse_args()
684
702
 
@@ -721,6 +739,17 @@ def main():
721
739
 
722
740
  # Standard command: parse args, send to server
723
741
  action, params = args_to_action_params(cmd, args)
742
+
743
+ # Handle pure sleep locally — avoids extension round-trip which can
744
+ # timeout on heavy SPA pages (YouTube, etc.) where the service worker
745
+ # becomes unresponsive during page load.
746
+ if action == "wait" and "seconds" in params:
747
+ import time
748
+ seconds = params["seconds"]
749
+ time.sleep(seconds)
750
+ print(json.dumps({"success": True, "data": {"waited": seconds}}))
751
+ return
752
+
724
753
  _client().send_command(action, params)
725
754
 
726
755
 
@@ -316,22 +316,29 @@ async function doReload() {
316
316
 
317
317
  async function doTabs() {
318
318
  const tabs = await chrome.tabs.query({});
319
+ // Get focused window ID for multi-window awareness
320
+ const focusedWindow = await chrome.windows.getLastFocused();
319
321
  return {
320
322
  tabs: tabs.map((t) => ({
321
323
  id: t.id,
322
324
  url: t.url,
323
325
  title: t.title,
324
326
  active: t.active,
327
+ windowId: t.windowId,
325
328
  })),
329
+ focusedWindowId: focusedWindow.id,
326
330
  };
327
331
  }
328
332
 
329
333
  async function doSwitchTab(params) {
330
334
  const tabId = parseInt(params.id, 10);
331
335
  if (isNaN(tabId)) throw new Error("Missing or invalid 'id' parameter");
332
- await chrome.tabs.update(tabId, { active: true });
333
336
  const tab = await chrome.tabs.get(tabId);
334
- return { id: tab.id, url: tab.url, title: tab.title };
337
+ // Focus the window containing this tab first (critical for multi-window setups)
338
+ await chrome.windows.update(tab.windowId, { focused: true });
339
+ await chrome.tabs.update(tabId, { active: true });
340
+ const updated = await chrome.tabs.get(tabId);
341
+ return { id: updated.id, url: updated.url, title: updated.title, windowId: updated.windowId };
335
342
  }
336
343
 
337
344
  async function doNewTab(params) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser-Ctl",
4
- "version": "0.2.6",
4
+ "version": "0.2.8",
5
5
  "description": "Developer tool for CLI-driven browser automation. Control Chrome via command-line — navigate, click, type, query DOM, capture screenshots, and download files, all through a local WebSocket bridge.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browser-ctl
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Control your browser from the command line via a Chrome extension + WebSocket bridge
5
5
  Author-email: geb <853934146@qq.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "browser-ctl"
3
- version = "0.2.6"
3
+ version = "0.2.8"
4
4
  description = "Control your browser from the command line via a Chrome extension + WebSocket bridge"
5
5
  readme = "README.md"
6
6
  license = "MIT"
File without changes
File without changes
File without changes