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.
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/PKG-INFO +1 -1
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/SKILL.md +70 -3
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/cli.py +29 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/background.js +9 -2
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/manifest.json +1 -1
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/PKG-INFO +1 -1
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/pyproject.toml +1 -1
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/LICENSE +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/README.md +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/__init__.py +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/__main__.py +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/client.py +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-128.png +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-16.png +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-32.png +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/extension/icon-48.png +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl/server.py +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/SOURCES.txt +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/dependency_links.txt +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/entry_points.txt +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/requires.txt +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/browser_ctl.egg-info/top_level.txt +0 -0
- {browser_ctl-0.2.6 → browser_ctl-0.2.8}/setup.cfg +0 -0
|
@@ -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. **
|
|
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
|
-
|
|
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.
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|