browserwright 0.6.2__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.
- browserwright-0.6.2/PKG-INFO +12 -0
- browserwright-0.6.2/README.md +346 -0
- browserwright-0.6.2/pyproject.toml +71 -0
- browserwright-0.6.2/setup.cfg +4 -0
- browserwright-0.6.2/src/browserwright/__init__.py +33 -0
- browserwright-0.6.2/src/browserwright/__main__.py +6 -0
- browserwright-0.6.2/src/browserwright/_executor/__init__.py +47 -0
- browserwright-0.6.2/src/browserwright/_executor/__main__.py +9 -0
- browserwright-0.6.2/src/browserwright/_executor/client.py +127 -0
- browserwright-0.6.2/src/browserwright/_executor/process.py +652 -0
- browserwright-0.6.2/src/browserwright/_executor/protocol.py +152 -0
- browserwright-0.6.2/src/browserwright/api.py +66 -0
- browserwright-0.6.2/src/browserwright/cdp.py +285 -0
- browserwright-0.6.2/src/browserwright/cli.py +741 -0
- browserwright-0.6.2/src/browserwright/daemon/__init__.py +8 -0
- browserwright-0.6.2/src/browserwright/daemon/_ipc.py +444 -0
- browserwright-0.6.2/src/browserwright/daemon/active_tab.py +183 -0
- browserwright-0.6.2/src/browserwright/daemon/auth.py +395 -0
- browserwright-0.6.2/src/browserwright/daemon/backends/__init__.py +59 -0
- browserwright-0.6.2/src/browserwright/daemon/backends/base.py +120 -0
- browserwright-0.6.2/src/browserwright/daemon/backends/cloud.py +222 -0
- browserwright-0.6.2/src/browserwright/daemon/backends/env.py +119 -0
- browserwright-0.6.2/src/browserwright/daemon/backends/extension.py +185 -0
- browserwright-0.6.2/src/browserwright/daemon/backends/rdp.py +214 -0
- browserwright-0.6.2/src/browserwright/daemon/cli.py +1437 -0
- browserwright-0.6.2/src/browserwright/daemon/config.py +380 -0
- browserwright-0.6.2/src/browserwright/daemon/doctor.py +179 -0
- browserwright-0.6.2/src/browserwright/daemon/errors.py +34 -0
- browserwright-0.6.2/src/browserwright/daemon/launch_chrome.py +353 -0
- browserwright-0.6.2/src/browserwright/daemon/observability.py +181 -0
- browserwright-0.6.2/src/browserwright/daemon/platforms.py +234 -0
- browserwright-0.6.2/src/browserwright/daemon/resolver.py +72 -0
- browserwright-0.6.2/src/browserwright/daemon/server/__init__.py +6 -0
- browserwright-0.6.2/src/browserwright/daemon/server/daemon.py +229 -0
- browserwright-0.6.2/src/browserwright/daemon/server/executor_registry.py +434 -0
- browserwright-0.6.2/src/browserwright/daemon/server/extension_upstream.py +677 -0
- browserwright-0.6.2/src/browserwright/daemon/server/facade.py +375 -0
- browserwright-0.6.2/src/browserwright/daemon/server/facade_extension.py +969 -0
- browserwright-0.6.2/src/browserwright/daemon/server/listener.py +1058 -0
- browserwright-0.6.2/src/browserwright/daemon/server/proxy.py +1991 -0
- browserwright-0.6.2/src/browserwright/daemon/server/relay.py +783 -0
- browserwright-0.6.2/src/browserwright/daemon/server/state.py +432 -0
- browserwright-0.6.2/src/browserwright/daemon/server/upstream.py +266 -0
- browserwright-0.6.2/src/browserwright/daemon/userscripts.py +150 -0
- browserwright-0.6.2/src/browserwright/discovery.py +213 -0
- browserwright-0.6.2/src/browserwright/errors.py +177 -0
- browserwright-0.6.2/src/browserwright/health.py +169 -0
- browserwright-0.6.2/src/browserwright/install.py +628 -0
- browserwright-0.6.2/src/browserwright/memory/__init__.py +15 -0
- browserwright-0.6.2/src/browserwright/memory/_md.py +120 -0
- browserwright-0.6.2/src/browserwright/memory/_yaml.py +217 -0
- browserwright-0.6.2/src/browserwright/memory/global_mem.py +201 -0
- browserwright-0.6.2/src/browserwright/memory/repl_mem.py +28 -0
- browserwright-0.6.2/src/browserwright/memory/session_decisions.py +53 -0
- browserwright-0.6.2/src/browserwright/memory/site_mem.py +381 -0
- browserwright-0.6.2/src/browserwright/mode_b_client.py +590 -0
- browserwright-0.6.2/src/browserwright/multitask.py +131 -0
- browserwright-0.6.2/src/browserwright/output_schema.py +99 -0
- browserwright-0.6.2/src/browserwright/primitives/__init__.py +67 -0
- browserwright-0.6.2/src/browserwright/primitives/discovery_api.py +79 -0
- browserwright-0.6.2/src/browserwright/primitives/http.py +42 -0
- browserwright-0.6.2/src/browserwright/primitives/inspect.py +876 -0
- browserwright-0.6.2/src/browserwright/primitives/interact.py +518 -0
- browserwright-0.6.2/src/browserwright/primitives/page.py +556 -0
- browserwright-0.6.2/src/browserwright/primitives/site.py +143 -0
- browserwright-0.6.2/src/browserwright/release_install.py +466 -0
- browserwright-0.6.2/src/browserwright/repl/__init__.py +6 -0
- browserwright-0.6.2/src/browserwright/repl/_namespace.py +106 -0
- browserwright-0.6.2/src/browserwright/repl/_smart_goto.py +236 -0
- browserwright-0.6.2/src/browserwright/repl/inline.py +180 -0
- browserwright-0.6.2/src/browserwright/repl/playwright_handle.py +449 -0
- browserwright-0.6.2/src/browserwright/repl/snapshot.py +150 -0
- browserwright-0.6.2/src/browserwright/session.py +229 -0
- browserwright-0.6.2/src/browserwright/session_create.py +252 -0
- browserwright-0.6.2/src/browserwright/session_ctx.py +24 -0
- browserwright-0.6.2/src/browserwright/session_registry.py +133 -0
- browserwright-0.6.2/src/browserwright/session_runtime.py +133 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/github.com/SKILL.md +14 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/github.com/memory.md +29 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +55 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/google.com/SKILL.md +16 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/google.com/memory.md +27 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/google.com/tasks/search.py +53 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +7 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/producthunt.com/memory.md +26 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +64 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +7 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/wikipedia.org/memory.md +22 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +55 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +8 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/ycombinator.com/memory.md +25 -0
- browserwright-0.6.2/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +63 -0
- browserwright-0.6.2/src/browserwright/skill_doc.py +140 -0
- browserwright-0.6.2/src/browserwright/skill_runtime.md +194 -0
- browserwright-0.6.2/src/browserwright/subscriptions.py +213 -0
- browserwright-0.6.2/src/browserwright/task_runner.py +125 -0
- browserwright-0.6.2/src/browserwright/version.py +117 -0
- browserwright-0.6.2/src/browserwright.egg-info/PKG-INFO +12 -0
- browserwright-0.6.2/src/browserwright.egg-info/SOURCES.txt +101 -0
- browserwright-0.6.2/src/browserwright.egg-info/dependency_links.txt +1 -0
- browserwright-0.6.2/src/browserwright.egg-info/entry_points.txt +3 -0
- browserwright-0.6.2/src/browserwright.egg-info/requires.txt +8 -0
- browserwright-0.6.2/src/browserwright.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: browserwright
|
|
3
|
+
Version: 0.6.2
|
|
4
|
+
Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: cdp-use==1.4.5
|
|
7
|
+
Requires-Dist: websockets==15.0.1
|
|
8
|
+
Requires-Dist: pillow==12.2.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: playwright>=1.60.0
|
|
11
|
+
Provides-Extra: ux
|
|
12
|
+
Requires-Dist: rich>=13; extra == "ux"
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# browserwright
|
|
2
|
+
|
|
3
|
+
Let an AI/code agent drive a real (or isolated) Chrome from the terminal over CDP — open pages, click, type, fill forms, scrape, screenshot — and author **userscripts** the browser runs on matching sites.
|
|
4
|
+
|
|
5
|
+
One installable package, two CLIs that work together:
|
|
6
|
+
|
|
7
|
+
- **`browserwright-daemon`** (Layer 1) — resolves a Chrome CDP WebSocket URL and proxies it. Backends: `env / rdp / extension / cloud`. Also `launch-chrome` to spawn an isolated Chrome.
|
|
8
|
+
- **`browserwright`** (Layer 2) — the agent-facing surface: sessions, heredoc scripting with pre-imported primitives, reusable site tasks, memory, and userscript management.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
.
|
|
12
|
+
├── src/browserwright/ the package
|
|
13
|
+
│ ├── … Layer 2 — sessions / primitives / site skills / memory / userscripts
|
|
14
|
+
│ └── daemon/ Layer 1 — CDP URL resolver + proxy (env/rdp/extension/cloud backends)
|
|
15
|
+
├── chrome-extension/ unpacked relay extension for the `extension` backend
|
|
16
|
+
├── skill/ Agent skill bundle (symlinked into Claude Code, Codex, and Pi)
|
|
17
|
+
├── tests/{skill,daemon}/ test suites
|
|
18
|
+
├── docs/ deeper docs (skill.md, daemon.md, session-model.md, …)
|
|
19
|
+
└── browser-connection.md why this stack exists (CDP discovery, Chrome 144+ popups)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- macOS or Linux
|
|
25
|
+
- Python 3.11 (`brew install python@3.11` / `pyenv install 3.11`)
|
|
26
|
+
- Chrome / Chromium (any flavor)
|
|
27
|
+
- `~/.local/bin` on `$PATH`
|
|
28
|
+
- [`uv`](https://docs.astral.sh/uv/) — manages the venv, lockfile, and Python toolchain (`uv` can fetch Python 3.11 itself)
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
### PyPI / global tool install
|
|
33
|
+
|
|
34
|
+
The distribution target is one PyPI package named `browserwright`. A global tool
|
|
35
|
+
install should expose both CLIs:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pipx install browserwright
|
|
39
|
+
# or
|
|
40
|
+
uv tool install browserwright
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After that, install the one global daemon service:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
browserwright-daemon install
|
|
47
|
+
browserwright-daemon status
|
|
48
|
+
browserwright-daemon doctor
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
On macOS, `browserwright-daemon install` registers the daemon as a LaunchAgent
|
|
52
|
+
at `~/Library/LaunchAgents/com.browserwright-daemon.plist`, starts it on login,
|
|
53
|
+
and keeps the single daemon socket at:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
${XDG_RUNTIME_DIR:-/tmp}/browserwright-daemon.sock
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Linux currently does not auto-install a service; run `browserwright-daemon serve`
|
|
60
|
+
from your own systemd-user unit or process supervisor.
|
|
61
|
+
|
|
62
|
+
The browser extension is intentionally not part of the PyPI packaging contract.
|
|
63
|
+
The long-term user path is to publish/install it through the Chrome Web Store.
|
|
64
|
+
Until then, local development and release installs can still use the repo's
|
|
65
|
+
`chrome-extension/` directory or the copied local release extension described
|
|
66
|
+
below.
|
|
67
|
+
|
|
68
|
+
The agent skill bundle remains a thin shell around the installed CLI. For
|
|
69
|
+
local release installs, the release installer symlinks that shell into Claude
|
|
70
|
+
Code, Codex, and Pi. For PyPI installs, the intended stable contract is:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
browserwright --print-skill
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Agents should read the runtime guide generated by the installed package version,
|
|
77
|
+
so the skill instructions stay version-locked to the CLI that actually runs.
|
|
78
|
+
|
|
79
|
+
The global install is considered healthy when these pass:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
browserwright version check
|
|
83
|
+
browserwright-daemon version check
|
|
84
|
+
browserwright-daemon doctor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If the daemon was already running when you upgraded the global tool, restart it:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
browserwright-daemon restart
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If it is running in the foreground instead of as the macOS LaunchAgent:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
browserwright-daemon stop
|
|
97
|
+
browserwright-daemon serve
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### PyPI release flow
|
|
101
|
+
|
|
102
|
+
PyPI releases are driven by git tags. For published artifacts, the git tag is
|
|
103
|
+
the source of truth for the package version; the release workflow rewrites
|
|
104
|
+
`pyproject.toml` to that tag version before building the sdist and wheel.
|
|
105
|
+
|
|
106
|
+
After a PR has landed on `main`, publish by pushing a version tag:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
git checkout main
|
|
110
|
+
git pull
|
|
111
|
+
git tag v0.6.3
|
|
112
|
+
git push origin v0.6.3
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The GitHub Action only runs for `v*` tags. A tag like `v0.6.3` publishes package
|
|
116
|
+
version `0.6.3`.
|
|
117
|
+
|
|
118
|
+
### Local immutable release install
|
|
119
|
+
|
|
120
|
+
From the repo root. If you have [`mise`](https://mise.jdx.dev/), install an immutable local release:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
mise run install-release
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This builds a wheel, installs it into:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
~/.local/share/browserwright/releases/<version>/
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
and points global entry points at that release copy:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
~/.local/bin/browserwright -> releases/<version>/.venv/bin/browserwright
|
|
136
|
+
~/.local/bin/browserwright-daemon -> releases/<version>/.venv/bin/browserwright-daemon
|
|
137
|
+
~/.claude/skills/browserwright -> releases/<version>/skill
|
|
138
|
+
~/.codex/skills/browserwright -> releases/<version>/skill
|
|
139
|
+
~/.pi/agent/skills/browserwright -> releases/<version>/skill
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The global install does not point at the development checkout, so broken in-progress edits do not break agents already using browserwright.
|
|
143
|
+
|
|
144
|
+
Useful release commands:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
browserwright release status
|
|
148
|
+
browserwright release list
|
|
149
|
+
browserwright release activate <version>
|
|
150
|
+
mise run version-check
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The skill bundle is intentionally a thin shell. The authoritative agent instructions come from `browserwright --print-skill`, so installed agents read docs generated by the installed CLI in the active release.
|
|
154
|
+
|
|
155
|
+
For development only, `mise run dev-link` links the current checkout into global PATH and skill dirs. That is convenient for debugging but can break global agents if the checkout is broken.
|
|
156
|
+
|
|
157
|
+
## Update the local global install
|
|
158
|
+
|
|
159
|
+
Use this flow when you want the global Code Agent install on this machine to pick up the current repo state:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# 1. Build a non-editable release and atomically update global symlinks.
|
|
163
|
+
mise run install-release
|
|
164
|
+
|
|
165
|
+
# 2. Verify the active release, CLI, daemon, generated skill, and extension metadata.
|
|
166
|
+
browserwright release status
|
|
167
|
+
browserwright version check
|
|
168
|
+
browserwright-daemon version check
|
|
169
|
+
|
|
170
|
+
# 3. Restart the daemon if release status says the running daemon is stale.
|
|
171
|
+
browserwright-daemon restart
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If the daemon is running manually instead of as the macOS LaunchAgent, restart it manually:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
browserwright-daemon stop
|
|
178
|
+
browserwright-daemon serve
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The release installer also copies the unpacked Chrome extension into the stable local load path:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
/Users/metajs/Library/Mobile Documents/com~apple~CloudDocs/etc/chrome-extension/browserwright
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
If `install-release` reports `reload_chrome_extension: true`, open `chrome://extensions/` and reload the `browserwright` unpacked extension from that stable path. The path does not change across releases; the installer overwrites its contents.
|
|
188
|
+
|
|
189
|
+
## Local release discipline
|
|
190
|
+
|
|
191
|
+
For local immutable release installs, `pyproject.toml` is the semver source used
|
|
192
|
+
by the local build. For PyPI releases, the git tag is the source of truth and
|
|
193
|
+
the publish workflow writes that version into `pyproject.toml` before building.
|
|
194
|
+
The Python runtime, daemon runtime, generated skill document, and extension
|
|
195
|
+
checks all read or compare against the built package version.
|
|
196
|
+
|
|
197
|
+
After installing a release:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
browserwright version check
|
|
201
|
+
browserwright-daemon version check
|
|
202
|
+
browserwright-daemon restart # when installed as the macOS LaunchAgent
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If `browserwright release install-local` reports that `chrome_extension` changed, reload the unpacked extension in Chrome from the active release's `chrome-extension/` path. Chrome does not allow this repo to refresh the extension automatically.
|
|
206
|
+
|
|
207
|
+
## Smoke test
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Start an isolated Chrome (own profile dir, won't touch your daily Chrome)
|
|
211
|
+
browserwright-daemon launch-chrome --port 9333 --profile bs-smoke --persistent --json
|
|
212
|
+
|
|
213
|
+
# Drive it
|
|
214
|
+
BD_PORT=9333 BD_BACKEND=rdp browserwright <<'PY'
|
|
215
|
+
page.goto("https://example.com", wait_until="load")
|
|
216
|
+
print(f"URL: {page.url}")
|
|
217
|
+
print(f"Title: {page.title()}")
|
|
218
|
+
PY
|
|
219
|
+
# expected:
|
|
220
|
+
# URL: https://example.com/
|
|
221
|
+
# Title: Example Domain
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Clean up: `kill <pid>` (the `launch-chrome --json` output includes the pid).
|
|
225
|
+
|
|
226
|
+
## Usage
|
|
227
|
+
|
|
228
|
+
### Sessions: create once, pass everywhere
|
|
229
|
+
|
|
230
|
+
A **session** is the isolation key that lets multiple agents drive browsers without colliding. Create one, then every later call carries its id (via `--session` or `BD_SESSION`):
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
sid=$(browserwright session new --backend=extension)
|
|
234
|
+
BD_SESSION=$sid browserwright <<'PY'
|
|
235
|
+
page.goto("https://news.ycombinator.com", wait_until="load")
|
|
236
|
+
print(page.title())
|
|
237
|
+
PY
|
|
238
|
+
browserwright whoami --session=$sid
|
|
239
|
+
browserwright session end --session=$sid
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
A bare heredoc with no session/`BD_PORT` context exits 2 with guidance — the daemon is never silently shared.
|
|
243
|
+
|
|
244
|
+
One global daemon serves every session (fixed socket `browserwright-daemon.sock`; no per-instance name). The session's backend is fixed at `session new` and never changes. On `extension` the session's "browser" is a Chrome **tab group** (named after the session) inside the user's real Chrome; `session end` closes the whole group. On `rdp` the daemon launches and owns a dedicated, isolated Chrome (profile `bs-s<id>`) that dies with the session. **Isolation caveat:** rdp sessions get isolated profiles (separate cookies/storage), but extension tab groups isolate only the *tab set* — all extension sessions share the user's one profile, so they share cookies/login/origin storage with each other and with the user.
|
|
245
|
+
|
|
246
|
+
### Two invocation forms
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# (a) Inline heredoc — one-off scripts; drive the injected Playwright `page`
|
|
250
|
+
BD_SESSION=$sid browserwright <<'PY'
|
|
251
|
+
page.goto("https://news.ycombinator.com", wait_until="load")
|
|
252
|
+
print(page.title())
|
|
253
|
+
print(snapshot()) # [ref=eN] aria tree → page.locator("aria-ref=eN")
|
|
254
|
+
PY
|
|
255
|
+
|
|
256
|
+
# (b) Solidified task — reusable, pre-saved flow under ~/.browserwright/site-skills/<host>/tasks/
|
|
257
|
+
browserwright list-tasks
|
|
258
|
+
browserwright list-tasks --query="search the web"
|
|
259
|
+
browserwright task wikipedia.org/lookup --title="Wikipedia"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Choose a backend
|
|
263
|
+
|
|
264
|
+
| Scenario | Backend | How |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| Your daily Chrome (logged-in / personal) *(default for "use my browser")* | `extension` | `browserwright session new --backend=extension …` — load `chrome-extension/` once, connect via the daemon's relay; zero popups |
|
|
267
|
+
| Scripts / iterative work in throwaway profiles | `rdp` + isolated Chrome | `browserwright-daemon launch-chrome --port 9333 --profile bs-dev` + `BD_PORT=9333 BD_BACKEND=rdp` |
|
|
268
|
+
| Fingerprint browser (AdsPower / MultiLogin / 比特浏览器) | `rdp` | point `BD_PORT` at the tool's exposed port |
|
|
269
|
+
| Remote Chrome (Browser Use / Browserless / Hyperbrowser) | `cloud` | `browserwright-daemon serve --provider <name>` + auth env vars |
|
|
270
|
+
|
|
271
|
+
Interactive wizard: `browserwright install` — walks the decision tree and writes your pick.
|
|
272
|
+
|
|
273
|
+
### Userscripts
|
|
274
|
+
|
|
275
|
+
Author Tampermonkey-style scripts the `extension` backend injects on matching sites:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
browserwright userscript push ./greet.user.js --verify
|
|
279
|
+
browserwright userscript list
|
|
280
|
+
browserwright userscript toggle <id>
|
|
281
|
+
browserwright userscript logs <id>
|
|
282
|
+
browserwright userscript remove <id>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### The heredoc surface
|
|
286
|
+
|
|
287
|
+
Browser driving is **real synchronous Playwright**. Every heredoc gets three
|
|
288
|
+
names injected, already connected through the daemon's Playwright CDP facade:
|
|
289
|
+
|
|
290
|
+
- **`page`** — a Playwright `Page` bound to the session's current tab, **reused
|
|
291
|
+
across heredocs**. Navigate it in place (`page.goto`, `page.locator`,
|
|
292
|
+
`page.fill`, `page.click`, …); never `page.close()`.
|
|
293
|
+
- **`context`** — the Playwright `BrowserContext`. `context.new_page()` only when
|
|
294
|
+
you genuinely need a second tab.
|
|
295
|
+
- **`snapshot()`** — a first-party AI aria snapshot; each node carries a
|
|
296
|
+
`[ref=eN]` you act on via `page.locator("aria-ref=eN")`. Prefer this over
|
|
297
|
+
screenshots; re-`snapshot()` after each action (observe → act → observe).
|
|
298
|
+
|
|
299
|
+
The connection is lazy — a heredoc that only uses the helpers below opens no
|
|
300
|
+
browser. **Tab discipline:** reuse the bound tab, navigate in place, never
|
|
301
|
+
close the browser/context (those are the user's real tabs).
|
|
302
|
+
|
|
303
|
+
Non-browser helpers (also pre-imported):
|
|
304
|
+
|
|
305
|
+
- **HTTP (no browser, for static pages):** `http_get(url)`
|
|
306
|
+
- **Memory:** `remember`, `remember_global`, `remember_preference`, `memory_read`
|
|
307
|
+
- **Site-skills / tasks:** `list_site_skills`, `load_site_skill`, `run_task`, `run_tasks_concurrent`, `bootstrap_site`
|
|
308
|
+
|
|
309
|
+
Full catalogue and guidance in `skill/SKILL.md`.
|
|
310
|
+
|
|
311
|
+
### Diagnostics
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
browserwright-daemon doctor # which backends are live, why each is/isn't usable
|
|
315
|
+
browserwright-daemon list-backends
|
|
316
|
+
browserwright doctor # skill-side health
|
|
317
|
+
browserwright-daemon stats # observability counters when `serve` is running
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Agent integrations
|
|
321
|
+
|
|
322
|
+
The `skill/` directory is an agent skill bundle. Release install symlinks it into Claude Code, Codex, and Pi skill directories, and each symlink points at the active immutable release copy. The bundle is a stable shell; it tells the agent to run `browserwright --print-skill` for the version-locked runtime guide. Prompts like *"open example.com and screenshot it"*, *"scrape the HN front page"*, or *"write me a userscript that …"* trigger it automatically.
|
|
323
|
+
|
|
324
|
+
## Uninstall
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
rm ~/.local/bin/browserwright ~/.local/bin/browserwright-daemon
|
|
328
|
+
rm ~/.claude/skills/browserwright
|
|
329
|
+
rm ~/.codex/skills/browserwright
|
|
330
|
+
rm ~/.pi/agent/skills/browserwright
|
|
331
|
+
rm -rf .venv
|
|
332
|
+
rm -rf ~/.cache/browserwright-daemon ~/.browserwright
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Further reading
|
|
336
|
+
|
|
337
|
+
- `TESTING.md` — map of the test suites and how to run them
|
|
338
|
+
- `browser-connection.md` — *why* this stack exists (CDP discovery paths, Chrome 144+ popup mechanics)
|
|
339
|
+
- `docs/daemon.md` — backend internals, env vars, `config.toml`
|
|
340
|
+
- `docs/skill.md` — full primitive surface and release notes
|
|
341
|
+
- `docs/session-model.md` — the session isolation model
|
|
342
|
+
- `ONBOARDING.md` — contributor-oriented architecture tour
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
TBD — currently un-licensed source-available. Add a `LICENSE` file before publishing.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "browserwright"
|
|
7
|
+
version = "0.6.2"
|
|
8
|
+
description = "Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends)."
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"cdp-use==1.4.5",
|
|
12
|
+
"websockets==15.0.1",
|
|
13
|
+
"pillow==12.2.0",
|
|
14
|
+
"httpx>=0.27",
|
|
15
|
+
# Phase C: the skill heredoc injects a real Playwright `page`/`context`
|
|
16
|
+
# bound to the daemon facade, so playwright is a runtime dependency now (it
|
|
17
|
+
# was previously dev-only). `playwright install chromium` is still needed
|
|
18
|
+
# for tests that launch a browser; the daemon-facade path drives the
|
|
19
|
+
# daemon-resolved Chrome and does not need Playwright's bundled browser.
|
|
20
|
+
"playwright>=1.60.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
ux = ["rich>=13"]
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"coverage>=7",
|
|
29
|
+
"pytest>=8",
|
|
30
|
+
"pytest-asyncio>=0.23",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
browserwright = "browserwright.cli:main"
|
|
35
|
+
browserwright-daemon = "browserwright.daemon.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
package-dir = {"" = "src"}
|
|
39
|
+
include-package-data = true
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
browserwright = [
|
|
46
|
+
"site_skills_starter/**/*.md",
|
|
47
|
+
"site_skills_starter/**/*.py",
|
|
48
|
+
"skill_runtime.md",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.exclude-package-data]
|
|
52
|
+
browserwright = ["**/__pycache__/*", "**/*.pyc", "**/*.pyo"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
pythonpath = ["src"]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
markers = [
|
|
59
|
+
"real_chrome: end-to-end test that launches a real Chrome + extension (skipped unless explicitly selected)",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.coverage.run]
|
|
63
|
+
source = ["src/browserwright"]
|
|
64
|
+
omit = [
|
|
65
|
+
"src/browserwright/__main__.py",
|
|
66
|
+
"src/browserwright/site_skills_starter/*",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.coverage.report]
|
|
70
|
+
skip_covered = true
|
|
71
|
+
sort = "Cover"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""browserwright — Layer 2 of the browser stack.
|
|
2
|
+
|
|
3
|
+
Public surface: ``browserwright.cli:main`` is the CLI entry point.
|
|
4
|
+
|
|
5
|
+
For programmatic use inside REPL/task scripts, import names from the top-level
|
|
6
|
+
``browserwright`` namespace (everything from ``browserwright.api`` is re-exported
|
|
7
|
+
here)::
|
|
8
|
+
|
|
9
|
+
from browserwright import http_get, remember, run_task
|
|
10
|
+
|
|
11
|
+
Browser driving itself is done with real Playwright in inline ``-s/-e`` calls
|
|
12
|
+
via the injected ``page`` / ``context`` (and ``snapshot()``) — see
|
|
13
|
+
``repl/_namespace.build_globals``. Those are NOT importable from this module;
|
|
14
|
+
they are bound per call to the session's current tab.
|
|
15
|
+
"""
|
|
16
|
+
from .version import __version__ # noqa: F401
|
|
17
|
+
|
|
18
|
+
# Re-export the primitive namespace assembled in api.py so user scripts can
|
|
19
|
+
# `from browserwright import *`. The REPL/inline/task entry points use the same
|
|
20
|
+
# helper to populate their exec globals.
|
|
21
|
+
from .api import EXPORTS # noqa: F401
|
|
22
|
+
from .api import * # noqa: F401,F403
|
|
23
|
+
from .errors import ( # noqa: F401
|
|
24
|
+
AuthWall,
|
|
25
|
+
BrowserwrightError,
|
|
26
|
+
Captcha,
|
|
27
|
+
CDPError,
|
|
28
|
+
DaemonUnavailable,
|
|
29
|
+
ElementNotFound,
|
|
30
|
+
NeedsUserConfirm,
|
|
31
|
+
NetworkError,
|
|
32
|
+
PageLoadFailed,
|
|
33
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Phase B: the persistent per-session executor.
|
|
2
|
+
|
|
3
|
+
A resident, per-session **sync** subprocess (``python -m browserwright._executor
|
|
4
|
+
--session <id>``) that holds live Playwright ``page`` / ``context`` / ``browser``
|
|
5
|
+
+ a persistent ``state`` dict + one long-lived facade ``connect_over_cdp``
|
|
6
|
+
connection for its whole lifetime. The ``browserwright -s <id> -e <code>`` CLI
|
|
7
|
+
is a **thin client**: it ships the code body to the session's executor,
|
|
8
|
+
which runs it in a namespace where ``page`` / ``context`` / ``state`` are the
|
|
9
|
+
LIVE persistent objects, and returns the result.
|
|
10
|
+
|
|
11
|
+
Why a separate subprocess (not a thread in the asyncio daemon):
|
|
12
|
+
- sync Playwright is thread-affine and can't run on the daemon's event loop;
|
|
13
|
+
- agent code (infinite loop / segfault) crashing the privileged daemon — which
|
|
14
|
+
manages the user's real browser — is an unacceptable blast radius.
|
|
15
|
+
A per-session subprocess crashes only itself (D1 of the task).
|
|
16
|
+
|
|
17
|
+
Transport (Fork 2): the daemon owns the LIFECYCLE (spawn/discover via the
|
|
18
|
+
``ensureExecutor`` verb + an ``_ipc`` discovery file); the executor owns the
|
|
19
|
+
DATA PLANE — its own per-session unix socket speaking a simple length-framed
|
|
20
|
+
request/response of our design (``protocol.py``). The thin client connects
|
|
21
|
+
directly to that socket, keeping arbitrary code + large output OFF the daemon's
|
|
22
|
+
critical path.
|
|
23
|
+
|
|
24
|
+
Concurrency (Fork 3): a single dedicated worker thread owns the sync-Playwright
|
|
25
|
+
objects (thread-affine); the accept loop enqueues ``{code, timeout}`` requests
|
|
26
|
+
and the worker drains them FIFO (serial queue).
|
|
27
|
+
|
|
28
|
+
Status: PR1 (process skeleton + data plane), PR2 (daemon-side supervision —
|
|
29
|
+
idle reap / endSession kill / crash reap / orphan sweep), and PR3 (``reset()`` +
|
|
30
|
+
full output protocol: warnings / screenshots / truncation / traceback-bearing
|
|
31
|
+
errors + per-call timeout enforcement) are all in place.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from .protocol import (
|
|
36
|
+
ExecuteRequest,
|
|
37
|
+
ExecuteResponse,
|
|
38
|
+
recv_message,
|
|
39
|
+
send_message,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"ExecuteRequest",
|
|
44
|
+
"ExecuteResponse",
|
|
45
|
+
"recv_message",
|
|
46
|
+
"send_message",
|
|
47
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Thin-client side of the executor data plane.
|
|
2
|
+
|
|
3
|
+
Used by ``repl/inline.py`` when inline code touches ``page`` / ``context`` /
|
|
4
|
+
``snapshot`` / ``state`` / ``reset``: the whole code body is shipped to the
|
|
5
|
+
session's resident executor and the response is replayed locally.
|
|
6
|
+
|
|
7
|
+
Control plane (spawn + discover) goes through the daemon's
|
|
8
|
+
``BrowserwrightDaemon.ensureExecutor`` verb over the EXISTING mode_b socket
|
|
9
|
+
(tiny payload). The daemon spawns the executor if absent, waits for it to bind +
|
|
10
|
+
write its ``_ipc`` discovery file, and returns the socket path. The data plane
|
|
11
|
+
(this module) then connects DIRECTLY to that socket — keeping arbitrary code +
|
|
12
|
+
large output off the daemon's event loop (Fork 2).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import socket
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
from ..errors import BrowserwrightError
|
|
20
|
+
from .protocol import (
|
|
21
|
+
DEFAULT_TIMEOUT_MS,
|
|
22
|
+
ExecuteRequest,
|
|
23
|
+
ExecuteResponse,
|
|
24
|
+
recv_message,
|
|
25
|
+
send_message,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Generous slack added on TOP of the per-call timeout for the data-plane recv.
|
|
29
|
+
# The FIRST execute on a freshly-spawned executor triggers the lazy cold-start
|
|
30
|
+
# (connect_over_cdp + bind), which can take ~10-35s on a daemon-restart race
|
|
31
|
+
# (the executor's `_COLD_START_CONNECT_ATTEMPTS` backoff is ~10s; the registry's
|
|
32
|
+
# spawn-ready budget is 35s). The control-plane RPC no longer waits on that, so
|
|
33
|
+
# the wait moved HERE — the client's own blocking socket has no keepalive, so a
|
|
34
|
+
# long first call is fine as long as we don't time the recv out prematurely.
|
|
35
|
+
_COLD_START_RECV_SLACK_S = 45.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExecutorUnavailable(BrowserwrightError):
|
|
39
|
+
"""The session's executor could not be ensured/reached.
|
|
40
|
+
|
|
41
|
+
Surfaced when ``ensureExecutor`` fails or the executor socket can't be
|
|
42
|
+
connected — actionable: the daemon must be running (it spawns the
|
|
43
|
+
executor)."""
|
|
44
|
+
|
|
45
|
+
default_fix = ("ensure the daemon is running (`browserwright-daemon status "
|
|
46
|
+
"--json` should show `alive`); it lazily spawns the "
|
|
47
|
+
"per-session executor on first browser use.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_executor(sess) -> str:
|
|
51
|
+
"""Ask the daemon to ensure the session's executor and return its socket
|
|
52
|
+
path. Uses the session's mode_b CDP client (``sess.cdp``) to send the
|
|
53
|
+
control-plane verb."""
|
|
54
|
+
sid = _session_id(sess)
|
|
55
|
+
try:
|
|
56
|
+
# The browserwright session is already bound on the websocket query
|
|
57
|
+
# (`?session=<id>`). Do not pass it as CDP's top-level `sessionId`;
|
|
58
|
+
# that field means "attached target session" inside the proxy mux.
|
|
59
|
+
res = sess.cdp.send(
|
|
60
|
+
"BrowserwrightDaemon.ensureExecutor", bsSession=sid)
|
|
61
|
+
except Exception as e: # noqa: BLE001
|
|
62
|
+
raise ExecutorUnavailable(
|
|
63
|
+
f"ensureExecutor failed for session {sid!r}: {e}") from e
|
|
64
|
+
sock_path = res.get("exec_sock") if isinstance(res, dict) else None
|
|
65
|
+
if not isinstance(sock_path, str) or not sock_path:
|
|
66
|
+
raise ExecutorUnavailable(
|
|
67
|
+
f"ensureExecutor returned no socket for session {sid!r}: {res!r}")
|
|
68
|
+
return sock_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_on_executor(sess, code: str, *,
|
|
72
|
+
timeout_ms: int = DEFAULT_TIMEOUT_MS) -> ExecuteResponse:
|
|
73
|
+
"""Ship ``code`` to the session's executor and return its response.
|
|
74
|
+
|
|
75
|
+
Ensures the executor (control plane), connects its socket (data plane),
|
|
76
|
+
sends one :class:`ExecuteRequest`, reads one :class:`ExecuteResponse`.
|
|
77
|
+
|
|
78
|
+
The recv socket timeout is the per-call ``timeout_ms`` PLUS a cold-start
|
|
79
|
+
slack: the first execute on a fresh executor performs the lazy
|
|
80
|
+
connect_over_cdp + bind (moved off the control plane), which can add up to
|
|
81
|
+
~35s. The executor itself bounds the worker per-call timeout; this slack
|
|
82
|
+
only prevents the CLIENT recv from giving up before the executor replies."""
|
|
83
|
+
sock_path = ensure_executor(sess)
|
|
84
|
+
recv_timeout = max(timeout_ms, 1) / 1000.0 + _COLD_START_RECV_SLACK_S
|
|
85
|
+
conn = _connect(sock_path, timeout=recv_timeout)
|
|
86
|
+
try:
|
|
87
|
+
send_message(conn, ExecuteRequest(code=code, timeout_ms=timeout_ms).to_dict())
|
|
88
|
+
msg = recv_message(conn)
|
|
89
|
+
except (ConnectionError, OSError, ValueError) as e:
|
|
90
|
+
raise ExecutorUnavailable(
|
|
91
|
+
f"executor data-plane error on {sock_path!r}: {e}") from e
|
|
92
|
+
finally:
|
|
93
|
+
try:
|
|
94
|
+
conn.close()
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
return ExecuteResponse.from_dict(msg)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _connect(sock_path: str, *, timeout: float = 30.0,
|
|
101
|
+
retry_until: float = 5.0) -> socket.socket:
|
|
102
|
+
"""Connect the executor's unix socket, briefly retrying a not-yet-bound
|
|
103
|
+
socket (the daemon returns the path the moment it spawns; the bind may race
|
|
104
|
+
by a few ms)."""
|
|
105
|
+
deadline = time.monotonic() + retry_until
|
|
106
|
+
last: OSError | None = None
|
|
107
|
+
while True:
|
|
108
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
109
|
+
s.settimeout(timeout)
|
|
110
|
+
try:
|
|
111
|
+
s.connect(sock_path)
|
|
112
|
+
return s
|
|
113
|
+
except OSError as e:
|
|
114
|
+
last = e
|
|
115
|
+
s.close()
|
|
116
|
+
if time.monotonic() >= deadline:
|
|
117
|
+
raise ExecutorUnavailable(
|
|
118
|
+
f"could not connect executor socket {sock_path!r}: {e}"
|
|
119
|
+
) from last
|
|
120
|
+
time.sleep(0.05)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _session_id(sess) -> str:
|
|
124
|
+
rec = getattr(sess, "session_record", None)
|
|
125
|
+
if isinstance(rec, dict) and rec.get("id"):
|
|
126
|
+
return str(rec["id"])
|
|
127
|
+
raise ExecutorUnavailable("no session id bound; cannot reach an executor")
|