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.
Files changed (103) hide show
  1. browserwright-0.6.2/PKG-INFO +12 -0
  2. browserwright-0.6.2/README.md +346 -0
  3. browserwright-0.6.2/pyproject.toml +71 -0
  4. browserwright-0.6.2/setup.cfg +4 -0
  5. browserwright-0.6.2/src/browserwright/__init__.py +33 -0
  6. browserwright-0.6.2/src/browserwright/__main__.py +6 -0
  7. browserwright-0.6.2/src/browserwright/_executor/__init__.py +47 -0
  8. browserwright-0.6.2/src/browserwright/_executor/__main__.py +9 -0
  9. browserwright-0.6.2/src/browserwright/_executor/client.py +127 -0
  10. browserwright-0.6.2/src/browserwright/_executor/process.py +652 -0
  11. browserwright-0.6.2/src/browserwright/_executor/protocol.py +152 -0
  12. browserwright-0.6.2/src/browserwright/api.py +66 -0
  13. browserwright-0.6.2/src/browserwright/cdp.py +285 -0
  14. browserwright-0.6.2/src/browserwright/cli.py +741 -0
  15. browserwright-0.6.2/src/browserwright/daemon/__init__.py +8 -0
  16. browserwright-0.6.2/src/browserwright/daemon/_ipc.py +444 -0
  17. browserwright-0.6.2/src/browserwright/daemon/active_tab.py +183 -0
  18. browserwright-0.6.2/src/browserwright/daemon/auth.py +395 -0
  19. browserwright-0.6.2/src/browserwright/daemon/backends/__init__.py +59 -0
  20. browserwright-0.6.2/src/browserwright/daemon/backends/base.py +120 -0
  21. browserwright-0.6.2/src/browserwright/daemon/backends/cloud.py +222 -0
  22. browserwright-0.6.2/src/browserwright/daemon/backends/env.py +119 -0
  23. browserwright-0.6.2/src/browserwright/daemon/backends/extension.py +185 -0
  24. browserwright-0.6.2/src/browserwright/daemon/backends/rdp.py +214 -0
  25. browserwright-0.6.2/src/browserwright/daemon/cli.py +1437 -0
  26. browserwright-0.6.2/src/browserwright/daemon/config.py +380 -0
  27. browserwright-0.6.2/src/browserwright/daemon/doctor.py +179 -0
  28. browserwright-0.6.2/src/browserwright/daemon/errors.py +34 -0
  29. browserwright-0.6.2/src/browserwright/daemon/launch_chrome.py +353 -0
  30. browserwright-0.6.2/src/browserwright/daemon/observability.py +181 -0
  31. browserwright-0.6.2/src/browserwright/daemon/platforms.py +234 -0
  32. browserwright-0.6.2/src/browserwright/daemon/resolver.py +72 -0
  33. browserwright-0.6.2/src/browserwright/daemon/server/__init__.py +6 -0
  34. browserwright-0.6.2/src/browserwright/daemon/server/daemon.py +229 -0
  35. browserwright-0.6.2/src/browserwright/daemon/server/executor_registry.py +434 -0
  36. browserwright-0.6.2/src/browserwright/daemon/server/extension_upstream.py +677 -0
  37. browserwright-0.6.2/src/browserwright/daemon/server/facade.py +375 -0
  38. browserwright-0.6.2/src/browserwright/daemon/server/facade_extension.py +969 -0
  39. browserwright-0.6.2/src/browserwright/daemon/server/listener.py +1058 -0
  40. browserwright-0.6.2/src/browserwright/daemon/server/proxy.py +1991 -0
  41. browserwright-0.6.2/src/browserwright/daemon/server/relay.py +783 -0
  42. browserwright-0.6.2/src/browserwright/daemon/server/state.py +432 -0
  43. browserwright-0.6.2/src/browserwright/daemon/server/upstream.py +266 -0
  44. browserwright-0.6.2/src/browserwright/daemon/userscripts.py +150 -0
  45. browserwright-0.6.2/src/browserwright/discovery.py +213 -0
  46. browserwright-0.6.2/src/browserwright/errors.py +177 -0
  47. browserwright-0.6.2/src/browserwright/health.py +169 -0
  48. browserwright-0.6.2/src/browserwright/install.py +628 -0
  49. browserwright-0.6.2/src/browserwright/memory/__init__.py +15 -0
  50. browserwright-0.6.2/src/browserwright/memory/_md.py +120 -0
  51. browserwright-0.6.2/src/browserwright/memory/_yaml.py +217 -0
  52. browserwright-0.6.2/src/browserwright/memory/global_mem.py +201 -0
  53. browserwright-0.6.2/src/browserwright/memory/repl_mem.py +28 -0
  54. browserwright-0.6.2/src/browserwright/memory/session_decisions.py +53 -0
  55. browserwright-0.6.2/src/browserwright/memory/site_mem.py +381 -0
  56. browserwright-0.6.2/src/browserwright/mode_b_client.py +590 -0
  57. browserwright-0.6.2/src/browserwright/multitask.py +131 -0
  58. browserwright-0.6.2/src/browserwright/output_schema.py +99 -0
  59. browserwright-0.6.2/src/browserwright/primitives/__init__.py +67 -0
  60. browserwright-0.6.2/src/browserwright/primitives/discovery_api.py +79 -0
  61. browserwright-0.6.2/src/browserwright/primitives/http.py +42 -0
  62. browserwright-0.6.2/src/browserwright/primitives/inspect.py +876 -0
  63. browserwright-0.6.2/src/browserwright/primitives/interact.py +518 -0
  64. browserwright-0.6.2/src/browserwright/primitives/page.py +556 -0
  65. browserwright-0.6.2/src/browserwright/primitives/site.py +143 -0
  66. browserwright-0.6.2/src/browserwright/release_install.py +466 -0
  67. browserwright-0.6.2/src/browserwright/repl/__init__.py +6 -0
  68. browserwright-0.6.2/src/browserwright/repl/_namespace.py +106 -0
  69. browserwright-0.6.2/src/browserwright/repl/_smart_goto.py +236 -0
  70. browserwright-0.6.2/src/browserwright/repl/inline.py +180 -0
  71. browserwright-0.6.2/src/browserwright/repl/playwright_handle.py +449 -0
  72. browserwright-0.6.2/src/browserwright/repl/snapshot.py +150 -0
  73. browserwright-0.6.2/src/browserwright/session.py +229 -0
  74. browserwright-0.6.2/src/browserwright/session_create.py +252 -0
  75. browserwright-0.6.2/src/browserwright/session_ctx.py +24 -0
  76. browserwright-0.6.2/src/browserwright/session_registry.py +133 -0
  77. browserwright-0.6.2/src/browserwright/session_runtime.py +133 -0
  78. browserwright-0.6.2/src/browserwright/site_skills_starter/github.com/SKILL.md +14 -0
  79. browserwright-0.6.2/src/browserwright/site_skills_starter/github.com/memory.md +29 -0
  80. browserwright-0.6.2/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +55 -0
  81. browserwright-0.6.2/src/browserwright/site_skills_starter/google.com/SKILL.md +16 -0
  82. browserwright-0.6.2/src/browserwright/site_skills_starter/google.com/memory.md +27 -0
  83. browserwright-0.6.2/src/browserwright/site_skills_starter/google.com/tasks/search.py +53 -0
  84. browserwright-0.6.2/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +7 -0
  85. browserwright-0.6.2/src/browserwright/site_skills_starter/producthunt.com/memory.md +26 -0
  86. browserwright-0.6.2/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +64 -0
  87. browserwright-0.6.2/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +7 -0
  88. browserwright-0.6.2/src/browserwright/site_skills_starter/wikipedia.org/memory.md +22 -0
  89. browserwright-0.6.2/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +55 -0
  90. browserwright-0.6.2/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +8 -0
  91. browserwright-0.6.2/src/browserwright/site_skills_starter/ycombinator.com/memory.md +25 -0
  92. browserwright-0.6.2/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +63 -0
  93. browserwright-0.6.2/src/browserwright/skill_doc.py +140 -0
  94. browserwright-0.6.2/src/browserwright/skill_runtime.md +194 -0
  95. browserwright-0.6.2/src/browserwright/subscriptions.py +213 -0
  96. browserwright-0.6.2/src/browserwright/task_runner.py +125 -0
  97. browserwright-0.6.2/src/browserwright/version.py +117 -0
  98. browserwright-0.6.2/src/browserwright.egg-info/PKG-INFO +12 -0
  99. browserwright-0.6.2/src/browserwright.egg-info/SOURCES.txt +101 -0
  100. browserwright-0.6.2/src/browserwright.egg-info/dependency_links.txt +1 -0
  101. browserwright-0.6.2/src/browserwright.egg-info/entry_points.txt +3 -0
  102. browserwright-0.6.2/src/browserwright.egg-info/requires.txt +8 -0
  103. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,6 @@
1
+ """Allow ``python -m browserwright ...`` as an alternate entry point."""
2
+ from .cli import main
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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,9 @@
1
+ """``python -m browserwright._executor --session <id>`` entrypoint."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ from .process import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
@@ -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")